From 1151d1b868bdcff4f954e9ddb028843dd2b2e21b Mon Sep 17 00:00:00 2001 From: dscyrescotti Date: Sun, 9 Jun 2024 09:50:17 +0700 Subject: [PATCH] feat: store eraser stroke in eraser object --- Memola.xcodeproj/project.pbxproj | 4 + .../xcshareddata/xcschemes/Memola.xcscheme | 6 + Memola/Canvas/Contexts/GraphicContext.swift | 118 ++++++++++++------ Memola/Canvas/Core/Canvas.swift | 2 +- .../Canvas/Geometries/Primitives/Quad.swift | 10 ++ .../Stroke/Strokes/EraserStroke.swift | 82 ++++++++++++ .../Geometries/Stroke/Strokes/PenStroke.swift | 54 +++++++- Memola/Canvas/RTree/RTree.swift | 14 ++- .../RenderPasses/EraserRenderPass.swift | 30 ++++- .../RenderPasses/GraphicRenderPass.swift | 14 +++ Memola/Persistence/Objects/EraserObject.swift | 20 +++ Memola/Persistence/Objects/QuadObject.swift | 1 + Memola/Persistence/Objects/StrokeObject.swift | 1 + .../MemolaModel.xcdatamodel/contents | 11 ++ 14 files changed, 318 insertions(+), 49 deletions(-) create mode 100644 Memola/Persistence/Objects/EraserObject.swift diff --git a/Memola.xcodeproj/project.pbxproj b/Memola.xcodeproj/project.pbxproj index 921a58b..83a5b4b 100644 --- a/Memola.xcodeproj/project.pbxproj +++ b/Memola.xcodeproj/project.pbxproj @@ -28,6 +28,7 @@ EC7F6BEC2BE5E6E300A34A7B /* MemolaApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC7F6BEB2BE5E6E300A34A7B /* MemolaApp.swift */; }; EC7F6BF02BE5E6E400A34A7B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = EC7F6BEF2BE5E6E400A34A7B /* Assets.xcassets */; }; EC7F6BF32BE5E6E400A34A7B /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = EC7F6BF22BE5E6E400A34A7B /* Preview Assets.xcassets */; }; + EC9AB09F2C1401A40076AF58 /* EraserObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC9AB09E2C1401A40076AF58 /* EraserObject.swift */; }; ECA7387A2BE5EF0400A4542E /* MemosView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738792BE5EF0400A4542E /* MemosView.swift */; }; ECA7387D2BE5EF4B00A4542E /* MemoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA7387C2BE5EF4B00A4542E /* MemoView.swift */; }; ECA738832BE5FEFE00A4542E /* RenderPass.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738822BE5FEFE00A4542E /* RenderPass.swift */; }; @@ -117,6 +118,7 @@ EC7F6BEB2BE5E6E300A34A7B /* MemolaApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemolaApp.swift; sourceTree = ""; }; EC7F6BEF2BE5E6E400A34A7B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; EC7F6BF22BE5E6E400A34A7B /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + EC9AB09E2C1401A40076AF58 /* EraserObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EraserObject.swift; sourceTree = ""; }; ECA738792BE5EF0400A4542E /* MemosView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemosView.swift; sourceTree = ""; }; ECA7387C2BE5EF4B00A4542E /* MemoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoView.swift; sourceTree = ""; }; ECA738822BE5FEFE00A4542E /* RenderPass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RenderPass.swift; sourceTree = ""; }; @@ -673,6 +675,7 @@ ECFA15272BEF225000455818 /* QuadObject.swift */, EC0D14202BF79C73009BFE5F /* ToolObject.swift */, EC0D14252BF7A8C9009BFE5F /* PenObject.swift */, + EC9AB09E2C1401A40076AF58 /* EraserObject.swift */, ); path = Objects; sourceTree = ""; @@ -820,6 +823,7 @@ ECA738F62BE612B700A4542E /* MTLDevice++.swift in Sources */, EC0D14212BF79C73009BFE5F /* ToolObject.swift in Sources */, EC50500D2BF6674400B4D86E /* OnDragViewModifier.swift in Sources */, + EC9AB09F2C1401A40076AF58 /* EraserObject.swift in Sources */, ECA7389E2BE601CB00A4542E /* QuadVertex.swift in Sources */, ECA738B32BE60D9E00A4542E /* CanvasView.swift in Sources */, ECA738C42BE60E8800A4542E /* MarkerPenStyle.swift in Sources */, diff --git a/Memola.xcodeproj/xcshareddata/xcschemes/Memola.xcscheme b/Memola.xcodeproj/xcshareddata/xcschemes/Memola.xcscheme index 95b05ce..c2758d9 100644 --- a/Memola.xcodeproj/xcshareddata/xcschemes/Memola.xcscheme +++ b/Memola.xcodeproj/xcshareddata/xcschemes/Memola.xcscheme @@ -50,6 +50,12 @@ ReferencedContainer = "container:Memola.xcodeproj"> + + + + (maxEntries: 8) + var eraserStrokes: Set = [] var object: GraphicContextObject? var currentStroke: (any Stroke)? @@ -23,6 +27,10 @@ final class GraphicContext: @unchecked Sendable { var vertexCount: Int = 4 var vertexBuffer: MTLBuffer? + var erasers: [URL: EraserStroke] = [:] + + let concurrentQueue = DispatchQueue(label: "com.dispatchBarrier", attributes: .concurrent) + init() { setViewPortVertices() } @@ -72,23 +80,25 @@ extension GraphicContext { let queue = OperationQueue() queue.qualityOfService = .userInteractive object.strokes.forEach { stroke in - guard let stroke = stroke as? StrokeObject else { return } + 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 { + queue.addOperation { [weak self] in + guard let self else { return } withPersistenceSync(\.newBackgroundContext) { [_stroke] context in guard let stroke = try? context.existingObject(with: id) as? StrokeObject else { return } - _stroke.loadQuads(from: stroke) + _stroke.loadQuads(from: stroke, with: self) } withPersistence(\.backgroundContext) { [stroke] context in context.refresh(stroke, mergeChanges: false) } } } else { - withPersistence(\.backgroundContext) { [stroke] context in - _stroke.loadQuads() + withPersistence(\.backgroundContext) { [weak self, stroke] context in + guard let self else { return } + _stroke.loadQuads(with: self) context.refresh(stroke, mergeChanges: false) } } @@ -96,10 +106,10 @@ extension GraphicContext { queue.waitUntilAllOperationsAreFinished() } - func loadQuads(_ bounds: CGRect) { + func loadQuads(_ bounds: CGRect, on context: NSManagedObjectContext) { for _stroke in self.tree.search(box: bounds.box) { guard let stroke = _stroke.stroke(as: PenStroke.self), stroke.isEmpty else { continue } - stroke.loadQuads() + stroke.loadQuads(with: self) } } } @@ -122,24 +132,52 @@ extension GraphicContext: Drawable { extension GraphicContext { func beginStroke(at point: CGPoint, pen: Pen) -> any Stroke { - let stroke = PenStroke( - bounds: [point.x - pen.thickness, point.y - pen.thickness, point.x + pen.thickness, point.y + pen.thickness], - color: pen.rgba, - style: pen.strokeStyle, - createdAt: .now, - thickness: pen.thickness - ) - withPersistence(\.backgroundContext) { [graphicContext = object, _stroke = stroke] context in - let stroke = StrokeObject(\.backgroundContext) - stroke.bounds = _stroke.bounds - stroke.color = _stroke.color - stroke.style = _stroke.style.rawValue - stroke.thickness = _stroke.thickness - stroke.createdAt = _stroke.createdAt - stroke.quads = [] - stroke.graphicContext = graphicContext - graphicContext?.strokes.add(stroke) - _stroke.object = stroke + let stroke: any Stroke + switch pen.strokeStyle { + case .marker: + let penStroke = PenStroke( + bounds: [point.x - pen.thickness, point.y - pen.thickness, point.x + pen.thickness, point.y + pen.thickness], + color: pen.rgba, + style: pen.strokeStyle, + createdAt: .now, + thickness: pen.thickness + ) + withPersistence(\.backgroundContext) { [graphicContext = object, _stroke = penStroke] context in + let stroke = StrokeObject(\.backgroundContext) + stroke.bounds = _stroke.bounds + stroke.color = _stroke.color + stroke.style = _stroke.style.rawValue + stroke.thickness = _stroke.thickness + stroke.createdAt = _stroke.createdAt + stroke.quads = [] + stroke.erasers = .init() + stroke.graphicContext = graphicContext + graphicContext?.strokes.add(stroke) + _stroke.object = stroke + } + stroke = penStroke + case .eraser: + let eraserStroke = EraserStroke( + bounds: [point.x - pen.thickness, point.y - pen.thickness, point.x + pen.thickness, point.y + pen.thickness], + color: pen.rgba, + style: pen.strokeStyle, + createdAt: .now, + thickness: pen.thickness + ) + withPersistence(\.backgroundContext) { [graphicContext = object, _stroke = eraserStroke] context in + let stroke = EraserObject(\.backgroundContext) + stroke.bounds = _stroke.bounds + stroke.color = _stroke.color + stroke.style = _stroke.style.rawValue + stroke.thickness = _stroke.thickness + stroke.createdAt = _stroke.createdAt + stroke.quads = [] + stroke.strokes = .init() + graphicContext?.strokes.add(stroke) + _stroke.object = stroke + } + eraserStroke.graphicContext = self + stroke = eraserStroke } currentStroke = stroke currentPoint = point @@ -157,16 +195,23 @@ extension GraphicContext { } func endStroke(at point: CGPoint) { - guard currentPoint != nil, let currentStroke = currentStroke?.stroke(as: PenStroke.self) else { return } + guard currentPoint != nil, let currentStroke = currentStroke else { return } currentStroke.finish(at: point) - tree.insert(currentStroke.anyStroke, in: currentStroke.strokeBox) - currentStroke.saveQuads() - withPersistence(\.backgroundContext) { [currentStroke] context in - guard let stroke = currentStroke.stroke(as: PenStroke.self) else { return } - stroke.object?.bounds = stroke.bounds - try context.saveIfNeeded() - if let object = stroke.object { - context.refresh(object, mergeChanges: false) + if let penStroke = currentStroke.stroke(as: PenStroke.self) { + penStroke.saveQuads() + tree.insert(currentStroke.anyStroke, in: currentStroke.strokeBox) + withPersistence(\.backgroundContext) { [penStroke] context in + penStroke.object?.bounds = penStroke.bounds + try context.saveIfNeeded() + if let object = penStroke.object { + context.refresh(object, mergeChanges: false) + } + } + } else if let eraserStroke = currentStroke.stroke(as: EraserStroke.self) { + eraserStroke.saveQuads() + eraserStrokes.insert(eraserStroke) + withPersistence(\.backgroundContext) { context in + try context.saveIfNeeded() } } previousStroke = currentStroke @@ -175,12 +220,15 @@ extension GraphicContext { } func cancelStroke() { - if !tree.isEmpty, let stroke = currentStroke?.stroke(as: PenStroke.self) { + if !tree.isEmpty, let stroke = currentStroke { let _stroke = tree.remove(stroke.anyStroke, in: stroke.strokeBox) withPersistence(\.backgroundContext) { [graphicContext = object, _stroke] context in if let stroke = _stroke?.stroke(as: PenStroke.self)?.object { graphicContext?.strokes.remove(stroke) context.delete(stroke) + } else if let stroke = _stroke?.stroke(as: EraserStroke.self)?.object { + graphicContext?.strokes.remove(stroke) + context.delete(stroke) } try context.saveIfNeeded() } diff --git a/Memola/Canvas/Core/Canvas.swift b/Memola/Canvas/Core/Canvas.swift index f4ee75b..241e782 100644 --- a/Memola/Canvas/Core/Canvas.swift +++ b/Memola/Canvas/Core/Canvas.swift @@ -68,7 +68,7 @@ extension Canvas { func loadStrokes(_ bounds: CGRect) { withPersistence(\.backgroundContext) { [weak self, bounds] context in - self?.graphicContext.loadQuads(bounds) + self?.graphicContext.loadQuads(bounds, on: context) } } } diff --git a/Memola/Canvas/Geometries/Primitives/Quad.swift b/Memola/Canvas/Geometries/Primitives/Quad.swift index 585cce3..1140f3d 100644 --- a/Memola/Canvas/Geometries/Primitives/Quad.swift +++ b/Memola/Canvas/Geometries/Primitives/Quad.swift @@ -53,4 +53,14 @@ extension Quad { func getColor() -> [CGFloat] { [color.x.cgFloat, color.y.cgFloat, color.z.cgFloat, color.w.cgFloat] } + + var quadBounds: CGRect { + let halfSize = size.cgFloat / 2 + return CGRect(x: originX.cgFloat - halfSize, y: originY.cgFloat - halfSize, width: size.cgFloat, height: size.cgFloat) + } + + var quadBox: Box { + let halfSize = size / 2 + return Box(minX: Double(originX - halfSize), minY: Double(originY - halfSize), maxX: Double(originX + halfSize), maxY: Double(originY + halfSize)) + } } diff --git a/Memola/Canvas/Geometries/Stroke/Strokes/EraserStroke.swift b/Memola/Canvas/Geometries/Stroke/Strokes/EraserStroke.swift index 011ece5..f96f3df 100644 --- a/Memola/Canvas/Geometries/Stroke/Strokes/EraserStroke.swift +++ b/Memola/Canvas/Geometries/Stroke/Strokes/EraserStroke.swift @@ -5,6 +5,7 @@ // Created by Dscyre Scotti on 5/24/24. // +import CoreData import MetalKit import Foundation @@ -25,6 +26,15 @@ final class EraserStroke: Stroke, @unchecked Sendable { var indexBuffer: (any MTLBuffer)? var vertexBuffer: (any MTLBuffer)? + let batchSize: Int = 50 + var batchIndex: Int = 0 + + var object: EraserObject? + + weak var graphicContext: GraphicContext? + + var finishesSaving: Bool = false + init( bounds: [CGFloat], color: [CGFloat], @@ -41,4 +51,76 @@ final class EraserStroke: Stroke, @unchecked Sendable { self.quads = quads self.penStyle = style.penStyle } + + convenience init(object: EraserObject) { + let style = StrokeStyle(rawValue: object.style) ?? .marker + self.init( + bounds: object.bounds, + color: object.color, + style: style, + createdAt: object.createdAt, + thickness: object.thickness + ) + self.object = object + } + + func addQuad(at point: CGPoint, rotation: CGFloat, shape: QuadShape) { + let quad = Quad( + origin: point, + size: thickness, + rotation: rotation, + shape: shape.rawValue, + color: color + ) + quads.append(quad) + bounds = [ + min(quad.originX.cgFloat, bounds[0]), + min(quad.originY.cgFloat, bounds[1]), + max(quad.originX.cgFloat, bounds[2]), + max(quad.originY.cgFloat, bounds[3]) + ] + if quads.endIndex >= batchIndex + batchSize { + saveQuads(to: batchIndex + batchSize) + } + } + + func loadQuads(from object: EraserObject) { + quads = object.quads.compactMap { quad in + guard let quad = quad as? QuadObject else { return nil } + return Quad(object: quad) + } + } + + func saveQuads(to endIndex: Int? = nil) { + let isEnded: Bool = endIndex == nil + guard let graphicContext else { return } + let endIndex = endIndex ?? quads.endIndex + let batch = quads[batchIndex.. = [] + + var isEmptyErasedQuads: Bool { + eraserStrokes.isEmpty + } + + weak var graphicContext: GraphicContext? init( bounds: [CGFloat], @@ -60,16 +71,29 @@ final class PenStroke: Stroke, @unchecked Sendable { self.object = object } - func loadQuads() { + func loadQuads(with graphicContext: GraphicContext) { guard let object else { return } - loadQuads(from: object) + loadQuads(from: object, with: graphicContext) } - func loadQuads(from object: StrokeObject) { + func loadQuads(from object: StrokeObject, with graphicContext: GraphicContext) { quads = object.quads.compactMap { quad in guard let quad = quad as? QuadObject else { return nil } return Quad(object: quad) } + eraserStrokes = Set(object.erasers.compactMap { [graphicContext] eraser -> EraserStroke? in + guard let eraser = eraser as? EraserObject else { return nil } + let url = eraser.objectID.uriRepresentation() + return graphicContext.concurrentQueue.sync(flags: .barrier) { + if graphicContext.erasers[url] == nil { + let _stroke = EraserStroke(object: eraser) + _stroke.loadQuads(from: eraser) + graphicContext.erasers[url] = _stroke + return _stroke + } + return graphicContext.erasers[url] + } + }) } func addQuad(at point: CGPoint, rotation: CGFloat, shape: QuadShape) { @@ -110,4 +134,26 @@ final class PenStroke: Stroke, @unchecked Sendable { } } } + + func getAllErasedQuads() -> [Quad] { + eraserStrokes.flatMap { $0.quads } + } + + func erase(device: MTLDevice, renderEncoder: MTLRenderCommandEncoder) { + guard !isEmptyErasedQuads, let erasedIndexBuffer else { + return + } + prepare(device: device) + renderEncoder.setFragmentTexture(texture, index: 0) + renderEncoder.setVertexBuffer(erasedVertexBuffer, offset: 0, index: 0) + renderEncoder.drawIndexedPrimitives( + type: .triangle, + indexCount: erasedQuadCount * 6, + indexType: .uint32, + indexBuffer: erasedIndexBuffer, + indexBufferOffset: 0 + ) + self.erasedIndexBuffer = nil + self.erasedVertexBuffer = nil + } } diff --git a/Memola/Canvas/RTree/RTree.swift b/Memola/Canvas/RTree/RTree.swift index be75479..367a7ba 100644 --- a/Memola/Canvas/RTree/RTree.swift +++ b/Memola/Canvas/RTree/RTree.swift @@ -66,7 +66,7 @@ class RTree where T: Equatable & Comparable { } // MARK: - Search - func search(box: Box) -> [T] { + func search(box: Box, isInOrder: Bool = true) -> [T] { guard box.intersects(with: root.box) else { return [] } var result: [T] = [] var queue: [Node] = [root] @@ -76,10 +76,18 @@ class RTree where T: Equatable & Comparable { if box.intersects(with: childNode.box) { if node.isLeaf { if let value = childNode.value { - result = _merge(result, [value]) + if isInOrder { + result = _merge(result, [value]) + } else { + result.append(value) + } } } else if box.contains(with: childNode.box) { - result = _merge(result, _traverse(from: childNode)) + if isInOrder { + result = _merge(result, _traverse(from: childNode)) + } else { + result.append(contentsOf: _traverse(from: childNode)) + } } else { queue.append(childNode) } diff --git a/Memola/Canvas/RenderPasses/EraserRenderPass.swift b/Memola/Canvas/RenderPasses/EraserRenderPass.swift index 6eeb0bd..9a1099c 100644 --- a/Memola/Canvas/RenderPasses/EraserRenderPass.swift +++ b/Memola/Canvas/RenderPasses/EraserRenderPass.swift @@ -41,21 +41,33 @@ class EraserRenderPass: RenderPass { renderEncoder.setRenderPipelineState(eraserPipelineState) canvas.setUniformsBuffer(device: renderer.device, renderEncoder: renderEncoder) - stroke?.draw(device: renderer.device, renderEncoder: renderEncoder) + if let stroke = stroke as? PenStroke { + stroke.erase(device: renderer.device, renderEncoder: renderEncoder) + } else { + stroke?.draw(device: renderer.device, renderEncoder: renderEncoder) + } renderEncoder.endEncoding() commandBuffer.commit() } private func generateVertexBuffer(on canvas: Canvas, with renderer: Renderer) { - guard let stroke, !stroke.isEmpty, let quadPipelineState else { return } + guard let stroke else { return } + let quadCount: Int + var quads: [Quad] + if let stroke = stroke as? PenStroke { + quads = stroke.getAllErasedQuads() + quadCount = quads.endIndex + } else { + quadCount = stroke.quads.endIndex + quads = stroke.quads + } + guard !quads.isEmpty, let quadPipelineState else { return } guard let quadCommandBuffer = renderer.commandQueue.makeCommandBuffer() else { return } guard let computeEncoder = quadCommandBuffer.makeComputeCommandEncoder() else { return } computeEncoder.label = "Quad Render Pass" - let quadCount = stroke.quads.endIndex - var quads = stroke.quads let quadBuffer = renderer.device.makeBuffer(bytes: &quads, length: MemoryLayout.stride * quadCount, options: []) let indexBuffer = renderer.device.makeBuffer(length: MemoryLayout.stride * quadCount * 6, options: []) let vertexBuffer = renderer.device.makeBuffer(length: MemoryLayout.stride * quadCount * 4, options: []) @@ -65,8 +77,14 @@ class EraserRenderPass: RenderPass { computeEncoder.setBuffer(indexBuffer, offset: 0, index: 1) computeEncoder.setBuffer(vertexBuffer, offset: 0, index: 2) - stroke.indexBuffer = indexBuffer - stroke.vertexBuffer = vertexBuffer + if let stroke = stroke as? PenStroke { + stroke.erasedIndexBuffer = indexBuffer + stroke.erasedVertexBuffer = vertexBuffer + stroke.erasedQuadCount = quadCount + } else { + stroke.indexBuffer = indexBuffer + stroke.vertexBuffer = vertexBuffer + } let threadsPerGroup = MTLSize(width: 1, height: 1, depth: 1) let numThreadgroups = MTLSize(width: quadCount + 1, height: 1, depth: 1) diff --git a/Memola/Canvas/RenderPasses/GraphicRenderPass.swift b/Memola/Canvas/RenderPasses/GraphicRenderPass.swift index 8e5bf1e..45b30ff 100644 --- a/Memola/Canvas/RenderPasses/GraphicRenderPass.swift +++ b/Memola/Canvas/RenderPasses/GraphicRenderPass.swift @@ -64,6 +64,13 @@ class GraphicRenderPass: RenderPass { 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) + } } } renderer.redrawsGraphicRender = false @@ -83,6 +90,13 @@ class GraphicRenderPass: RenderPass { 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) + } } graphicContext.previousStroke = nil } diff --git a/Memola/Persistence/Objects/EraserObject.swift b/Memola/Persistence/Objects/EraserObject.swift new file mode 100644 index 0000000..7c5ac48 --- /dev/null +++ b/Memola/Persistence/Objects/EraserObject.swift @@ -0,0 +1,20 @@ +// +// EraserObject.swift +// Memola +// +// Created by Dscyre Scotti on 6/8/24. +// + +import CoreData +import Foundation + +@objc(EraserObject) +final class EraserObject: NSManagedObject { + @NSManaged var bounds: [CGFloat] + @NSManaged var color: [CGFloat] + @NSManaged var style: Int16 + @NSManaged var createdAt: Date + @NSManaged var thickness: CGFloat + @NSManaged var quads: NSMutableOrderedSet + @NSManaged var strokes: NSMutableSet +} diff --git a/Memola/Persistence/Objects/QuadObject.swift b/Memola/Persistence/Objects/QuadObject.swift index 636e944..5c5738b 100644 --- a/Memola/Persistence/Objects/QuadObject.swift +++ b/Memola/Persistence/Objects/QuadObject.swift @@ -17,4 +17,5 @@ final class QuadObject: NSManagedObject { @NSManaged var shape: Int16 @NSManaged var color: [CGFloat] @NSManaged var stroke: StrokeObject? + @NSManaged var eraser: EraserObject? } diff --git a/Memola/Persistence/Objects/StrokeObject.swift b/Memola/Persistence/Objects/StrokeObject.swift index 2d9767a..a90f24a 100644 --- a/Memola/Persistence/Objects/StrokeObject.swift +++ b/Memola/Persistence/Objects/StrokeObject.swift @@ -16,5 +16,6 @@ final class StrokeObject: NSManagedObject { @NSManaged var createdAt: Date @NSManaged var thickness: CGFloat @NSManaged var quads: NSMutableOrderedSet + @NSManaged var erasers: NSMutableSet @NSManaged var graphicContext: GraphicContextObject? } diff --git a/Memola/Resources/Models/MemolaModel.xcdatamodeld/MemolaModel.xcdatamodel/contents b/Memola/Resources/Models/MemolaModel.xcdatamodeld/MemolaModel.xcdatamodel/contents index 798206c..c9ca0bf 100644 --- a/Memola/Resources/Models/MemolaModel.xcdatamodeld/MemolaModel.xcdatamodel/contents +++ b/Memola/Resources/Models/MemolaModel.xcdatamodeld/MemolaModel.xcdatamodel/contents @@ -6,6 +6,15 @@ + + + + + + + + + @@ -32,6 +41,7 @@ + @@ -40,6 +50,7 @@ +