From e9708ae072a440bc8ef422e253d82479ed2cfee8 Mon Sep 17 00:00:00 2001 From: dscyrescotti Date: Fri, 7 Jun 2024 19:59:29 +0700 Subject: [PATCH 01/17] feat: save quads by batch --- Memola/Canvas/Contexts/GraphicContext.swift | 1 + .../Geometries/Stroke/Strokes/PenStroke.swift | 34 +++++++++++++------ 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/Memola/Canvas/Contexts/GraphicContext.swift b/Memola/Canvas/Contexts/GraphicContext.swift index 8b4bb61..e8979db 100644 --- a/Memola/Canvas/Contexts/GraphicContext.swift +++ b/Memola/Canvas/Contexts/GraphicContext.swift @@ -160,6 +160,7 @@ extension GraphicContext { guard currentPoint != nil, let currentStroke = currentStroke?.stroke(as: PenStroke.self) 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 diff --git a/Memola/Canvas/Geometries/Stroke/Strokes/PenStroke.swift b/Memola/Canvas/Geometries/Stroke/Strokes/PenStroke.swift index c8f9d70..bd57e8a 100644 --- a/Memola/Canvas/Geometries/Stroke/Strokes/PenStroke.swift +++ b/Memola/Canvas/Geometries/Stroke/Strokes/PenStroke.swift @@ -28,6 +28,9 @@ final class PenStroke: Stroke, @unchecked Sendable { var object: StrokeObject? + let batchSize: Int = 50 + var batchIndex: Int = 0 + init( bounds: [CGFloat], color: [CGFloat], @@ -84,16 +87,27 @@ final class PenStroke: Stroke, @unchecked Sendable { max(quad.originX.cgFloat, bounds[2]), max(quad.originY.cgFloat, bounds[3]) ] - withPersistence(\.backgroundContext) { [object, _quad = quad] context in - let quad = QuadObject(\.backgroundContext) - quad.originX = _quad.originX.cgFloat - quad.originY = _quad.originY.cgFloat - quad.size = _quad.size.cgFloat - quad.rotation = _quad.rotation.cgFloat - quad.shape = _quad.shape - quad.color = _quad.getColor() - quad.stroke = object - object?.quads.add(quad) + if quads.endIndex >= batchIndex + batchSize { + saveQuads(to: batchIndex + batchSize) + } + } + + func saveQuads(to endIndex: Int? = nil) { + let endIndex = endIndex ?? quads.endIndex + let batch = quads[batchIndex.. Date: Sun, 9 Jun 2024 09:50:17 +0700 Subject: [PATCH 02/17] 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 @@ + From 20e653c200da722b5002890d6a1aba65540223ca Mon Sep 17 00:00:00 2001 From: dscyrescotti Date: Sun, 9 Jun 2024 10:01:09 +0700 Subject: [PATCH 03/17] feat: create new pen using last pen if selected one is eraser --- Memola/Features/Memo/PenDock/PenDock.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Memola/Features/Memo/PenDock/PenDock.swift b/Memola/Features/Memo/PenDock/PenDock.swift index 1e02b77..b4b77e0 100644 --- a/Memola/Features/Memo/PenDock/PenDock.swift +++ b/Memola/Features/Memo/PenDock/PenDock.swift @@ -250,7 +250,9 @@ struct PenDock: View { var newPenButton: some View { Button { let pen = PenObject.createObject(\.viewContext, penStyle: .marker) - if let color = (tool.selectedPen ?? tool.pens.last)?.rgba { + var selectedPen = tool.selectedPen + selectedPen = (selectedPen?.strokeStyle == .marker ? (selectedPen ?? tool.pens.last) : tool.pens.last) + if let color = selectedPen?.rgba { pen.color = color } pen.isSelected = true From c3a14bd608f06affc1b1220259ac77f68fe43736 Mon Sep 17 00:00:00 2001 From: dscyrescotti Date: Sun, 9 Jun 2024 12:32:36 +0700 Subject: [PATCH 04/17] refactor: clean up --- Memola/Canvas/Contexts/GraphicContext.swift | 2 +- .../Geometries/Stroke/Strokes/PenStroke.swift | 2 +- Memola/Canvas/RenderPasses/GraphicRenderPass.swift | 13 +++++++++++++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/Memola/Canvas/Contexts/GraphicContext.swift b/Memola/Canvas/Contexts/GraphicContext.swift index a0f28b2..ebf6c3e 100644 --- a/Memola/Canvas/Contexts/GraphicContext.swift +++ b/Memola/Canvas/Contexts/GraphicContext.swift @@ -29,7 +29,7 @@ final class GraphicContext: @unchecked Sendable { var erasers: [URL: EraserStroke] = [:] - let concurrentQueue = DispatchQueue(label: "com.dispatchBarrier", attributes: .concurrent) + let barrierQueue = DispatchQueue(label: "com.memola.app.graphic-context", attributes: .concurrent) init() { setViewPortVertices() diff --git a/Memola/Canvas/Geometries/Stroke/Strokes/PenStroke.swift b/Memola/Canvas/Geometries/Stroke/Strokes/PenStroke.swift index 6eb9c62..2bdd8b8 100644 --- a/Memola/Canvas/Geometries/Stroke/Strokes/PenStroke.swift +++ b/Memola/Canvas/Geometries/Stroke/Strokes/PenStroke.swift @@ -84,7 +84,7 @@ final class PenStroke: Stroke, @unchecked Sendable { 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) { + return graphicContext.barrierQueue.sync(flags: .barrier) { if graphicContext.erasers[url] == nil { let _stroke = EraserStroke(object: eraser) _stroke.loadQuads(from: eraser) diff --git a/Memola/Canvas/RenderPasses/GraphicRenderPass.swift b/Memola/Canvas/RenderPasses/GraphicRenderPass.swift index 45b30ff..f798406 100644 --- a/Memola/Canvas/RenderPasses/GraphicRenderPass.swift +++ b/Memola/Canvas/RenderPasses/GraphicRenderPass.swift @@ -100,5 +100,18 @@ class GraphicRenderPass: RenderPass { } graphicContext.previousStroke = nil } + + let eraserStrokes = graphicContext.eraserStrokes + for eraserStroke in eraserStrokes { + if eraserStroke.finishesSaving { + graphicContext.eraserStrokes.remove(eraserStroke) + continue + } + descriptor.colorAttachments[0].loadAction = clearsTexture ? .clear : .load + clearsTexture = false + eraserRenderPass.stroke = eraserStroke + eraserRenderPass.descriptor = descriptor + eraserRenderPass.draw(on: canvas, with: renderer) + } } } From 87b53e069afc4dbe5ad9afac1b1e57e41e83da79 Mon Sep 17 00:00:00 2001 From: dscyrescotti Date: Sun, 9 Jun 2024 14:40:33 +0700 Subject: [PATCH 05/17] feat: save memo before dismissing canvas view --- Memola/Canvas/Contexts/GraphicContext.swift | 8 ++++---- Memola/Features/Memo/Toolbar/Toolbar.swift | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Memola/Canvas/Contexts/GraphicContext.swift b/Memola/Canvas/Contexts/GraphicContext.swift index ebf6c3e..37ceee1 100644 --- a/Memola/Canvas/Contexts/GraphicContext.swift +++ b/Memola/Canvas/Contexts/GraphicContext.swift @@ -203,15 +203,15 @@ extension GraphicContext { withPersistence(\.backgroundContext) { [penStroke] context in penStroke.object?.bounds = penStroke.bounds try context.saveIfNeeded() - if let object = penStroke.object { - context.refresh(object, mergeChanges: false) - } + context.refreshAllObjects() } } else if let eraserStroke = currentStroke.stroke(as: EraserStroke.self) { eraserStroke.saveQuads() eraserStrokes.insert(eraserStroke) - withPersistence(\.backgroundContext) { context in + withPersistence(\.backgroundContext) { [eraserStroke] context in + eraserStroke.object?.bounds = eraserStroke.bounds try context.saveIfNeeded() + context.refreshAllObjects() } } previousStroke = currentStroke diff --git a/Memola/Features/Memo/Toolbar/Toolbar.swift b/Memola/Features/Memo/Toolbar/Toolbar.swift index 32b13a8..0f4d48d 100644 --- a/Memola/Features/Memo/Toolbar/Toolbar.swift +++ b/Memola/Features/Memo/Toolbar/Toolbar.swift @@ -128,15 +128,15 @@ struct Toolbar: View { } func closeMemo() { - DispatchQueue.global(qos: .userInitiated).async { + withAnimation { + canvas.state = .closing + } + withPersistence(\.backgroundContext) { context in + try? context.saveIfNeeded() DispatchQueue.main.async { - canvas.state = .closing - } - withPersistenceSync(\.viewContext) { context in - try context.saveIfNeeded() - } - DispatchQueue.main.async { - canvas.state = .closed + withAnimation { + canvas.state = .closed + } dismiss() } } From 99abf94351471848934a799473ebf64c3cd02dde Mon Sep 17 00:00:00 2001 From: dscyrescotti Date: Sun, 9 Jun 2024 14:58:57 +0700 Subject: [PATCH 06/17] feat: save stroke as soon as it is created at the beginning of touch --- Memola/Canvas/Contexts/GraphicContext.swift | 41 +++++++++++-------- .../ViewController/CanvasViewController.swift | 3 -- Memola/Features/Memos/MemosView.swift | 4 +- 3 files changed, 26 insertions(+), 22 deletions(-) diff --git a/Memola/Canvas/Contexts/GraphicContext.swift b/Memola/Canvas/Contexts/GraphicContext.swift index 37ceee1..f4661d2 100644 --- a/Memola/Canvas/Contexts/GraphicContext.swift +++ b/Memola/Canvas/Contexts/GraphicContext.swift @@ -10,8 +10,6 @@ import MetalKit import CoreData import Foundation -#warning("ISSUE: after closing the canvas, instantly opening the memo misses erasers of a particular stroke") -#warning("TODO: it is required to remove eraser stroke that finishes saving") #warning("TODO: to update history undo and redo logic") final class GraphicContext: @unchecked Sendable { var tree: RTree = RTree(maxEntries: 8) @@ -91,15 +89,11 @@ extension GraphicContext { guard let stroke = try? context.existingObject(with: id) as? StrokeObject else { return } _stroke.loadQuads(from: stroke, with: self) } - withPersistence(\.backgroundContext) { [stroke] context in - context.refresh(stroke, mergeChanges: false) - } } } else { - withPersistence(\.backgroundContext) { [weak self, stroke] context in + withPersistence(\.backgroundContext) { [weak self] context in guard let self else { return } _stroke.loadQuads(with: self) - context.refresh(stroke, mergeChanges: false) } } } @@ -154,6 +148,7 @@ extension GraphicContext { stroke.graphicContext = graphicContext graphicContext?.strokes.add(stroke) _stroke.object = stroke + try context.saveIfNeeded() } stroke = penStroke case .eraser: @@ -175,6 +170,7 @@ extension GraphicContext { stroke.strokes = .init() graphicContext?.strokes.add(stroke) _stroke.object = stroke + try context.saveIfNeeded() } eraserStroke.graphicContext = self stroke = eraserStroke @@ -220,18 +216,29 @@ extension GraphicContext { } func cancelStroke() { - 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) + if let stroke = currentStroke { + switch stroke.style { + case .marker: + guard !tree.isEmpty else { return } + 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) + } + try context.saveIfNeeded() + } + case .eraser: + guard let eraserStroke = stroke.stroke(as: EraserStroke.self) else { return } + eraserStrokes.remove(eraserStroke) + withPersistence(\.backgroundContext) { [eraserStroke] context in + if let stroke = eraserStroke.object { + context.delete(stroke) + } + try context.saveIfNeeded() } - try context.saveIfNeeded() } + } currentStroke = nil currentPoint = nil diff --git a/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift b/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift index 5e3bac4..e2d60a4 100644 --- a/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift +++ b/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift @@ -62,9 +62,6 @@ class CanvasViewController: UIViewController { override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) history.resetRedo() - withPersistence(\.backgroundContext) { context in - context.refreshAllObjects() - } } } diff --git a/Memola/Features/Memos/MemosView.swift b/Memola/Features/Memos/MemosView.swift index 331016c..82ac324 100644 --- a/Memola/Features/Memos/MemosView.swift +++ b/Memola/Features/Memos/MemosView.swift @@ -68,8 +68,8 @@ struct MemosView: View { memoObject.updatedAt = .now let canvasObject = CanvasObject(context: managedObjectContext) - canvasObject.width = 8_000 - canvasObject.height = 8_000 + canvasObject.width = 4_000 + canvasObject.height = 4_000 let toolObject = ToolObject(\.viewContext) toolObject.pens = [] From 90de6310e1df9e46fd2237957e03796d42bf17bc Mon Sep 17 00:00:00 2001 From: dscyrescotti Date: Sun, 9 Jun 2024 17:40:58 +0700 Subject: [PATCH 07/17] bug: remove eraser stroke adding into graphic context --- Memola/Canvas/Contexts/GraphicContext.swift | 13 +++++-------- Memola/Canvas/Core/Canvas.swift | 2 +- Memola/Features/Memo/PenDock/PenDock.swift | 2 -- Memola/Features/Memos/MemosView.swift | 4 ++-- 4 files changed, 8 insertions(+), 13 deletions(-) diff --git a/Memola/Canvas/Contexts/GraphicContext.swift b/Memola/Canvas/Contexts/GraphicContext.swift index f4661d2..20da0fc 100644 --- a/Memola/Canvas/Contexts/GraphicContext.swift +++ b/Memola/Canvas/Contexts/GraphicContext.swift @@ -159,7 +159,8 @@ extension GraphicContext { createdAt: .now, thickness: pen.thickness ) - withPersistence(\.backgroundContext) { [graphicContext = object, _stroke = eraserStroke] context in + eraserStroke.graphicContext = self + withPersistence(\.backgroundContext) { [_stroke = eraserStroke] context in let stroke = EraserObject(\.backgroundContext) stroke.bounds = _stroke.bounds stroke.color = _stroke.color @@ -168,11 +169,9 @@ extension GraphicContext { stroke.createdAt = _stroke.createdAt stroke.quads = [] stroke.strokes = .init() - graphicContext?.strokes.add(stroke) _stroke.object = stroke try context.saveIfNeeded() } - eraserStroke.graphicContext = self stroke = eraserStroke } currentStroke = stroke @@ -219,17 +218,16 @@ extension GraphicContext { if let stroke = currentStroke { switch stroke.style { case .marker: - guard !tree.isEmpty else { return } - let _stroke = tree.remove(stroke.anyStroke, in: stroke.strokeBox) + guard let _stroke = stroke.stroke(as: PenStroke.self) else { break } withPersistence(\.backgroundContext) { [graphicContext = object, _stroke] context in - if let stroke = _stroke?.stroke(as: PenStroke.self)?.object { + if let stroke = _stroke.object { graphicContext?.strokes.remove(stroke) context.delete(stroke) } try context.saveIfNeeded() } case .eraser: - guard let eraserStroke = stroke.stroke(as: EraserStroke.self) else { return } + guard let eraserStroke = stroke.stroke(as: EraserStroke.self) else { break } eraserStrokes.remove(eraserStroke) withPersistence(\.backgroundContext) { [eraserStroke] context in if let stroke = eraserStroke.object { @@ -238,7 +236,6 @@ extension GraphicContext { try context.saveIfNeeded() } } - } currentStroke = nil currentPoint = nil diff --git a/Memola/Canvas/Core/Canvas.swift b/Memola/Canvas/Core/Canvas.swift index 241e782..3a5931b 100644 --- a/Memola/Canvas/Core/Canvas.swift +++ b/Memola/Canvas/Core/Canvas.swift @@ -59,7 +59,7 @@ extension Canvas { let graphicContext = canvas.graphicContext self?.graphicContext.object = graphicContext self?.graphicContext.loadStrokes(bounds) - context.refresh(canvas, mergeChanges: false) + context.refreshAllObjects() DispatchQueue.main.async { [weak self] in self?.state = .loaded } diff --git a/Memola/Features/Memo/PenDock/PenDock.swift b/Memola/Features/Memo/PenDock/PenDock.swift index b4b77e0..1fd2b5c 100644 --- a/Memola/Features/Memo/PenDock/PenDock.swift +++ b/Memola/Features/Memo/PenDock/PenDock.swift @@ -194,7 +194,6 @@ struct PenDock: View { .stroke(Color.gray, lineWidth: 0.4) } .padding(0.2) - .drawingGroup() } .buttonStyle(.plain) .hoverEffect(.lift) @@ -302,7 +301,6 @@ struct PenDock: View { .resizable() .renderingMode(.template) } - .drawingGroup() .foregroundStyle(.black.opacity(0.2)) .blur(radius: 3) if let tip = pen.style.icon.tip { diff --git a/Memola/Features/Memos/MemosView.swift b/Memola/Features/Memos/MemosView.swift index 82ac324..331016c 100644 --- a/Memola/Features/Memos/MemosView.swift +++ b/Memola/Features/Memos/MemosView.swift @@ -68,8 +68,8 @@ struct MemosView: View { memoObject.updatedAt = .now let canvasObject = CanvasObject(context: managedObjectContext) - canvasObject.width = 4_000 - canvasObject.height = 4_000 + canvasObject.width = 8_000 + canvasObject.height = 8_000 let toolObject = ToolObject(\.viewContext) toolObject.pens = [] From 63aced942ad46d8caf06cadbb84bda7cf763db83 Mon Sep 17 00:00:00 2001 From: dscyrescotti Date: Sun, 9 Jun 2024 23:31:03 +0700 Subject: [PATCH 08/17] feat: save title changes --- Memola/Features/Memo/Toolbar/Toolbar.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Memola/Features/Memo/Toolbar/Toolbar.swift b/Memola/Features/Memo/Toolbar/Toolbar.swift index 0f4d48d..6388fc0 100644 --- a/Memola/Features/Memo/Toolbar/Toolbar.swift +++ b/Memola/Features/Memo/Toolbar/Toolbar.swift @@ -72,6 +72,9 @@ struct Toolbar: View { } else { title = memo.title } + withPersistence(\.viewContext) { context in + try context.saveIfNeeded() + } } } .transition(.move(edge: .top).combined(with: .blurReplace)) From 89b97be07d3f3a5c578c818f1866c3512ffe4e62 Mon Sep 17 00:00:00 2001 From: dscyrescotti Date: Mon, 10 Jun 2024 00:46:02 +0700 Subject: [PATCH 09/17] feat: fetch eraser objects instead of loading from stroke --- Memola/Canvas/Contexts/GraphicContext.swift | 4 +++- .../Geometries/Stroke/Strokes/PenStroke.swift | 17 +++++++++++++++-- Memola/Features/Memos/MemosView.swift | 1 + Memola/Persistence/Objects/EraserObject.swift | 1 + .../Objects/GraphicContextObject.swift | 1 + .../MemolaModel.xcdatamodel/contents | 2 ++ 6 files changed, 23 insertions(+), 3 deletions(-) diff --git a/Memola/Canvas/Contexts/GraphicContext.swift b/Memola/Canvas/Contexts/GraphicContext.swift index 20da0fc..d54af58 100644 --- a/Memola/Canvas/Contexts/GraphicContext.swift +++ b/Memola/Canvas/Contexts/GraphicContext.swift @@ -160,7 +160,7 @@ extension GraphicContext { thickness: pen.thickness ) eraserStroke.graphicContext = self - withPersistence(\.backgroundContext) { [_stroke = eraserStroke] context in + withPersistence(\.backgroundContext) { [graphicContext = object, _stroke = eraserStroke] context in let stroke = EraserObject(\.backgroundContext) stroke.bounds = _stroke.bounds stroke.color = _stroke.color @@ -169,6 +169,8 @@ extension GraphicContext { stroke.createdAt = _stroke.createdAt stroke.quads = [] stroke.strokes = .init() + stroke.graphicContext = graphicContext + graphicContext?.erasers.add(stroke) _stroke.object = stroke try context.saveIfNeeded() } diff --git a/Memola/Canvas/Geometries/Stroke/Strokes/PenStroke.swift b/Memola/Canvas/Geometries/Stroke/Strokes/PenStroke.swift index 2bdd8b8..38637c3 100644 --- a/Memola/Canvas/Geometries/Stroke/Strokes/PenStroke.swift +++ b/Memola/Canvas/Geometries/Stroke/Strokes/PenStroke.swift @@ -81,8 +81,8 @@ final class PenStroke: Stroke, @unchecked Sendable { 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 erasers = fetchErasers(of: object) + eraserStrokes = Set(erasers.compactMap { [graphicContext] eraser -> EraserStroke? in let url = eraser.objectID.uriRepresentation() return graphicContext.barrierQueue.sync(flags: .barrier) { if graphicContext.erasers[url] == nil { @@ -96,6 +96,19 @@ final class PenStroke: Stroke, @unchecked Sendable { }) } + func fetchErasers(of stroke: StrokeObject) -> [EraserObject] { + let fetchRequest: NSFetchRequest = .init(entityName: "EraserObject") + fetchRequest.predicate = NSPredicate(format: "ANY strokes == %@", stroke) + + do { + let erasers = try Persistence.shared.backgroundContext.fetch(fetchRequest) + return erasers + } catch { + NSLog("[Memola] - \(error.localizedDescription)") + } + return [] + } + func addQuad(at point: CGPoint, rotation: CGFloat, shape: QuadShape) { let quad = Quad( origin: point, diff --git a/Memola/Features/Memos/MemosView.swift b/Memola/Features/Memos/MemosView.swift index 331016c..6ff6fe9 100644 --- a/Memola/Features/Memos/MemosView.swift +++ b/Memola/Features/Memos/MemosView.swift @@ -85,6 +85,7 @@ struct MemosView: View { let graphicContextObject = GraphicContextObject(\.viewContext) graphicContextObject.strokes = [] + graphicContextObject.erasers = .init() memoObject.canvas = canvasObject memoObject.tool = toolObject diff --git a/Memola/Persistence/Objects/EraserObject.swift b/Memola/Persistence/Objects/EraserObject.swift index 7c5ac48..5c35ab5 100644 --- a/Memola/Persistence/Objects/EraserObject.swift +++ b/Memola/Persistence/Objects/EraserObject.swift @@ -17,4 +17,5 @@ final class EraserObject: NSManagedObject { @NSManaged var thickness: CGFloat @NSManaged var quads: NSMutableOrderedSet @NSManaged var strokes: NSMutableSet + @NSManaged var graphicContext: GraphicContextObject? } diff --git a/Memola/Persistence/Objects/GraphicContextObject.swift b/Memola/Persistence/Objects/GraphicContextObject.swift index f62a765..9d97a0b 100644 --- a/Memola/Persistence/Objects/GraphicContextObject.swift +++ b/Memola/Persistence/Objects/GraphicContextObject.swift @@ -12,4 +12,5 @@ import Foundation final class GraphicContextObject: NSManagedObject { @NSManaged var canvas: CanvasObject? @NSManaged var strokes: NSMutableOrderedSet + @NSManaged var erasers: NSMutableSet } diff --git a/Memola/Resources/Models/MemolaModel.xcdatamodeld/MemolaModel.xcdatamodel/contents b/Memola/Resources/Models/MemolaModel.xcdatamodeld/MemolaModel.xcdatamodel/contents index c9ca0bf..862dffc 100644 --- a/Memola/Resources/Models/MemolaModel.xcdatamodeld/MemolaModel.xcdatamodel/contents +++ b/Memola/Resources/Models/MemolaModel.xcdatamodeld/MemolaModel.xcdatamodel/contents @@ -12,11 +12,13 @@ + + From 9c10e4621352e7af45025b81a15f63e571877860 Mon Sep 17 00:00:00 2001 From: dscyrescotti Date: Mon, 10 Jun 2024 00:51:50 +0700 Subject: [PATCH 10/17] feat: remove erasers in graphic context --- Memola/Canvas/Contexts/GraphicContext.swift | 4 +--- Memola/Canvas/Geometries/Stroke/Strokes/PenStroke.swift | 2 +- Memola/Features/Memos/MemosView.swift | 1 - Memola/Persistence/Objects/EraserObject.swift | 1 - Memola/Persistence/Objects/GraphicContextObject.swift | 1 - .../MemolaModel.xcdatamodeld/MemolaModel.xcdatamodel/contents | 2 -- 6 files changed, 2 insertions(+), 9 deletions(-) diff --git a/Memola/Canvas/Contexts/GraphicContext.swift b/Memola/Canvas/Contexts/GraphicContext.swift index d54af58..20da0fc 100644 --- a/Memola/Canvas/Contexts/GraphicContext.swift +++ b/Memola/Canvas/Contexts/GraphicContext.swift @@ -160,7 +160,7 @@ extension GraphicContext { thickness: pen.thickness ) eraserStroke.graphicContext = self - withPersistence(\.backgroundContext) { [graphicContext = object, _stroke = eraserStroke] context in + withPersistence(\.backgroundContext) { [_stroke = eraserStroke] context in let stroke = EraserObject(\.backgroundContext) stroke.bounds = _stroke.bounds stroke.color = _stroke.color @@ -169,8 +169,6 @@ extension GraphicContext { stroke.createdAt = _stroke.createdAt stroke.quads = [] stroke.strokes = .init() - stroke.graphicContext = graphicContext - graphicContext?.erasers.add(stroke) _stroke.object = stroke try context.saveIfNeeded() } diff --git a/Memola/Canvas/Geometries/Stroke/Strokes/PenStroke.swift b/Memola/Canvas/Geometries/Stroke/Strokes/PenStroke.swift index 38637c3..68f669f 100644 --- a/Memola/Canvas/Geometries/Stroke/Strokes/PenStroke.swift +++ b/Memola/Canvas/Geometries/Stroke/Strokes/PenStroke.swift @@ -65,7 +65,7 @@ final class PenStroke: Stroke, @unchecked Sendable { bounds: object.bounds, color: object.color, style: style, - createdAt: object.createdAt, + createdAt: object.createdAt, // sometimes crash here thickness: object.thickness ) self.object = object diff --git a/Memola/Features/Memos/MemosView.swift b/Memola/Features/Memos/MemosView.swift index 6ff6fe9..331016c 100644 --- a/Memola/Features/Memos/MemosView.swift +++ b/Memola/Features/Memos/MemosView.swift @@ -85,7 +85,6 @@ struct MemosView: View { let graphicContextObject = GraphicContextObject(\.viewContext) graphicContextObject.strokes = [] - graphicContextObject.erasers = .init() memoObject.canvas = canvasObject memoObject.tool = toolObject diff --git a/Memola/Persistence/Objects/EraserObject.swift b/Memola/Persistence/Objects/EraserObject.swift index 5c35ab5..7c5ac48 100644 --- a/Memola/Persistence/Objects/EraserObject.swift +++ b/Memola/Persistence/Objects/EraserObject.swift @@ -17,5 +17,4 @@ final class EraserObject: NSManagedObject { @NSManaged var thickness: CGFloat @NSManaged var quads: NSMutableOrderedSet @NSManaged var strokes: NSMutableSet - @NSManaged var graphicContext: GraphicContextObject? } diff --git a/Memola/Persistence/Objects/GraphicContextObject.swift b/Memola/Persistence/Objects/GraphicContextObject.swift index 9d97a0b..f62a765 100644 --- a/Memola/Persistence/Objects/GraphicContextObject.swift +++ b/Memola/Persistence/Objects/GraphicContextObject.swift @@ -12,5 +12,4 @@ import Foundation final class GraphicContextObject: NSManagedObject { @NSManaged var canvas: CanvasObject? @NSManaged var strokes: NSMutableOrderedSet - @NSManaged var erasers: NSMutableSet } diff --git a/Memola/Resources/Models/MemolaModel.xcdatamodeld/MemolaModel.xcdatamodel/contents b/Memola/Resources/Models/MemolaModel.xcdatamodeld/MemolaModel.xcdatamodel/contents index 862dffc..c9ca0bf 100644 --- a/Memola/Resources/Models/MemolaModel.xcdatamodeld/MemolaModel.xcdatamodel/contents +++ b/Memola/Resources/Models/MemolaModel.xcdatamodeld/MemolaModel.xcdatamodel/contents @@ -12,13 +12,11 @@ - - From 2ef8956ad2f320e546d1e019fa36382e1b1b349b Mon Sep 17 00:00:00 2001 From: dscyrescotti Date: Mon, 10 Jun 2024 00:53:23 +0700 Subject: [PATCH 11/17] refactor: clean up --- Memola/Canvas/RenderPasses/GraphicRenderPass.swift | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Memola/Canvas/RenderPasses/GraphicRenderPass.swift b/Memola/Canvas/RenderPasses/GraphicRenderPass.swift index f798406..aca8d71 100644 --- a/Memola/Canvas/RenderPasses/GraphicRenderPass.swift +++ b/Memola/Canvas/RenderPasses/GraphicRenderPass.swift @@ -107,11 +107,6 @@ class GraphicRenderPass: RenderPass { graphicContext.eraserStrokes.remove(eraserStroke) continue } - descriptor.colorAttachments[0].loadAction = clearsTexture ? .clear : .load - clearsTexture = false - eraserRenderPass.stroke = eraserStroke - eraserRenderPass.descriptor = descriptor - eraserRenderPass.draw(on: canvas, with: renderer) } } } From 7d52733d5dacfea63af6c66bec6c4f85f6add1ec Mon Sep 17 00:00:00 2001 From: dscyrescotti Date: Mon, 10 Jun 2024 22:44:55 +0700 Subject: [PATCH 12/17] feat: add undo and redo logic for eraser stroke --- Memola/Canvas/Contexts/GraphicContext.swift | 58 ++++++++++++++----- .../Stroke/Strokes/EraserStroke.swift | 4 +- .../Geometries/Stroke/Strokes/PenStroke.swift | 3 +- Memola/Canvas/History/History.swift | 20 +++++-- Memola/Persistence/Objects/EraserObject.swift | 2 +- Memola/Persistence/Objects/StrokeObject.swift | 2 +- 6 files changed, 68 insertions(+), 21 deletions(-) diff --git a/Memola/Canvas/Contexts/GraphicContext.swift b/Memola/Canvas/Contexts/GraphicContext.swift index 20da0fc..9467fc1 100644 --- a/Memola/Canvas/Contexts/GraphicContext.swift +++ b/Memola/Canvas/Contexts/GraphicContext.swift @@ -10,7 +10,6 @@ import MetalKit import CoreData import Foundation -#warning("TODO: to update history undo and redo logic") final class GraphicContext: @unchecked Sendable { var tree: RTree = RTree(maxEntries: 8) var eraserStrokes: Set = [] @@ -46,26 +45,59 @@ final class GraphicContext: @unchecked Sendable { func undoGraphic(for event: HistoryEvent) { switch event { case .stroke(let stroke): - guard let _stroke = stroke.stroke(as: PenStroke.self) else { return } - let deletedStroke = tree.remove(_stroke.anyStroke, in: _stroke.strokeBox) - withPersistence(\.backgroundContext) { [stroke = deletedStroke] context in - stroke?.stroke(as: PenStroke.self)?.object?.graphicContext = nil - try context.saveIfNeeded() + switch stroke.style { + case .marker: + guard let penStroke = stroke.stroke(as: PenStroke.self) else { return } + let deletedStroke = tree.remove(penStroke.anyStroke, in: penStroke.strokeBox) + withPersistence(\.backgroundContext) { [stroke = deletedStroke] context in + stroke?.stroke(as: PenStroke.self)?.object?.graphicContext = nil + try context.saveIfNeeded() + } + case .eraser: + guard let eraserStroke = stroke.stroke(as: EraserStroke.self) else { return } + eraserStrokes.remove(eraserStroke) + let penStrokes = eraserStroke.penStrokes + withPersistence(\.backgroundContext) { [penStrokes] context in + for penStroke in penStrokes { + penStroke.eraserStrokes.remove(eraserStroke) + if let object = eraserStroke.object { + penStroke.object?.erasers.remove(object) + } + } + } } previousStroke = nil } - } func redoGraphic(for event: HistoryEvent) { switch event { case .stroke(let stroke): - if let stroke = stroke.stroke(as: PenStroke.self) { - tree.insert(stroke.anyStroke, in: stroke.strokeBox) - } - withPersistence(\.backgroundContext) { [weak self, stroke] context in - stroke.stroke(as: PenStroke.self)?.object?.graphicContext = self?.object - try context.saveIfNeeded() + switch stroke.style { + case .marker: + guard let penStroke = stroke.stroke(as: PenStroke.self) else { + break + } + tree.insert(penStroke.anyStroke, in: penStroke.strokeBox) + withPersistence(\.backgroundContext) { [weak self, penStroke] context in + penStroke.object?.graphicContext = self?.object + try context.saveIfNeeded() + } + case .eraser: + guard let eraserStroke = stroke.stroke(as: EraserStroke.self) else { + break + } + eraserStrokes.insert(eraserStroke) + let penStrokes = eraserStroke.penStrokes + withPersistence(\.backgroundContext) { [eraserStroke, penStrokes] context in + for penStroke in penStrokes { + penStroke.eraserStrokes.insert(eraserStroke) + if let object = eraserStroke.object { + penStroke.object?.erasers.add(object) + } + } + try context.saveIfNeeded() + } } previousStroke = nil } diff --git a/Memola/Canvas/Geometries/Stroke/Strokes/EraserStroke.swift b/Memola/Canvas/Geometries/Stroke/Strokes/EraserStroke.swift index f96f3df..06bf90a 100644 --- a/Memola/Canvas/Geometries/Stroke/Strokes/EraserStroke.swift +++ b/Memola/Canvas/Geometries/Stroke/Strokes/EraserStroke.swift @@ -34,6 +34,7 @@ final class EraserStroke: Stroke, @unchecked Sendable { weak var graphicContext: GraphicContext? var finishesSaving: Bool = false + var penStrokes: Set = [] init( bounds: [CGFloat], @@ -58,7 +59,7 @@ final class EraserStroke: Stroke, @unchecked Sendable { bounds: object.bounds, color: object.color, style: style, - createdAt: object.createdAt, + createdAt: object.createdAt ?? .now, thickness: object.thickness ) self.object = object @@ -111,6 +112,7 @@ final class EraserStroke: Stroke, @unchecked Sendable { for stroke in graphicContext.tree.search(box: _quad.quadBox) { if let _penStroke = stroke.stroke(as: PenStroke.self), !_penStroke.eraserStrokes.contains(self) { _penStroke.eraserStrokes.insert(self) + penStrokes.insert(_penStroke) if let penStroke = _penStroke.object { penStroke.erasers.add(eraser) eraser.strokes.add(penStroke) diff --git a/Memola/Canvas/Geometries/Stroke/Strokes/PenStroke.swift b/Memola/Canvas/Geometries/Stroke/Strokes/PenStroke.swift index 68f669f..81f9a67 100644 --- a/Memola/Canvas/Geometries/Stroke/Strokes/PenStroke.swift +++ b/Memola/Canvas/Geometries/Stroke/Strokes/PenStroke.swift @@ -61,11 +61,12 @@ final class PenStroke: Stroke, @unchecked Sendable { convenience init(object: StrokeObject) { let style = StrokeStyle(rawValue: object.style) ?? .marker + #warning("TODO: revisit here and check if there is any crash") self.init( bounds: object.bounds, color: object.color, style: style, - createdAt: object.createdAt, // sometimes crash here + createdAt: object.createdAt ?? .now, // sometimes crash here thickness: object.thickness ) self.object = object diff --git a/Memola/Canvas/History/History.swift b/Memola/Canvas/History/History.swift index 825f357..6125381 100644 --- a/Memola/Canvas/History/History.swift +++ b/Memola/Canvas/History/History.swift @@ -52,11 +52,23 @@ class History: ObservableObject { for event in redoStack { switch event { case .stroke(let _stroke): - withPersistence(\.backgroundContext) { context in - if let stroke = _stroke.stroke(as: PenStroke.self)?.object { - context.delete(stroke) + switch _stroke.style { + case .marker: + guard let penStroke = _stroke.stroke(as: PenStroke.self) else { return } + withPersistence(\.backgroundContext) { context in + if let stroke = penStroke.object { + context.delete(stroke) + } + try context.saveIfNeeded() + } + case .eraser: + guard let eraserStroke = _stroke.stroke(as: EraserStroke.self) else { return } + withPersistence(\.backgroundContext) { context in + if let stroke = eraserStroke.object { + context.delete(stroke) + } + try context.saveIfNeeded() } - try context.saveIfNeeded() } } } diff --git a/Memola/Persistence/Objects/EraserObject.swift b/Memola/Persistence/Objects/EraserObject.swift index 7c5ac48..ad8f648 100644 --- a/Memola/Persistence/Objects/EraserObject.swift +++ b/Memola/Persistence/Objects/EraserObject.swift @@ -13,7 +13,7 @@ final class EraserObject: NSManagedObject { @NSManaged var bounds: [CGFloat] @NSManaged var color: [CGFloat] @NSManaged var style: Int16 - @NSManaged var createdAt: Date + @NSManaged var createdAt: Date? @NSManaged var thickness: CGFloat @NSManaged var quads: NSMutableOrderedSet @NSManaged var strokes: NSMutableSet diff --git a/Memola/Persistence/Objects/StrokeObject.swift b/Memola/Persistence/Objects/StrokeObject.swift index a90f24a..e304f46 100644 --- a/Memola/Persistence/Objects/StrokeObject.swift +++ b/Memola/Persistence/Objects/StrokeObject.swift @@ -13,7 +13,7 @@ final class StrokeObject: NSManagedObject { @NSManaged var bounds: [CGFloat] @NSManaged var color: [CGFloat] @NSManaged var style: Int16 - @NSManaged var createdAt: Date + @NSManaged var createdAt: Date? @NSManaged var thickness: CGFloat @NSManaged var quads: NSMutableOrderedSet @NSManaged var erasers: NSMutableSet From 2e254dc4af38200b3cef7781e72df482a67ac938 Mon Sep 17 00:00:00 2001 From: dscyrescotti Date: Mon, 10 Jun 2024 22:53:29 +0700 Subject: [PATCH 13/17] feat: refresh objects --- Memola/Canvas/Contexts/GraphicContext.swift | 7 +++++++ Memola/Canvas/Core/Canvas.swift | 1 + 2 files changed, 8 insertions(+) diff --git a/Memola/Canvas/Contexts/GraphicContext.swift b/Memola/Canvas/Contexts/GraphicContext.swift index 9467fc1..a9ccfda 100644 --- a/Memola/Canvas/Contexts/GraphicContext.swift +++ b/Memola/Canvas/Contexts/GraphicContext.swift @@ -52,6 +52,7 @@ final class GraphicContext: @unchecked Sendable { withPersistence(\.backgroundContext) { [stroke = deletedStroke] context in stroke?.stroke(as: PenStroke.self)?.object?.graphicContext = nil try context.saveIfNeeded() + context.refreshAllObjects() } case .eraser: guard let eraserStroke = stroke.stroke(as: EraserStroke.self) else { return } @@ -64,6 +65,8 @@ final class GraphicContext: @unchecked Sendable { penStroke.object?.erasers.remove(object) } } + try context.saveIfNeeded() + context.refreshAllObjects() } } previousStroke = nil @@ -82,6 +85,7 @@ final class GraphicContext: @unchecked Sendable { withPersistence(\.backgroundContext) { [weak self, penStroke] context in penStroke.object?.graphicContext = self?.object try context.saveIfNeeded() + context.refreshAllObjects() } case .eraser: guard let eraserStroke = stroke.stroke(as: EraserStroke.self) else { @@ -97,6 +101,7 @@ final class GraphicContext: @unchecked Sendable { } } try context.saveIfNeeded() + context.refreshAllObjects() } } previousStroke = nil @@ -120,12 +125,14 @@ extension GraphicContext { withPersistenceSync(\.newBackgroundContext) { [_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] context in guard let self else { return } _stroke.loadQuads(with: self) + context.refreshAllObjects() } } } diff --git a/Memola/Canvas/Core/Canvas.swift b/Memola/Canvas/Core/Canvas.swift index 3a5931b..821d953 100644 --- a/Memola/Canvas/Core/Canvas.swift +++ b/Memola/Canvas/Core/Canvas.swift @@ -69,6 +69,7 @@ extension Canvas { func loadStrokes(_ bounds: CGRect) { withPersistence(\.backgroundContext) { [weak self, bounds] context in self?.graphicContext.loadQuads(bounds, on: context) + context.refreshAllObjects() } } } From 09b97da1fed013e1709daa6e0cb78a9a2570bdca Mon Sep 17 00:00:00 2001 From: dscyrescotti Date: Mon, 10 Jun 2024 23:11:11 +0700 Subject: [PATCH 14/17] feat: resolve memory issue --- Memola/Canvas/View/Canvas/CanvasView.swift | 6 +++--- Memola/Features/Memo/Memo/MemoView.swift | 9 +++------ Memola/Features/Memo/PenDock/PenDock.swift | 4 ++-- Memola/Features/Memo/Toolbar/Toolbar.swift | 13 ++++++++----- Memola/Features/Memos/MemosView.swift | 1 + 5 files changed, 17 insertions(+), 16 deletions(-) diff --git a/Memola/Canvas/View/Canvas/CanvasView.swift b/Memola/Canvas/View/Canvas/CanvasView.swift index 3842c6d..d697358 100644 --- a/Memola/Canvas/View/Canvas/CanvasView.swift +++ b/Memola/Canvas/View/Canvas/CanvasView.swift @@ -8,9 +8,9 @@ import SwiftUI struct CanvasView: UIViewControllerRepresentable { - @EnvironmentObject var tool: Tool - @EnvironmentObject var canvas: Canvas - @EnvironmentObject var history: History + @ObservedObject var tool: Tool + @ObservedObject var canvas: Canvas + @ObservedObject var history: History func makeUIViewController(context: Context) -> CanvasViewController { CanvasViewController(tool: tool, canvas: canvas, history: history) diff --git a/Memola/Features/Memo/Memo/MemoView.swift b/Memola/Features/Memo/Memo/MemoView.swift index 550b472..b12e1ed 100644 --- a/Memola/Features/Memo/Memo/MemoView.swift +++ b/Memola/Features/Memo/Memo/MemoView.swift @@ -27,17 +27,17 @@ struct MemoView: View { } var body: some View { - CanvasView() + CanvasView(tool: tool, canvas: canvas, history: history) .ignoresSafeArea() .overlay(alignment: .trailing) { - PenDock() + PenDock(tool: tool, canvas: canvas) } .overlay(alignment: .bottomLeading) { zoomControl } .disabled(textFieldState) .overlay(alignment: .top) { - Toolbar(memo: memo, size: size) + Toolbar(size: size, memo: memo, canvas: canvas, history: history) } .disabled(canvas.state == .loading || canvas.state == .closing) .overlay { @@ -50,9 +50,6 @@ struct MemoView: View { EmptyView() } } - .environmentObject(tool) - .environmentObject(canvas) - .environmentObject(history) } @ViewBuilder diff --git a/Memola/Features/Memo/PenDock/PenDock.swift b/Memola/Features/Memo/PenDock/PenDock.swift index 1fd2b5c..bcaa02c 100644 --- a/Memola/Features/Memo/PenDock/PenDock.swift +++ b/Memola/Features/Memo/PenDock/PenDock.swift @@ -8,8 +8,8 @@ import SwiftUI struct PenDock: View { - @EnvironmentObject var tool: Tool - @EnvironmentObject var canvas: Canvas + @ObservedObject var tool: Tool + @ObservedObject var canvas: Canvas let width: CGFloat = 90 let height: CGFloat = 30 diff --git a/Memola/Features/Memo/Toolbar/Toolbar.swift b/Memola/Features/Memo/Toolbar/Toolbar.swift index 6388fc0..24c7cba 100644 --- a/Memola/Features/Memo/Toolbar/Toolbar.swift +++ b/Memola/Features/Memo/Toolbar/Toolbar.swift @@ -10,9 +10,9 @@ import Foundation struct Toolbar: View { @Environment(\.dismiss) var dismiss - - @EnvironmentObject var history: History - @EnvironmentObject var canvas: Canvas + + @ObservedObject var canvas: Canvas + @ObservedObject var history: History @State var memo: MemoObject @State var title: String @@ -20,9 +20,11 @@ struct Toolbar: View { let size: CGFloat - init(memo: MemoObject, size: CGFloat) { - self.memo = memo + init(size: CGFloat, memo: MemoObject, canvas: Canvas, history: History) { self.size = size + self.memo = memo + self.canvas = canvas + self.history = history self.title = memo.title } @@ -136,6 +138,7 @@ struct Toolbar: View { } withPersistence(\.backgroundContext) { context in try? context.saveIfNeeded() + context.refreshAllObjects() DispatchQueue.main.async { withAnimation { canvas.state = .closed diff --git a/Memola/Features/Memos/MemosView.swift b/Memola/Features/Memos/MemosView.swift index 331016c..99e87c5 100644 --- a/Memola/Features/Memos/MemosView.swift +++ b/Memola/Features/Memos/MemosView.swift @@ -33,6 +33,7 @@ struct MemosView: View { MemoView(memo: memo) .onDisappear { withPersistence(\.viewContext) { context in + try context.saveIfNeeded() context.refreshAllObjects() } } From d9834644025b1c87dbc0464ca6d88a23fbdd8b72 Mon Sep 17 00:00:00 2001 From: dscyrescotti Date: Mon, 10 Jun 2024 23:25:32 +0700 Subject: [PATCH 15/17] refactor: use weak reference --- Memola/Canvas/Contexts/GraphicContext.swift | 47 ++++++++++--------- .../Stroke/Strokes/EraserStroke.swift | 2 +- .../Geometries/Stroke/Strokes/PenStroke.swift | 2 +- 3 files changed, 28 insertions(+), 23 deletions(-) diff --git a/Memola/Canvas/Contexts/GraphicContext.swift b/Memola/Canvas/Contexts/GraphicContext.swift index a9ccfda..8cdd836 100644 --- a/Memola/Canvas/Contexts/GraphicContext.swift +++ b/Memola/Canvas/Contexts/GraphicContext.swift @@ -49,17 +49,17 @@ final class GraphicContext: @unchecked Sendable { case .marker: guard let penStroke = stroke.stroke(as: PenStroke.self) else { return } let deletedStroke = tree.remove(penStroke.anyStroke, in: penStroke.strokeBox) - withPersistence(\.backgroundContext) { [stroke = deletedStroke] context in - stroke?.stroke(as: PenStroke.self)?.object?.graphicContext = nil + withPersistence(\.backgroundContext) { [weak penStroke] context in + penStroke?.object?.graphicContext = nil try context.saveIfNeeded() context.refreshAllObjects() } case .eraser: guard let eraserStroke = stroke.stroke(as: EraserStroke.self) else { return } eraserStrokes.remove(eraserStroke) - let penStrokes = eraserStroke.penStrokes - withPersistence(\.backgroundContext) { [penStrokes] context in - for penStroke in penStrokes { + withPersistence(\.backgroundContext) { [weak eraserStroke] context in + guard let eraserStroke else { return } + for penStroke in eraserStroke.penStrokes { penStroke.eraserStrokes.remove(eraserStroke) if let object = eraserStroke.object { penStroke.object?.erasers.remove(object) @@ -82,8 +82,8 @@ final class GraphicContext: @unchecked Sendable { break } tree.insert(penStroke.anyStroke, in: penStroke.strokeBox) - withPersistence(\.backgroundContext) { [weak self, penStroke] context in - penStroke.object?.graphicContext = self?.object + withPersistence(\.backgroundContext) { [weak self, weak penStroke] context in + penStroke?.object?.graphicContext = self?.object try context.saveIfNeeded() context.refreshAllObjects() } @@ -92,9 +92,9 @@ final class GraphicContext: @unchecked Sendable { break } eraserStrokes.insert(eraserStroke) - let penStrokes = eraserStroke.penStrokes - withPersistence(\.backgroundContext) { [eraserStroke, penStrokes] context in - for penStroke in penStrokes { + withPersistence(\.backgroundContext) { [weak eraserStroke] context in + guard let eraserStroke else { return } + for penStroke in eraserStroke.penStrokes { penStroke.eraserStrokes.insert(eraserStroke) if let object = eraserStroke.object { penStroke.object?.erasers.add(object) @@ -122,16 +122,16 @@ extension GraphicContext { let id = stroke.objectID queue.addOperation { [weak self] in guard let self else { return } - withPersistenceSync(\.newBackgroundContext) { [_stroke] context in + withPersistenceSync(\.newBackgroundContext) { [weak _stroke] context in guard let stroke = try? context.existingObject(with: id) as? StrokeObject else { return } - _stroke.loadQuads(from: stroke, with: self) + _stroke?.loadQuads(from: stroke, with: self) context.refreshAllObjects() } } } else { - withPersistence(\.backgroundContext) { [weak self] context in + withPersistence(\.backgroundContext) { [weak self, weak _stroke] context in guard let self else { return } - _stroke.loadQuads(with: self) + _stroke?.loadQuads(with: self) context.refreshAllObjects() } } @@ -175,7 +175,8 @@ extension GraphicContext { createdAt: .now, thickness: pen.thickness ) - withPersistence(\.backgroundContext) { [graphicContext = object, _stroke = penStroke] context in + withPersistence(\.backgroundContext) { [weak graphicContext = object, weak _stroke = penStroke] context in + guard let _stroke else { return } let stroke = StrokeObject(\.backgroundContext) stroke.bounds = _stroke.bounds stroke.color = _stroke.color @@ -199,7 +200,8 @@ extension GraphicContext { thickness: pen.thickness ) eraserStroke.graphicContext = self - withPersistence(\.backgroundContext) { [_stroke = eraserStroke] context in + withPersistence(\.backgroundContext) { [weak _stroke = eraserStroke] context in + guard let _stroke else { return } let stroke = EraserObject(\.backgroundContext) stroke.bounds = _stroke.bounds stroke.color = _stroke.color @@ -234,7 +236,8 @@ extension GraphicContext { if let penStroke = currentStroke.stroke(as: PenStroke.self) { penStroke.saveQuads() tree.insert(currentStroke.anyStroke, in: currentStroke.strokeBox) - withPersistence(\.backgroundContext) { [penStroke] context in + withPersistence(\.backgroundContext) { [weak penStroke] context in + guard let penStroke else { return } penStroke.object?.bounds = penStroke.bounds try context.saveIfNeeded() context.refreshAllObjects() @@ -242,7 +245,8 @@ extension GraphicContext { } else if let eraserStroke = currentStroke.stroke(as: EraserStroke.self) { eraserStroke.saveQuads() eraserStrokes.insert(eraserStroke) - withPersistence(\.backgroundContext) { [eraserStroke] context in + withPersistence(\.backgroundContext) { [weak eraserStroke] context in + guard let eraserStroke else { return } eraserStroke.object?.bounds = eraserStroke.bounds try context.saveIfNeeded() context.refreshAllObjects() @@ -258,7 +262,8 @@ extension GraphicContext { switch stroke.style { case .marker: guard let _stroke = stroke.stroke(as: PenStroke.self) else { break } - withPersistence(\.backgroundContext) { [graphicContext = object, _stroke] context in + 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) @@ -268,8 +273,8 @@ extension GraphicContext { case .eraser: guard let eraserStroke = stroke.stroke(as: EraserStroke.self) else { break } eraserStrokes.remove(eraserStroke) - withPersistence(\.backgroundContext) { [eraserStroke] context in - if let stroke = eraserStroke.object { + withPersistence(\.backgroundContext) { [weak eraserStroke] context in + if let stroke = eraserStroke?.object { context.delete(stroke) } try context.saveIfNeeded() diff --git a/Memola/Canvas/Geometries/Stroke/Strokes/EraserStroke.swift b/Memola/Canvas/Geometries/Stroke/Strokes/EraserStroke.swift index 06bf90a..c9552ad 100644 --- a/Memola/Canvas/Geometries/Stroke/Strokes/EraserStroke.swift +++ b/Memola/Canvas/Geometries/Stroke/Strokes/EraserStroke.swift @@ -98,7 +98,7 @@ final class EraserStroke: Stroke, @unchecked Sendable { let endIndex = endIndex ?? quads.endIndex let batch = quads[batchIndex.. Date: Mon, 10 Jun 2024 23:54:14 +0700 Subject: [PATCH 16/17] feat: use NSHashTable to avoid memory leak --- Memola/Canvas/Contexts/GraphicContext.swift | 4 ++-- Memola/Canvas/Geometries/Stroke/Strokes/EraserStroke.swift | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Memola/Canvas/Contexts/GraphicContext.swift b/Memola/Canvas/Contexts/GraphicContext.swift index 8cdd836..38dc787 100644 --- a/Memola/Canvas/Contexts/GraphicContext.swift +++ b/Memola/Canvas/Contexts/GraphicContext.swift @@ -59,7 +59,7 @@ final class GraphicContext: @unchecked Sendable { eraserStrokes.remove(eraserStroke) withPersistence(\.backgroundContext) { [weak eraserStroke] context in guard let eraserStroke else { return } - for penStroke in eraserStroke.penStrokes { + for penStroke in eraserStroke.penStrokes.allObjects { penStroke.eraserStrokes.remove(eraserStroke) if let object = eraserStroke.object { penStroke.object?.erasers.remove(object) @@ -94,7 +94,7 @@ final class GraphicContext: @unchecked Sendable { eraserStrokes.insert(eraserStroke) withPersistence(\.backgroundContext) { [weak eraserStroke] context in guard let eraserStroke else { return } - for penStroke in eraserStroke.penStrokes { + for penStroke in eraserStroke.penStrokes.allObjects { penStroke.eraserStrokes.insert(eraserStroke) if let object = eraserStroke.object { penStroke.object?.erasers.add(object) diff --git a/Memola/Canvas/Geometries/Stroke/Strokes/EraserStroke.swift b/Memola/Canvas/Geometries/Stroke/Strokes/EraserStroke.swift index c9552ad..a4a2451 100644 --- a/Memola/Canvas/Geometries/Stroke/Strokes/EraserStroke.swift +++ b/Memola/Canvas/Geometries/Stroke/Strokes/EraserStroke.swift @@ -34,7 +34,7 @@ final class EraserStroke: Stroke, @unchecked Sendable { weak var graphicContext: GraphicContext? var finishesSaving: Bool = false - var penStrokes: Set = [] + var penStrokes: NSHashTable = .weakObjects() init( bounds: [CGFloat], @@ -112,7 +112,7 @@ final class EraserStroke: Stroke, @unchecked Sendable { for stroke in graphicContext.tree.search(box: _quad.quadBox) { if let _penStroke = stroke.stroke(as: PenStroke.self), !_penStroke.eraserStrokes.contains(self) { _penStroke.eraserStrokes.insert(self) - penStrokes.insert(_penStroke) + penStrokes.add(_penStroke) if let penStroke = _penStroke.object { penStroke.erasers.add(eraser) eraser.strokes.add(penStroke) From e775d9e0165c481c6ca6193946173186c2272139 Mon Sep 17 00:00:00 2001 From: dscyrescotti Date: Tue, 11 Jun 2024 00:19:11 +0700 Subject: [PATCH 17/17] refactor: clean up --- Memola/Canvas/Contexts/GraphicContext.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Memola/Canvas/Contexts/GraphicContext.swift b/Memola/Canvas/Contexts/GraphicContext.swift index 38dc787..37c637c 100644 --- a/Memola/Canvas/Contexts/GraphicContext.swift +++ b/Memola/Canvas/Contexts/GraphicContext.swift @@ -48,7 +48,7 @@ final class GraphicContext: @unchecked Sendable { switch stroke.style { case .marker: guard let penStroke = stroke.stroke(as: PenStroke.self) else { return } - let deletedStroke = tree.remove(penStroke.anyStroke, in: penStroke.strokeBox) + tree.remove(penStroke.anyStroke, in: penStroke.strokeBox) withPersistence(\.backgroundContext) { [weak penStroke] context in penStroke?.object?.graphicContext = nil try context.saveIfNeeded()