From 8ee010b77a844a21c3d8086a9aeb6d7939f666bc Mon Sep 17 00:00:00 2001 From: dscyrescotti Date: Tue, 14 May 2024 23:53:24 +0700 Subject: [PATCH 1/6] feat: load only visible strokes --- Memola/Canvas/Contexts/GraphicContext.swift | 29 +++++++++++++++---- Memola/Canvas/Core/Canvas.swift | 18 +++++++++--- Memola/Canvas/Geometries/Stroke/Stroke.swift | 22 ++++++++++++++ .../RenderPasses/GraphicRenderPass.swift | 1 + .../ViewController/CanvasViewController.swift | 26 +++++++++++++---- Memola/Extensions/CGRect++.swift | 4 +++ Memola/Persistence/Objects/StrokeObject.swift | 1 + .../MemolaModel.xcdatamodel/contents | 1 + 8 files changed, 87 insertions(+), 15 deletions(-) diff --git a/Memola/Canvas/Contexts/GraphicContext.swift b/Memola/Canvas/Contexts/GraphicContext.swift index 56a8ff1..34198cf 100644 --- a/Memola/Canvas/Contexts/GraphicContext.swift +++ b/Memola/Canvas/Contexts/GraphicContext.swift @@ -60,18 +60,32 @@ final class GraphicContext: @unchecked Sendable { } extension GraphicContext { - func loadStrokes() { + func loadStrokes(_ bounds: CGRect) { guard let object else { return } self.strokes = object.strokes.compactMap { stroke -> Stroke? in guard let stroke = stroke as? StrokeObject else { return nil } let _stroke = Stroke(object: stroke) - _stroke.loadQuads() - withPersistence(\.backgroundContext) { [stroke] context in - context.refresh(stroke, mergeChanges: false) + if _stroke.isVisible(in: bounds) { + _stroke.loadQuads() + withPersistence(\.backgroundContext) { [stroke] context in + context.refresh(stroke, mergeChanges: false) + } + } else { + withPersistence(\.backgroundContext) { [stroke] context in + _stroke.loadQuads() + context.refresh(stroke, mergeChanges: false) + } } return _stroke } } + + func loadQuads(_ bounds: CGRect) { + for stroke in self.strokes { + guard stroke.isVisible(in: bounds), stroke.quads.isEmpty else { continue } + stroke.loadQuads() + } + } } extension GraphicContext: Drawable { @@ -93,6 +107,7 @@ extension GraphicContext: Drawable { extension GraphicContext { func beginStroke(at point: CGPoint, pen: Pen) -> Stroke { let stroke = Stroke( + bounds: [point.x - pen.thickness, point.y - pen.thickness, point.x + pen.thickness, point.y + pen.thickness], color: pen.color, style: pen.strokeStyle.rawValue, createdAt: .now, @@ -100,6 +115,7 @@ extension GraphicContext { ) withPersistence(\.backgroundContext) { [graphicContext = object, _stroke = stroke] context in let stroke = StrokeObject(\.backgroundContext) + stroke.bounds = _stroke.bounds stroke.color = _stroke.color stroke.style = _stroke.style stroke.thickness = _stroke.thickness @@ -126,10 +142,11 @@ extension GraphicContext { func endStroke(at point: CGPoint) { guard currentPoint != nil, let currentStroke else { return } currentStroke.finish(at: point) - let saveIndex = currentStroke.batchIndex - let quads = Array(currentStroke.quads[saveIndex.. Bool { + bounds.contains(strokeBounds) + } func begin(at point: CGPoint) { penStyle.anyPenStyle.generator.begin(at: point, on: self) @@ -106,6 +121,8 @@ extension Stroke { } func saveQuads(for quads: [Quad]) { + var topLeft: CGPoint = CGPoint(x: bounds[0], y: bounds[1]) + var bottomRight: CGPoint = CGPoint(x: bounds[2], y: bounds[3]) for _quad in quads { let quad = QuadObject(\.backgroundContext) quad.originX = _quad.originX.cgFloat @@ -116,7 +133,12 @@ extension Stroke { quad.color = _quad.getColor() quad.stroke = object object?.quads.add(quad) + topLeft.x = min(quad.originX, topLeft.x) + topLeft.y = min(quad.originY, topLeft.y) + bottomRight.x = max(quad.originX, bottomRight.x) + bottomRight.y = max(quad.originY, bottomRight.y) } + bounds = [topLeft.x, topLeft.y, bottomRight.x, bottomRight.y] } } diff --git a/Memola/Canvas/RenderPasses/GraphicRenderPass.swift b/Memola/Canvas/RenderPasses/GraphicRenderPass.swift index fd685de..0f8e4e5 100644 --- a/Memola/Canvas/RenderPasses/GraphicRenderPass.swift +++ b/Memola/Canvas/RenderPasses/GraphicRenderPass.swift @@ -49,6 +49,7 @@ class GraphicRenderPass: RenderPass { if graphicContext.previousStroke === stroke || graphicContext.currentStroke === stroke { continue } + guard stroke.isVisible(in: canvas.bounds) else { continue } descriptor.colorAttachments[0].loadAction = clearsTexture ? .clear : .load clearsTexture = false if stroke.isEraserPenStyle { diff --git a/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift b/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift index 69faa94..0a37e68 100644 --- a/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift +++ b/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift @@ -41,13 +41,13 @@ class CanvasViewController: UIViewController { super.viewDidLoad() configureViews() configureListeners() - - loadMemo() } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) resizeDocumentView() + updateDocumentBounds() + loadMemo() } override func viewDidLayoutSubviews() { @@ -119,11 +119,11 @@ extension CanvasViewController { let newFrame = CGRect(x: 0, y: 0, width: width, height: height) drawingView.frame = newFrame - scrollView.setZoomScale(canvas.minimumZoomScale, animated: true) + scrollView.setZoomScale(canvas.defaultZoomScale, animated: true) centerDocumentView(to: newSize) - let offsetX = (newFrame.width * canvas.minimumZoomScale - view.frame.width) / 2 - let offsetY = (newFrame.height * canvas.minimumZoomScale - view.frame.height) / 2 + let offsetX = (newFrame.width * canvas.defaultZoomScale - view.frame.width) / 2 + let offsetY = (newFrame.height * canvas.defaultZoomScale - view.frame.height) / 2 let point = CGPoint(x: offsetX, y: offsetY) scrollView.setContentOffset(point, animated: true) @@ -138,6 +138,20 @@ extension CanvasViewController { let horizontalPadding = documentViewSize.width < scrollViewSize.width ? (scrollViewSize.width - documentViewSize.width) / 2 : 0 self.scrollView.contentInset = UIEdgeInsets(top: verticalPadding, left: horizontalPadding, bottom: verticalPadding, right: horizontalPadding) } + + func updateDocumentBounds() { + var bounds = scrollView.bounds.muliply(by: drawingView.ratio / scrollView.zoomScale) + let xDelta = bounds.minX * 0.05 + let yDelta = bounds.minY * 0.05 + bounds.origin.x -= xDelta + bounds.origin.y -= yDelta + bounds.size.width += xDelta * 2 + bounds.size.height += yDelta * 2 + canvas.bounds = bounds + if canvas.state == .loaded { + canvas.loadStrokes(bounds) + } + } } extension CanvasViewController { @@ -208,6 +222,7 @@ extension CanvasViewController: UIScrollViewDelegate { } func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) { + updateDocumentBounds() centerDocumentView() magnificationEnded() } @@ -234,6 +249,7 @@ extension CanvasViewController: UIScrollViewDelegate { } func scrollViewDidEndScrolling(_ scrollView: UIScrollView) { + updateDocumentBounds() draggingEnded() } } diff --git a/Memola/Extensions/CGRect++.swift b/Memola/Extensions/CGRect++.swift index a576e31..ca95178 100644 --- a/Memola/Extensions/CGRect++.swift +++ b/Memola/Extensions/CGRect++.swift @@ -16,4 +16,8 @@ extension CGRect { t = t.scaledBy(x: rect.width, y: rect.height) return t } + + func muliply(by factor: CGFloat) -> CGRect { + CGRect(origin: origin.muliply(by: factor), size: size.multiply(by: factor)) + } } diff --git a/Memola/Persistence/Objects/StrokeObject.swift b/Memola/Persistence/Objects/StrokeObject.swift index ffa049b..2d9767a 100644 --- a/Memola/Persistence/Objects/StrokeObject.swift +++ b/Memola/Persistence/Objects/StrokeObject.swift @@ -10,6 +10,7 @@ import Foundation @objc(StrokeObject) final class StrokeObject: NSManagedObject { + @NSManaged var bounds: [CGFloat] @NSManaged var color: [CGFloat] @NSManaged var style: Int16 @NSManaged var createdAt: Date diff --git a/Memola/Resources/Models/MemolaModel.xcdatamodeld/MemolaModel.xcdatamodel/contents b/Memola/Resources/Models/MemolaModel.xcdatamodeld/MemolaModel.xcdatamodel/contents index 72e5132..1b277fa 100644 --- a/Memola/Resources/Models/MemolaModel.xcdatamodeld/MemolaModel.xcdatamodel/contents +++ b/Memola/Resources/Models/MemolaModel.xcdatamodeld/MemolaModel.xcdatamodel/contents @@ -26,6 +26,7 @@ + From 42eb3e661e8d3ef10dc51a93e5e8d655de3b9fe3 Mon Sep 17 00:00:00 2001 From: dscyrescotti Date: Wed, 15 May 2024 01:58:46 +0700 Subject: [PATCH 2/6] feat: add intersection check on canvas view bounds --- Memola/Canvas/Geometries/Stroke/Stroke.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Memola/Canvas/Geometries/Stroke/Stroke.swift b/Memola/Canvas/Geometries/Stroke/Stroke.swift index f4821a1..712f7a8 100644 --- a/Memola/Canvas/Geometries/Stroke/Stroke.swift +++ b/Memola/Canvas/Geometries/Stroke/Stroke.swift @@ -72,7 +72,7 @@ final class Stroke: @unchecked Sendable { } func isVisible(in bounds: CGRect) -> Bool { - bounds.contains(strokeBounds) + bounds.contains(strokeBounds) || bounds.intersects(strokeBounds) } func begin(at point: CGPoint) { From 9d2b633f868219c3e29b98fa97cbd06acc2c234f Mon Sep 17 00:00:00 2001 From: dscyrescotti Date: Wed, 15 May 2024 01:59:30 +0700 Subject: [PATCH 3/6] feat: wait until vertex generation is completed --- Memola/Canvas/RenderPasses/StrokeRenderPass.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Memola/Canvas/RenderPasses/StrokeRenderPass.swift b/Memola/Canvas/RenderPasses/StrokeRenderPass.swift index 5d33aca..c268735 100644 --- a/Memola/Canvas/RenderPasses/StrokeRenderPass.swift +++ b/Memola/Canvas/RenderPasses/StrokeRenderPass.swift @@ -83,6 +83,7 @@ class StrokeRenderPass: RenderPass { computeEncoder.dispatchThreadgroups(numThreadgroups, threadsPerThreadgroup: threadsPerGroup) computeEncoder.endEncoding() quadCommandBuffer.commit() + quadCommandBuffer.waitUntilCompleted() } private func drawStrokeTexture(on canvas: Canvas, with renderer: Renderer) { From c0c039ebfe3a640da9b4a2e4932da181664f2699 Mon Sep 17 00:00:00 2001 From: dscyrescotti Date: Wed, 15 May 2024 09:06:52 +0700 Subject: [PATCH 4/6] feat: smooth out stroke --- Memola/Canvas/Contexts/GraphicContext.swift | 4 +- .../SolidPointStrokeGenerator.swift | 37 +++++++++---------- 2 files changed, 20 insertions(+), 21 deletions(-) diff --git a/Memola/Canvas/Contexts/GraphicContext.swift b/Memola/Canvas/Contexts/GraphicContext.swift index 34198cf..758cec6 100644 --- a/Memola/Canvas/Contexts/GraphicContext.swift +++ b/Memola/Canvas/Contexts/GraphicContext.swift @@ -134,7 +134,9 @@ extension GraphicContext { func appendStroke(with point: CGPoint) { guard let currentStroke else { return } - guard let currentPoint, point.distance(to: currentPoint) > currentStroke.thickness * currentStroke.penStyle.anyPenStyle.stepRate else { return } + guard let currentPoint, point.distance(to: currentPoint) > currentStroke.thickness * currentStroke.penStyle.anyPenStyle.stepRate else { + return + } currentStroke.append(to: point) self.currentPoint = point } diff --git a/Memola/Canvas/Geometries/Stroke/Generators/SolidPointStrokeGenerator.swift b/Memola/Canvas/Geometries/Stroke/Generators/SolidPointStrokeGenerator.swift index 1fef8b3..6b728d3 100644 --- a/Memola/Canvas/Geometries/Stroke/Generators/SolidPointStrokeGenerator.swift +++ b/Memola/Canvas/Geometries/Stroke/Generators/SolidPointStrokeGenerator.swift @@ -16,11 +16,11 @@ struct SolidPointStrokeGenerator: StrokeGenerator { } func append(to point: CGPoint, on stroke: Stroke) { - guard stroke.keyPoints.count > 0 else { + guard stroke.keyPoints.endIndex > 0 else { return } stroke.keyPoints.append(point) - switch stroke.keyPoints.count { + switch stroke.keyPoints.endIndex { case 2: let start = stroke.keyPoints[0] let end = stroke.keyPoints[1] @@ -28,7 +28,7 @@ struct SolidPointStrokeGenerator: StrokeGenerator { addCurve(from: start, to: end, by: control, on: stroke) case 3: stroke.removeQuads(from: stroke.quadIndex + 1) - let index = stroke.keyPoints.count - 1 + let index = stroke.keyPoints.endIndex - 1 var start = stroke.keyPoints[index - 2] var end = CGPoint.middle(p1: stroke.keyPoints[index - 2], p2: stroke.keyPoints[index - 1]) var control = CGPoint.middle(p1: start, p2: end) @@ -39,7 +39,7 @@ struct SolidPointStrokeGenerator: StrokeGenerator { addCurve(from: start, to: end, by: control, on: stroke) default: smoothOutPath(on: stroke) - let index = stroke.keyPoints.count - 1 + let index = stroke.keyPoints.endIndex - 1 let start = CGPoint.middle(p1: stroke.keyPoints[index - 2], p2: stroke.keyPoints[index - 1]) let control = stroke.keyPoints[index - 1] let end = CGPoint.middle(p1: stroke.keyPoints[index - 1], p2: stroke.keyPoints[index]) @@ -48,12 +48,12 @@ struct SolidPointStrokeGenerator: StrokeGenerator { } func finish(at point: CGPoint, on stroke: Stroke) { - switch stroke.keyPoints.count { + switch stroke.keyPoints.endIndex { case 0...1: break default: append(to: point, on: stroke) - let index = stroke.keyPoints.count - 1 + let index = stroke.keyPoints.endIndex - 1 let start = CGPoint.middle(p1: stroke.keyPoints[index - 2], p2: stroke.keyPoints[index - 1]) let end = stroke.keyPoints[index] let control = CGPoint.middle(p1: start, p2: end) @@ -64,37 +64,34 @@ struct SolidPointStrokeGenerator: StrokeGenerator { private func smoothOutPath(on stroke: Stroke) { stroke.removeQuads(from: stroke.quadIndex + 1) adjustPreviousKeyPoint(on: stroke) - switch stroke.keyPoints.count { + switch stroke.keyPoints.endIndex { case 4: - let index = stroke.keyPoints.count - 2 + let index = stroke.keyPoints.endIndex - 2 let start = stroke.keyPoints[index - 2] let end = CGPoint.middle(p1: stroke.keyPoints[index - 2], p2: stroke.keyPoints[index - 1]) let control = CGPoint.middle(p1: start, p2: end) addCurve(from: start, to: end, by: control, on: stroke) fallthrough default: - let index = stroke.keyPoints.count - 2 + let index = stroke.keyPoints.endIndex - 2 let start = CGPoint.middle(p1: stroke.keyPoints[index - 2], p2: stroke.keyPoints[index - 1]) let control = stroke.keyPoints[index - 1] let end = CGPoint.middle(p1: stroke.keyPoints[index - 1], p2: stroke.keyPoints[index]) addCurve(from: start, to: end, by: control, on: stroke) } - stroke.quadIndex = stroke.quads.count - 1 + stroke.quadIndex = stroke.quads.endIndex - 1 } private func adjustPreviousKeyPoint(on stroke: Stroke) { - let index = stroke.keyPoints.count - 1 - let prev = stroke.keyPoints[index - 1] + let index = stroke.keyPoints.endIndex - 1 + let prev = stroke.keyPoints[index - 2] + let mid = stroke.keyPoints[index - 1] let current = stroke.keyPoints[index] - let averageX = (prev.x + current.x) / 2 - let averageY = (prev.y + current.y) / 2 + let averageX = (prev.x + current.x + mid.x) / 3 + let averageY = (prev.y + current.y + mid.y) / 3 let point = CGPoint(x: averageX, y: averageY) - if index != 0 { - stroke.keyPoints[index] = point - } - if index - 1 != 0 { - stroke.keyPoints[index - 1] = point - } + stroke.keyPoints[index] = point + stroke.keyPoints[index - 1] = point } private func addPoint(_ point: CGPoint, on stroke: Stroke) { From 3ad8075a286341083e8ef20fd85adbfbf4db082b Mon Sep 17 00:00:00 2001 From: dscyrescotti Date: Wed, 15 May 2024 13:43:22 +0700 Subject: [PATCH 5/6] feat: update canvas zoom scale --- Memola/Canvas/Core/Canvas.swift | 4 +--- Memola/Canvas/Tool/Pen/PenStyles/EraserPenStyle.swift | 2 +- Memola/Canvas/Tool/Pen/PenStyles/MarkerPenStyle.swift | 2 +- Memola/Canvas/View/Bridge/Views/DrawingView.swift | 2 +- Memola/Features/Memos/MemosView.swift | 4 ++-- 5 files changed, 6 insertions(+), 8 deletions(-) diff --git a/Memola/Canvas/Core/Canvas.swift b/Memola/Canvas/Core/Canvas.swift index 6275f69..a2471c5 100644 --- a/Memola/Canvas/Core/Canvas.swift +++ b/Memola/Canvas/Core/Canvas.swift @@ -18,7 +18,7 @@ final class Canvas: ObservableObject, Identifiable, @unchecked Sendable { var graphicContext = GraphicContext() let viewPortContext = ViewPortContext() - let maximumZoomScale: CGFloat = 30 + let maximumZoomScale: CGFloat = 35 let minimumZoomScale: CGFloat = 5 let defaultZoomScale: CGFloat = 20 @@ -48,7 +48,6 @@ extension Canvas { func load() { withPersistence(\.backgroundContext) { [weak self, canvasID, bounds] context in DispatchQueue.main.async { [weak self] in - NSLog(Date().formatted(.dateTime.minute().second().secondFraction(.fractional(2)))) self?.state = .loading } guard let canvas = context.object(with: canvasID) as? CanvasObject else { @@ -59,7 +58,6 @@ extension Canvas { self?.graphicContext.loadStrokes(bounds) context.refresh(canvas, mergeChanges: false) DispatchQueue.main.async { [weak self] in - NSLog(Date().formatted(.dateTime.minute().second().secondFraction(.fractional(2)))) self?.state = .loaded } } diff --git a/Memola/Canvas/Tool/Pen/PenStyles/EraserPenStyle.swift b/Memola/Canvas/Tool/Pen/PenStyles/EraserPenStyle.swift index 25ca7c8..a18d8df 100644 --- a/Memola/Canvas/Tool/Pen/PenStyles/EraserPenStyle.swift +++ b/Memola/Canvas/Tool/Pen/PenStyles/EraserPenStyle.swift @@ -12,7 +12,7 @@ struct EraserPenStyle: PenStyle { var textureName: String = "point-texture" - var thinkness: (min: CGFloat, max: CGFloat) = (1, 120) + var thinkness: (min: CGFloat, max: CGFloat) = (0.5, 120) var color: [CGFloat] = [1, 1, 1, 0] diff --git a/Memola/Canvas/Tool/Pen/PenStyles/MarkerPenStyle.swift b/Memola/Canvas/Tool/Pen/PenStyles/MarkerPenStyle.swift index 0b821d8..ed82047 100644 --- a/Memola/Canvas/Tool/Pen/PenStyles/MarkerPenStyle.swift +++ b/Memola/Canvas/Tool/Pen/PenStyles/MarkerPenStyle.swift @@ -12,7 +12,7 @@ struct MarkerPenStyle: PenStyle { var textureName: String = "point-texture" - var thinkness: (min: CGFloat, max: CGFloat) = (1, 120) + var thinkness: (min: CGFloat, max: CGFloat) = (0.5, 120) var color: [CGFloat] = [1, 0.38, 0.38, 1] diff --git a/Memola/Canvas/View/Bridge/Views/DrawingView.swift b/Memola/Canvas/View/Bridge/Views/DrawingView.swift index 0b798e2..84b58d5 100644 --- a/Memola/Canvas/View/Bridge/Views/DrawingView.swift +++ b/Memola/Canvas/View/Bridge/Views/DrawingView.swift @@ -32,7 +32,7 @@ class DrawingView: UIView { } func updateDrawableSize(with size: CGSize) { - renderView.drawableSize = size.multiply(by: 2.0) + renderView.drawableSize = size.multiply(by: 3) } override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { diff --git a/Memola/Features/Memos/MemosView.swift b/Memola/Features/Memos/MemosView.swift index 1b4f079..57c6395 100644 --- a/Memola/Features/Memos/MemosView.swift +++ b/Memola/Features/Memos/MemosView.swift @@ -69,8 +69,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 graphicContextObject = GraphicContextObject(context: managedObjectContext) graphicContextObject.strokes = [] From 5748fe685deb5598c035877e3e333ad303b45201 Mon Sep 17 00:00:00 2001 From: dscyrescotti Date: Wed, 15 May 2024 18:24:48 +0700 Subject: [PATCH 6/6] feat: load quads using new background context --- Memola/Canvas/Contexts/GraphicContext.swift | 15 ++++++++++++--- Memola/Canvas/Geometries/Stroke/Stroke.swift | 7 +++++++ Memola/Persistence/Core/Persistence.swift | 18 ++++++++++++++++++ 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/Memola/Canvas/Contexts/GraphicContext.swift b/Memola/Canvas/Contexts/GraphicContext.swift index 758cec6..cf6f565 100644 --- a/Memola/Canvas/Contexts/GraphicContext.swift +++ b/Memola/Canvas/Contexts/GraphicContext.swift @@ -62,13 +62,21 @@ final class GraphicContext: @unchecked Sendable { extension GraphicContext { func loadStrokes(_ bounds: CGRect) { guard let object else { return } + let queue = OperationQueue() + queue.qualityOfService = .userInteractive self.strokes = object.strokes.compactMap { stroke -> Stroke? in guard let stroke = stroke as? StrokeObject else { return nil } let _stroke = Stroke(object: stroke) if _stroke.isVisible(in: bounds) { - _stroke.loadQuads() - withPersistence(\.backgroundContext) { [stroke] context in - context.refresh(stroke, mergeChanges: false) + let id = stroke.objectID + queue.addOperation { + withPersistenceSync(\.newBackgroundContext) { [_stroke] context in + guard let stroke = try? context.existingObject(with: id) as? StrokeObject else { return } + _stroke.loadQuads(from: stroke) + } + withPersistence(\.backgroundContext) { [stroke] context in + context.refresh(stroke, mergeChanges: false) + } } } else { withPersistence(\.backgroundContext) { [stroke] context in @@ -78,6 +86,7 @@ extension GraphicContext { } return _stroke } + queue.waitUntilAllOperationsAreFinished() } func loadQuads(_ bounds: CGRect) { diff --git a/Memola/Canvas/Geometries/Stroke/Stroke.swift b/Memola/Canvas/Geometries/Stroke/Stroke.swift index 712f7a8..0a13a11 100644 --- a/Memola/Canvas/Geometries/Stroke/Stroke.swift +++ b/Memola/Canvas/Geometries/Stroke/Stroke.swift @@ -98,6 +98,13 @@ extension Stroke { } } + func loadQuads(from object: StrokeObject) { + quads = object.quads.compactMap { quad in + guard let quad = quad as? QuadObject else { return nil } + return Quad(object: quad) + } + } + func addQuad(at point: CGPoint, rotation: CGFloat, shape: QuadShape) -> Quad { let quad = Quad( origin: point, diff --git a/Memola/Persistence/Core/Persistence.swift b/Memola/Persistence/Core/Persistence.swift index 4cb4a9a..5ebac21 100644 --- a/Memola/Persistence/Core/Persistence.swift +++ b/Memola/Persistence/Core/Persistence.swift @@ -26,6 +26,13 @@ final class Persistence { return context }() + var newBackgroundContext: NSManagedObjectContext { + let context = persistentContainer.newBackgroundContext() + context.undoManager = nil + context.automaticallyMergesChangesFromParent = true + return context + } + lazy var persistentContainer: NSPersistentContainer = { let persistentStore = NSPersistentStoreDescription() persistentStore.shouldMigrateStoreAutomatically = true @@ -83,3 +90,14 @@ func withPersistence(_ keypath: KeyPath, _ } } } + +func withPersistenceSync(_ keypath: KeyPath, _ task: @escaping (NSManagedObjectContext) throws -> Void) { + let context = Persistence.shared[keyPath: keypath] + context.performAndWait { + do { + try task(context) + } catch { + NSLog("[Memola] - \(error.localizedDescription)") + } + } +}