feat: wrap stroke object with element object

This commit is contained in:
dscyrescotti
2024-06-13 00:28:42 +07:00
parent d2eabb93e4
commit 2d0ca3478b
20 changed files with 147 additions and 57 deletions

View File

@@ -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 */,

View File

@@ -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()
}

View 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)
}
}

View File

@@ -125,4 +125,8 @@ extension Stroke {
var anyStroke: AnyStroke {
AnyStroke(self)
}
var element: Element {
.stroke(anyStroke)
}
}

View File

@@ -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

View File

@@ -85,7 +85,7 @@ struct MemosView: View {
}
let graphicContextObject = GraphicContextObject(\.viewContext)
graphicContextObject.strokes = []
graphicContextObject.elements = []
memoObject.canvas = canvasObject
memoObject.tool = toolObject

View File

@@ -8,7 +8,6 @@
import CoreData
import Foundation
@objc(CanvasObject)
final class CanvasObject: NSManagedObject {
@NSManaged var width: CGFloat

View 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?
}

View File

@@ -11,5 +11,5 @@ import Foundation
@objc(GraphicContextObject)
final class GraphicContextObject: NSManagedObject {
@NSManaged var canvas: CanvasObject?
@NSManaged var strokes: NSMutableOrderedSet
@NSManaged var elements: NSMutableOrderedSet
}

View File

@@ -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?
}

View File

@@ -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">