mirror of
https://github.com/dscyrescotti/Memola.git
synced 2026-05-11 10:20:04 +02:00
feat: wrap stroke object with element object
This commit is contained in:
@@ -82,6 +82,8 @@
|
||||
ECA738F62BE612B700A4542E /* MTLDevice++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738F52BE612B700A4542E /* MTLDevice++.swift */; };
|
||||
ECA738FC2BE61C5200A4542E /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738FB2BE61C5200A4542E /* Persistence.swift */; };
|
||||
ECA739082BE623F300A4542E /* PenDock.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA739072BE623F300A4542E /* PenDock.swift */; };
|
||||
ECD12A862C19EE3900B96E12 /* ElementObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECD12A852C19EE3900B96E12 /* ElementObject.swift */; };
|
||||
ECD12A8A2C19EFB000B96E12 /* Element.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECD12A892C19EFB000B96E12 /* Element.swift */; };
|
||||
ECE883BD2C00AA170045C53D /* EraserStroke.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECE883BC2C00AA170045C53D /* EraserStroke.swift */; };
|
||||
ECE883BF2C00AB440045C53D /* Stroke.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECE883BE2C00AB440045C53D /* Stroke.swift */; };
|
||||
ECE883C12C00C9CB0045C53D /* StrokeStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECE883C02C00C9CB0045C53D /* StrokeStyle.swift */; };
|
||||
@@ -172,6 +174,8 @@
|
||||
ECA738F52BE612B700A4542E /* MTLDevice++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MTLDevice++.swift"; sourceTree = "<group>"; };
|
||||
ECA738FB2BE61C5200A4542E /* Persistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = "<group>"; };
|
||||
ECA739072BE623F300A4542E /* PenDock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PenDock.swift; sourceTree = "<group>"; };
|
||||
ECD12A852C19EE3900B96E12 /* ElementObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementObject.swift; sourceTree = "<group>"; };
|
||||
ECD12A892C19EFB000B96E12 /* Element.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Element.swift; sourceTree = "<group>"; };
|
||||
ECE883BC2C00AA170045C53D /* EraserStroke.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EraserStroke.swift; sourceTree = "<group>"; };
|
||||
ECE883BE2C00AB440045C53D /* Stroke.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Stroke.swift; sourceTree = "<group>"; };
|
||||
ECE883C02C00C9CB0045C53D /* StrokeStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StrokeStyle.swift; sourceTree = "<group>"; };
|
||||
@@ -382,8 +386,8 @@
|
||||
ECA7387E2BE5FE4200A4542E /* Canvas */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ECD12A872C19EF8700B96E12 /* Elements */,
|
||||
EC2BEBF22C0F5FE1005DB0AF /* RTree */,
|
||||
ECA738F92BE6130000A4542E /* Geometries */,
|
||||
ECA738812BE5FEEE00A4542E /* Abstracts */,
|
||||
ECA738992BE6018900A4542E /* Buffers */,
|
||||
ECA738C72BE60EE200A4542E /* Contexts */,
|
||||
@@ -645,6 +649,23 @@
|
||||
path = Core;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
ECD12A872C19EF8700B96E12 /* Elements */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ECD12A882C19EF9500B96E12 /* Core */,
|
||||
ECA738F92BE6130000A4542E /* Geometries */,
|
||||
);
|
||||
path = Elements;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
ECD12A882C19EF9500B96E12 /* Core */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ECD12A892C19EFB000B96E12 /* Element.swift */,
|
||||
);
|
||||
path = Core;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
ECE883B82C009DC30045C53D /* Strokes */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -676,6 +697,7 @@
|
||||
EC0D14202BF79C73009BFE5F /* ToolObject.swift */,
|
||||
EC0D14252BF7A8C9009BFE5F /* PenObject.swift */,
|
||||
EC9AB09E2C1401A40076AF58 /* EraserObject.swift */,
|
||||
ECD12A852C19EE3900B96E12 /* ElementObject.swift */,
|
||||
);
|
||||
path = Objects;
|
||||
sourceTree = "<group>";
|
||||
@@ -805,6 +827,7 @@
|
||||
ECA738B82BE60DDC00A4542E /* HistoryEvent.swift in Sources */,
|
||||
EC0D14262BF7A8C9009BFE5F /* PenObject.swift in Sources */,
|
||||
ECA738952BE6012D00A4542E /* ViewPort.metal in Sources */,
|
||||
ECD12A862C19EE3900B96E12 /* ElementObject.swift in Sources */,
|
||||
EC0D14242BF79C98009BFE5F /* MemolaModel.xcdatamodeld in Sources */,
|
||||
ECA738F02BE6127700A4542E /* CGSize++.swift in Sources */,
|
||||
ECFA15242BEF223300455818 /* GraphicContextObject.swift in Sources */,
|
||||
@@ -840,6 +863,7 @@
|
||||
EC7F6BEC2BE5E6E300A34A7B /* MemolaApp.swift in Sources */,
|
||||
EC2BEBF82C0F601A005DB0AF /* Node.swift in Sources */,
|
||||
ECA738A02BE601E400A4542E /* ViewPortVertex.swift in Sources */,
|
||||
ECD12A8A2C19EFB000B96E12 /* Element.swift in Sources */,
|
||||
EC0D14282BF7BF20009BFE5F /* ContextMenuViewModifier.swift in Sources */,
|
||||
EC2106AD2C10C2A700FBE27C /* AnyStroke.swift in Sources */,
|
||||
ECA738BC2BE60E0300A4542E /* Tool.swift in Sources */,
|
||||
|
||||
@@ -11,7 +11,7 @@ import CoreData
|
||||
import Foundation
|
||||
|
||||
final class GraphicContext: @unchecked Sendable {
|
||||
var tree: RTree = RTree<AnyStroke>(maxEntries: 8)
|
||||
var tree: RTree = RTree<Element>(maxEntries: 8)
|
||||
var eraserStrokes: Set<EraserStroke> = []
|
||||
var object: GraphicContextObject?
|
||||
|
||||
@@ -48,9 +48,9 @@ final class GraphicContext: @unchecked Sendable {
|
||||
switch stroke.style {
|
||||
case .marker:
|
||||
guard let penStroke = stroke.stroke(as: PenStroke.self) else { return }
|
||||
tree.remove(penStroke.anyStroke, in: penStroke.strokeBox)
|
||||
tree.remove(penStroke.element, in: penStroke.strokeBox)
|
||||
withPersistence(\.backgroundContext) { [weak penStroke] context in
|
||||
penStroke?.object?.graphicContext = nil
|
||||
penStroke?.object?.element?.graphicContext = nil
|
||||
try context.saveIfNeeded()
|
||||
context.refreshAllObjects()
|
||||
}
|
||||
@@ -81,9 +81,9 @@ final class GraphicContext: @unchecked Sendable {
|
||||
guard let penStroke = stroke.stroke(as: PenStroke.self) else {
|
||||
break
|
||||
}
|
||||
tree.insert(penStroke.anyStroke, in: penStroke.strokeBox)
|
||||
tree.insert(penStroke.element, in: penStroke.strokeBox)
|
||||
withPersistence(\.backgroundContext) { [weak self, weak penStroke] context in
|
||||
penStroke?.object?.graphicContext = self?.object
|
||||
penStroke?.object?.element?.graphicContext = self?.object
|
||||
try context.saveIfNeeded()
|
||||
context.refreshAllObjects()
|
||||
}
|
||||
@@ -114,32 +114,43 @@ extension GraphicContext {
|
||||
guard let object else { return }
|
||||
let queue = OperationQueue()
|
||||
queue.qualityOfService = .userInteractive
|
||||
object.strokes.forEach { stroke in
|
||||
guard let stroke = stroke as? StrokeObject, stroke.style == 0 else { return }
|
||||
let _stroke = PenStroke(object: stroke)
|
||||
tree.insert(_stroke.anyStroke, in: _stroke.strokeBox)
|
||||
if _stroke.isVisible(in: bounds) {
|
||||
let id = stroke.objectID
|
||||
queue.addOperation { [weak self] in
|
||||
guard let self else { return }
|
||||
withPersistenceSync(\.newBackgroundContext) { [weak _stroke] context in
|
||||
guard let stroke = try? context.existingObject(with: id) as? StrokeObject else { return }
|
||||
_stroke?.loadQuads(from: stroke, with: self)
|
||||
object.elements.forEach { element in
|
||||
guard let element = element as? ElementObject else { return }
|
||||
switch element.type {
|
||||
case 0:
|
||||
guard let stroke = element.stroke, stroke.style == 0 else { return }
|
||||
let _stroke = PenStroke(object: stroke)
|
||||
tree.insert(_stroke.element, in: _stroke.strokeBox)
|
||||
if _stroke.isVisible(in: bounds) {
|
||||
let id = stroke.objectID
|
||||
queue.addOperation { [weak self] in
|
||||
guard let self else { return }
|
||||
withPersistenceSync(\.newBackgroundContext) { [weak _stroke] context in
|
||||
guard let stroke = try? context.existingObject(with: id) as? StrokeObject else { return }
|
||||
_stroke?.loadQuads(from: stroke, with: self)
|
||||
context.refreshAllObjects()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
withPersistence(\.backgroundContext) { [weak self, weak _stroke] context in
|
||||
guard let self else { return }
|
||||
_stroke?.loadQuads(with: self)
|
||||
context.refreshAllObjects()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
withPersistence(\.backgroundContext) { [weak self, weak _stroke] context in
|
||||
guard let self else { return }
|
||||
_stroke?.loadQuads(with: self)
|
||||
context.refreshAllObjects()
|
||||
}
|
||||
case 1:
|
||||
#warning("TODO: implement photo")
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
queue.waitUntilAllOperationsAreFinished()
|
||||
}
|
||||
|
||||
func loadQuads(_ bounds: CGRect, on context: NSManagedObjectContext) {
|
||||
#warning("TODO: implement photo")
|
||||
for _stroke in self.tree.search(box: bounds.box) {
|
||||
guard let stroke = _stroke.stroke(as: PenStroke.self), stroke.isEmpty else { continue }
|
||||
stroke.loadQuads(with: self)
|
||||
@@ -185,8 +196,13 @@ extension GraphicContext {
|
||||
stroke.createdAt = _stroke.createdAt
|
||||
stroke.quads = []
|
||||
stroke.erasers = .init()
|
||||
stroke.graphicContext = graphicContext
|
||||
graphicContext?.strokes.add(stroke)
|
||||
let element = ElementObject(\.backgroundContext)
|
||||
element.createdAt = _stroke.createdAt
|
||||
element.type = 0
|
||||
element.graphicContext = graphicContext
|
||||
stroke.element = element
|
||||
element.stroke = stroke
|
||||
graphicContext?.elements.add(element)
|
||||
_stroke.object = stroke
|
||||
try context.saveIfNeeded()
|
||||
}
|
||||
@@ -235,7 +251,7 @@ extension GraphicContext {
|
||||
currentStroke.finish(at: point)
|
||||
if let penStroke = currentStroke.stroke(as: PenStroke.self) {
|
||||
penStroke.saveQuads()
|
||||
tree.insert(currentStroke.anyStroke, in: currentStroke.strokeBox)
|
||||
tree.insert(currentStroke.element, in: currentStroke.strokeBox)
|
||||
withPersistence(\.backgroundContext) { [weak penStroke] context in
|
||||
guard let penStroke else { return }
|
||||
penStroke.object?.bounds = penStroke.bounds
|
||||
@@ -264,9 +280,8 @@ extension GraphicContext {
|
||||
guard let _stroke = stroke.stroke(as: PenStroke.self) else { break }
|
||||
withPersistence(\.backgroundContext) { [weak graphicContext = object, weak _stroke] context in
|
||||
guard let _stroke else { return }
|
||||
if let stroke = _stroke.object {
|
||||
graphicContext?.strokes.remove(stroke)
|
||||
context.delete(stroke)
|
||||
if let element = _stroke.object?.element {
|
||||
graphicContext?.elements.remove(element)
|
||||
}
|
||||
try context.saveIfNeeded()
|
||||
}
|
||||
|
||||
20
Memola/Canvas/Elements/Core/Element.swift
Normal file
20
Memola/Canvas/Elements/Core/Element.swift
Normal file
@@ -0,0 +1,20 @@
|
||||
//
|
||||
// Element.swift
|
||||
// Memola
|
||||
//
|
||||
// Created by Dscyre Scotti on 6/12/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum Element: Equatable, Comparable {
|
||||
case stroke(AnyStroke)
|
||||
case photo
|
||||
|
||||
func stroke<S: Stroke>(as type: S.Type) -> S? {
|
||||
guard case let .stroke(anyStroke) = self else {
|
||||
return nil
|
||||
}
|
||||
return anyStroke.stroke(as: type)
|
||||
}
|
||||
}
|
||||
@@ -125,4 +125,8 @@ extension Stroke {
|
||||
var anyStroke: AnyStroke {
|
||||
AnyStroke(self)
|
||||
}
|
||||
|
||||
var element: Element {
|
||||
.stroke(anyStroke)
|
||||
}
|
||||
}
|
||||
@@ -45,32 +45,37 @@ class GraphicRenderPass: RenderPass {
|
||||
let graphicContext = canvas.graphicContext
|
||||
if renderer.redrawsGraphicRender {
|
||||
canvas.setGraphicRenderType(.finished)
|
||||
for _stroke in graphicContext.tree.search(box: canvas.bounds.box) {
|
||||
let stroke = _stroke.value
|
||||
if graphicContext.previousStroke === stroke || graphicContext.currentStroke === stroke {
|
||||
continue
|
||||
}
|
||||
guard stroke.isVisible(in: canvas.bounds) else { continue }
|
||||
descriptor.colorAttachments[0].loadAction = clearsTexture ? .clear : .load
|
||||
clearsTexture = false
|
||||
switch stroke.style {
|
||||
case .eraser:
|
||||
eraserRenderPass.stroke = stroke
|
||||
eraserRenderPass.descriptor = descriptor
|
||||
eraserRenderPass.draw(on: canvas, with: renderer)
|
||||
case .marker:
|
||||
canvas.setGraphicRenderType(.finished)
|
||||
strokeRenderPass.stroke = stroke
|
||||
strokeRenderPass.graphicDescriptor = descriptor
|
||||
strokeRenderPass.graphicPipelineState = graphicPipelineState
|
||||
strokeRenderPass.draw(on: canvas, with: renderer)
|
||||
|
||||
if let stroke = stroke as? PenStroke, !stroke.isEmptyErasedQuads {
|
||||
descriptor.colorAttachments[0].loadAction = .load
|
||||
for _element in graphicContext.tree.search(box: canvas.bounds.box) {
|
||||
switch _element {
|
||||
case .stroke(let _stroke):
|
||||
let stroke = _stroke.value
|
||||
if graphicContext.previousStroke === stroke || graphicContext.currentStroke === stroke {
|
||||
continue
|
||||
}
|
||||
guard stroke.isVisible(in: canvas.bounds) else { continue }
|
||||
descriptor.colorAttachments[0].loadAction = clearsTexture ? .clear : .load
|
||||
clearsTexture = false
|
||||
switch stroke.style {
|
||||
case .eraser:
|
||||
eraserRenderPass.stroke = stroke
|
||||
eraserRenderPass.descriptor = descriptor
|
||||
eraserRenderPass.draw(on: canvas, with: renderer)
|
||||
case .marker:
|
||||
canvas.setGraphicRenderType(.finished)
|
||||
strokeRenderPass.stroke = stroke
|
||||
strokeRenderPass.graphicDescriptor = descriptor
|
||||
strokeRenderPass.graphicPipelineState = graphicPipelineState
|
||||
strokeRenderPass.draw(on: canvas, with: renderer)
|
||||
|
||||
if let stroke = stroke as? PenStroke, !stroke.isEmptyErasedQuads {
|
||||
descriptor.colorAttachments[0].loadAction = .load
|
||||
eraserRenderPass.stroke = stroke
|
||||
eraserRenderPass.descriptor = descriptor
|
||||
eraserRenderPass.draw(on: canvas, with: renderer)
|
||||
}
|
||||
}
|
||||
case .photo:
|
||||
break
|
||||
}
|
||||
}
|
||||
renderer.redrawsGraphicRender = false
|
||||
|
||||
@@ -85,7 +85,7 @@ struct MemosView: View {
|
||||
}
|
||||
|
||||
let graphicContextObject = GraphicContextObject(\.viewContext)
|
||||
graphicContextObject.strokes = []
|
||||
graphicContextObject.elements = []
|
||||
|
||||
memoObject.canvas = canvasObject
|
||||
memoObject.tool = toolObject
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
import CoreData
|
||||
import Foundation
|
||||
|
||||
|
||||
@objc(CanvasObject)
|
||||
final class CanvasObject: NSManagedObject {
|
||||
@NSManaged var width: CGFloat
|
||||
|
||||
17
Memola/Persistence/Objects/ElementObject.swift
Normal file
17
Memola/Persistence/Objects/ElementObject.swift
Normal file
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// ElementObject.swift
|
||||
// Memola
|
||||
//
|
||||
// Created by Dscyre Scotti on 6/12/24.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import Foundation
|
||||
|
||||
@objc(ElementObject)
|
||||
final class ElementObject: NSManagedObject {
|
||||
@NSManaged var type: Int16
|
||||
@NSManaged var createdAt: Date?
|
||||
@NSManaged var stroke: StrokeObject?
|
||||
@NSManaged var graphicContext: GraphicContextObject?
|
||||
}
|
||||
@@ -11,5 +11,5 @@ import Foundation
|
||||
@objc(GraphicContextObject)
|
||||
final class GraphicContextObject: NSManagedObject {
|
||||
@NSManaged var canvas: CanvasObject?
|
||||
@NSManaged var strokes: NSMutableOrderedSet
|
||||
@NSManaged var elements: NSMutableOrderedSet
|
||||
}
|
||||
|
||||
@@ -17,5 +17,5 @@ final class StrokeObject: NSManagedObject {
|
||||
@NSManaged var thickness: CGFloat
|
||||
@NSManaged var quads: NSMutableOrderedSet
|
||||
@NSManaged var erasers: NSMutableSet
|
||||
@NSManaged var graphicContext: GraphicContextObject?
|
||||
@NSManaged var element: ElementObject?
|
||||
}
|
||||
|
||||
@@ -6,6 +6,12 @@
|
||||
<relationship name="graphicContext" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="GraphicContextObject" inverseName="canvas" inverseEntity="GraphicContextObject"/>
|
||||
<relationship name="memo" maxCount="1" deletionRule="Deny" destinationEntity="MemoObject" inverseName="canvas" inverseEntity="MemoObject"/>
|
||||
</entity>
|
||||
<entity name="ElementObject" representedClassName="ElementObject" syncable="YES">
|
||||
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="type" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="graphicContext" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="GraphicContextObject" inverseName="elements" inverseEntity="GraphicContextObject"/>
|
||||
<relationship name="stroke" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StrokeObject" inverseName="element" inverseEntity="StrokeObject"/>
|
||||
</entity>
|
||||
<entity name="EraserObject" representedClassName="EraserObject" syncable="YES">
|
||||
<attribute name="bounds" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromDataTransformer" customClassName="[CGFloat]"/>
|
||||
<attribute name="color" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromDataTransformer" customClassName="[CGFloat]"/>
|
||||
@@ -17,7 +23,7 @@
|
||||
</entity>
|
||||
<entity name="GraphicContextObject" representedClassName="GraphicContextObject" syncable="YES">
|
||||
<relationship name="canvas" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="CanvasObject" inverseName="graphicContext" inverseEntity="CanvasObject"/>
|
||||
<relationship name="strokes" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="StrokeObject" inverseName="graphicContext" inverseEntity="StrokeObject"/>
|
||||
<relationship name="elements" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="ElementObject" inverseName="graphicContext" inverseEntity="ElementObject"/>
|
||||
</entity>
|
||||
<entity name="MemoObject" representedClassName="MemoObject" syncable="YES">
|
||||
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||
@@ -50,8 +56,8 @@
|
||||
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="style" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="thickness" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<relationship name="element" maxCount="1" deletionRule="Nullify" destinationEntity="ElementObject" inverseName="stroke" inverseEntity="ElementObject"/>
|
||||
<relationship name="erasers" toMany="YES" deletionRule="Nullify" destinationEntity="EraserObject" inverseName="strokes" inverseEntity="EraserObject"/>
|
||||
<relationship name="graphicContext" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="GraphicContextObject" inverseName="strokes" inverseEntity="GraphicContextObject"/>
|
||||
<relationship name="quads" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="QuadObject" inverseName="stroke" inverseEntity="QuadObject"/>
|
||||
</entity>
|
||||
<entity name="ToolObject" representedClassName="ToolObject" syncable="YES">
|
||||
|
||||
Reference in New Issue
Block a user