From 1f9c176eb07cfb29d98ece4c9a46dba5760b54df Mon Sep 17 00:00:00 2001 From: dscyrescotti Date: Wed, 8 May 2024 22:51:20 +0700 Subject: [PATCH 01/11] feat: reduce memory footprint --- Memola.xcodeproj/project.pbxproj | 34 +++--- .../xcshareddata/xcschemes/Memola.xcscheme | 78 +++++++++++++ Memola/Canvas/Contexts/GraphicContext.swift | 75 ++++++------ Memola/Canvas/Core/Canvas.swift | 83 +++----------- .../Canvas/Geometries/Primitives/Quad.swift | 48 +++----- .../Geometries/Primitives/StrokeQuad.swift | 18 +++ .../SolidPointStrokeGenerator.swift | 17 +-- Memola/Canvas/Geometries/Stroke/Stroke.swift | 108 ++++++++---------- .../RenderPasses/GraphicRenderPass.swift | 3 +- Memola/Canvas/Tool/Pen/Core/Pen.swift | 13 ++- Memola/Canvas/View/CanvasView.swift | 2 +- .../Entities => Features/Memo}/Memo.swift | 1 + Memola/Features/Memo/MemoView.swift | 21 ++-- Memola/Features/Memos/MemosView.swift | 34 +++--- Memola/Persistence/Core/Persistence.swift | 10 +- .../Transformers/QuadValueTransformer.swift | 53 +++++++++ .../MemolaModel.xcdatamodel/contents | 25 +++- 17 files changed, 370 insertions(+), 253 deletions(-) create mode 100644 Memola.xcodeproj/xcshareddata/xcschemes/Memola.xcscheme create mode 100644 Memola/Canvas/Geometries/Primitives/StrokeQuad.swift rename Memola/{Persistence/Entities => Features/Memo}/Memo.swift (91%) create mode 100644 Memola/Persistence/Transformers/QuadValueTransformer.swift diff --git a/Memola.xcodeproj/project.pbxproj b/Memola.xcodeproj/project.pbxproj index 9ba2c66..a060075 100644 --- a/Memola.xcodeproj/project.pbxproj +++ b/Memola.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + EC4538892BEBCAE000A86FEC /* Quad.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC4538882BEBCAE000A86FEC /* Quad.swift */; }; + EC771E602BEB6EE50053CC68 /* QuadValueTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC771E5F2BEB6EE50053CC68 /* QuadValueTransformer.swift */; }; 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 */; }; @@ -61,7 +63,7 @@ ECA738F22BE6128F00A4542E /* Collection++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738F12BE6128F00A4542E /* Collection++.swift */; }; ECA738F42BE612A000A4542E /* Array++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738F32BE612A000A4542E /* Array++.swift */; }; ECA738F62BE612B700A4542E /* MTLDevice++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738F52BE612B700A4542E /* MTLDevice++.swift */; }; - ECA738F82BE612EB00A4542E /* Quad.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738F72BE612EB00A4542E /* Quad.swift */; }; + ECA738F82BE612EB00A4542E /* StrokeQuad.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738F72BE612EB00A4542E /* StrokeQuad.swift */; }; ECA738FC2BE61C5200A4542E /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738FB2BE61C5200A4542E /* Persistence.swift */; }; ECA739012BE61D9C00A4542E /* MemolaModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = ECA738FF2BE61D9C00A4542E /* MemolaModel.xcdatamodeld */; }; ECA739052BE61E3100A4542E /* Memo.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA739042BE61E3100A4542E /* Memo.swift */; }; @@ -69,6 +71,8 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + EC4538882BEBCAE000A86FEC /* Quad.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Quad.swift; sourceTree = ""; }; + EC771E5F2BEB6EE50053CC68 /* QuadValueTransformer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuadValueTransformer.swift; sourceTree = ""; }; EC7F6BE82BE5E6E300A34A7B /* Memola.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Memola.app; sourceTree = BUILT_PRODUCTS_DIR; }; EC7F6BEB2BE5E6E300A34A7B /* MemolaApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemolaApp.swift; sourceTree = ""; }; EC7F6BEF2BE5E6E400A34A7B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -124,7 +128,7 @@ ECA738F12BE6128F00A4542E /* Collection++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection++.swift"; sourceTree = ""; }; ECA738F32BE612A000A4542E /* Array++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array++.swift"; sourceTree = ""; }; ECA738F52BE612B700A4542E /* MTLDevice++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MTLDevice++.swift"; sourceTree = ""; }; - ECA738F72BE612EB00A4542E /* Quad.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Quad.swift; sourceTree = ""; }; + ECA738F72BE612EB00A4542E /* StrokeQuad.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StrokeQuad.swift; sourceTree = ""; }; ECA738FB2BE61C5200A4542E /* Persistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = ""; }; ECA739002BE61D9C00A4542E /* MemolaModel.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MemolaModel.xcdatamodel; sourceTree = ""; }; ECA739042BE61E3100A4542E /* Memo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Memo.swift; sourceTree = ""; }; @@ -158,6 +162,14 @@ path = ViewController; sourceTree = ""; }; + EC771E5C2BEB37FC0053CC68 /* Transformers */ = { + isa = PBXGroup; + children = ( + EC771E5F2BEB6EE50053CC68 /* QuadValueTransformer.swift */, + ); + path = Transformers; + sourceTree = ""; + }; EC7F6BDF2BE5E6E300A34A7B = { isa = PBXGroup; children = ( @@ -224,6 +236,7 @@ ECA7387B2BE5EF3500A4542E /* Memo */ = { isa = PBXGroup; children = ( + ECA739042BE61E3100A4542E /* Memo.swift */, ECA7387C2BE5EF4B00A4542E /* MemoView.swift */, ECA739072BE623F300A4542E /* PenToolView.swift */, ); @@ -291,7 +304,8 @@ ECA738982BE6015700A4542E /* Primitives */ = { isa = PBXGroup; children = ( - ECA738F72BE612EB00A4542E /* Quad.swift */, + ECA738F72BE612EB00A4542E /* StrokeQuad.swift */, + EC4538882BEBCAE000A86FEC /* Quad.swift */, ); path = Primitives; sourceTree = ""; @@ -449,7 +463,7 @@ ECA738FA2BE61B1700A4542E /* Persistence */ = { isa = PBXGroup; children = ( - ECA739032BE61E2600A4542E /* Entities */, + EC771E5C2BEB37FC0053CC68 /* Transformers */, ECA739022BE61DE700A4542E /* Core */, ); path = Persistence; @@ -479,14 +493,6 @@ path = Core; sourceTree = ""; }; - ECA739032BE61E2600A4542E /* Entities */ = { - isa = PBXGroup; - children = ( - ECA739042BE61E3100A4542E /* Memo.swift */, - ); - path = Entities; - sourceTree = ""; - }; ECA739062BE61F7500A4542E /* Core */ = { isa = PBXGroup; children = ( @@ -566,6 +572,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + EC771E602BEB6EE50053CC68 /* QuadValueTransformer.swift in Sources */, ECA738B02BE60D0B00A4542E /* CanvasViewController.swift in Sources */, ECA738E42BE6110800A4542E /* Drawable.swift in Sources */, ECA738AD2BE60CC600A4542E /* DrawingView.swift in Sources */, @@ -603,6 +610,7 @@ ECA738DC2BE6108D00A4542E /* StrokeRenderPass.swift in Sources */, ECA738F42BE612A000A4542E /* Array++.swift in Sources */, ECA739052BE61E3100A4542E /* Memo.swift in Sources */, + EC4538892BEBCAE000A86FEC /* Quad.swift in Sources */, ECA7388F2BE600DA00A4542E /* Grid.metal in Sources */, ECA738C92BE60EF700A4542E /* GraphicContext.swift in Sources */, ECA738F62BE612B700A4542E /* MTLDevice++.swift in Sources */, @@ -620,7 +628,7 @@ EC7F6BEC2BE5E6E300A34A7B /* MemolaApp.swift in Sources */, ECA738A02BE601E400A4542E /* ViewPortVertex.swift in Sources */, ECA738BC2BE60E0300A4542E /* Tool.swift in Sources */, - ECA738F82BE612EB00A4542E /* Quad.swift in Sources */, + ECA738F82BE612EB00A4542E /* StrokeQuad.swift in Sources */, ECA738972BE6014200A4542E /* Graphic.metal in Sources */, ECA7388A2BE6006A00A4542E /* PipelineStates.swift in Sources */, ); diff --git a/Memola.xcodeproj/xcshareddata/xcschemes/Memola.xcscheme b/Memola.xcodeproj/xcshareddata/xcschemes/Memola.xcscheme new file mode 100644 index 0000000..95b05ce --- /dev/null +++ b/Memola.xcodeproj/xcshareddata/xcschemes/Memola.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Memola/Canvas/Contexts/GraphicContext.swift b/Memola/Canvas/Contexts/GraphicContext.swift index 39d5e24..34486a0 100644 --- a/Memola/Canvas/Contexts/GraphicContext.swift +++ b/Memola/Canvas/Contexts/GraphicContext.swift @@ -7,45 +7,28 @@ import Combine import MetalKit +import CoreData import Foundation -protocol GraphicContextDelegate: AnyObject { - var didUpdate: PassthroughSubject { get set } -} +@objc(GraphicContext) +class GraphicContext: NSManagedObject { + @NSManaged var id: UUID + @NSManaged var canvas: Canvas + @NSManaged var strokes: NSMutableOrderedSet -class GraphicContext: Codable { - var strokes: [Stroke] = [] var currentStroke: Stroke? var previousStroke: Stroke? var currentPoint: CGPoint? - var renderType: RenderType = .finished - var vertices: [ViewPortVertex] = [] var vertexCount: Int = 4 var vertexBuffer: MTLBuffer? - weak var delegate: GraphicContextDelegate? - - init() { + override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) { + super.init(entity: entity, insertInto: context) setViewPortVertices() } - enum CodingKeys: CodingKey { - case strokes - } - - required init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - self.strokes = try container.decode([Stroke].self, forKey: .strokes) - setViewPortVertices() - } - - func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(self.strokes, forKey: .strokes) - } - func setViewPortVertices() { vertexBuffer = nil vertices = [ @@ -57,19 +40,17 @@ class GraphicContext: Codable { } func undoGraphic() { - guard !strokes.isEmpty else { return } - strokes.removeLast() + guard let stroke = strokes.lastObject as? Stroke else { return } + strokes.remove(stroke) previousStroke = nil - delegate?.didUpdate.send() } func redoGraphic(for event: HistoryEvent) { switch event { case .stroke(let stroke): - strokes.append(stroke) + strokes.add(stroke) previousStroke = nil } - delegate?.didUpdate.send() } } @@ -91,12 +72,15 @@ extension GraphicContext: Drawable { extension GraphicContext { func beginStroke(at point: CGPoint, pen: Pen) -> Stroke { - let stroke = Stroke( - color: pen.color, - style: pen.style, - thickness: pen.thickness - ) - strokes.append(stroke) + let stroke = Stroke(context: Persistence.shared.viewContext) + stroke.id = UUID() + stroke.color = pen.color + stroke.style = pen.strokeStyle.rawValue + stroke.thickness = pen.thickness + stroke.createdAt = .now + stroke.strokeQuads = [] + stroke.graphicContext = self + strokes.add(stroke) currentStroke = stroke currentPoint = point currentStroke?.begin(at: point) @@ -105,23 +89,28 @@ extension GraphicContext { func appendStroke(with point: CGPoint) { guard let currentStroke else { return } - guard let currentPoint, point.distance(to: currentPoint) > currentStroke.thickness * currentStroke.style.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 } func endStroke(at point: CGPoint) { - guard currentPoint != nil else { return } - currentStroke?.finish(at: point) + guard currentPoint != nil, let currentStroke else { return } + currentStroke.finish(at: point) + currentStroke.saveQuads() + do { + try Persistence.shared.viewContext.save() + } catch { + NSLog("[Memola] - \(error.localizedDescription)") + } previousStroke = currentStroke - currentStroke = nil + self.currentStroke = nil self.currentPoint = nil - delegate?.didUpdate.send() } func cancelStroke() { - if !strokes.isEmpty { - strokes.removeLast() + if let stroke = strokes.lastObject { + strokes.remove(stroke) } currentStroke = nil currentPoint = nil diff --git a/Memola/Canvas/Core/Canvas.swift b/Memola/Canvas/Core/Canvas.swift index d3c9a46..4f93c28 100644 --- a/Memola/Canvas/Core/Canvas.swift +++ b/Memola/Canvas/Core/Canvas.swift @@ -10,92 +10,43 @@ import CoreData import MetalKit import Foundation -final class Canvas: NSObject, ObservableObject, Identifiable, Codable, GraphicContextDelegate { - let size: CGSize +@objc(Canvas) +class Canvas: NSManagedObject, Identifiable { + @NSManaged var id: UUID + @NSManaged var width: CGFloat + @NSManaged var height: CGFloat + @NSManaged var memo: Memo + @NSManaged var graphicContext: GraphicContext + + let gridContext = GridContext() + let viewPortContext = ViewPortContext() let maximumZoomScale: CGFloat = 25 let minimumZoomScale: CGFloat = 3.1 var transform: simd_float4x4 = .init() - - var uniformsBuffer: MTLBuffer? - - let gridContext = GridContext() - var graphicContext = GraphicContext() - let viewPortContext = ViewPortContext() - var clipBounds: CGRect = .zero var zoomScale: CGFloat = .zero - - weak var memo: Memo? - var graphicLoader: (() throws -> GraphicContext)? + var uniformsBuffer: MTLBuffer? @Published var state: State = .initial - lazy var didUpdate = PassthroughSubject() + var size: CGSize { CGSize(width: width, height: height) } var hasValidStroke: Bool { if let currentStroke = graphicContext.currentStroke { return Date.now.timeIntervalSince(currentStroke.createdAt) * 1000 > 80 } return false } - - init(size: CGSize = .init(width: 4_000, height: 4_000)) { - self.size = size - } - - enum CodingKeys: CodingKey { - case size - case graphicContext - } - - required init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - self.size = try container.decode(CGSize.self, forKey: .size) - self.graphicLoader = { try container.decode(GraphicContext.self, forKey: .graphicContext) } - } } // MARK: - Actions extension Canvas { func load() { - guard let graphicLoader else { return } - Task(priority: .high) { [unowned self, graphicLoader] in - await MainActor.run { - self.state = .loading - } - do { - let graphicContext = try graphicLoader() - graphicContext.delegate = self - await MainActor.run { - self.graphicContext = graphicContext - self.state = .loaded - } - } catch { - NSLog("[Memola] - \(error.localizedDescription)") - await MainActor.run { - self.state = .failed - } - } + state = .loading + graphicContext.strokes.forEach { + ($0 as? Stroke)?.loadVertices() } - } - - func save(on managedObjectContext: NSManagedObjectContext) async { - guard let memo else { return } - do { - memo.data = try JSONEncoder().encode(self) - memo.updatedAt = Date() - try managedObjectContext.save() - } catch { - NSLog("[Memola] - \(error.localizedDescription)") - } - } - - func listen(on managedObjectContext: NSManagedObjectContext) { -// Task(priority: .utility) { [unowned self] in -// for await _ in didUpdate.throttle(for: 500, scheduler: DispatchQueue.global(qos: .utility), latest: false).values { -// await save(on: managedObjectContext) -// } -// } + state = .loaded } } @@ -148,7 +99,7 @@ extension Canvas { } func getNewlyAddedStroke() -> Stroke? { - graphicContext.strokes.last + graphicContext.strokes.lastObject as? Stroke } } diff --git a/Memola/Canvas/Geometries/Primitives/Quad.swift b/Memola/Canvas/Geometries/Primitives/Quad.swift index fda3f2d..5e8eda4 100644 --- a/Memola/Canvas/Geometries/Primitives/Quad.swift +++ b/Memola/Canvas/Geometries/Primitives/Quad.swift @@ -2,31 +2,27 @@ // Quad.swift // Memola // -// Created by Dscyre Scotti on 5/4/24. +// Created by Dscyre Scotti on 5/8/24. // -import MetalKit import Foundation -class Quad { +struct Quad: Codable { var origin: CGPoint var color: [CGFloat] var size: CGFloat var rotation: CGFloat - var vertices: [QuadVertex] = [] + var shape: QuadShape - var vertexBuffer: MTLBuffer? - var vertexCount: Int = 0 - - init(origin: CGPoint, size: CGFloat, color: [CGFloat], rotation: CGFloat, shape: Shape = .rounded) { + init(origin: CGPoint, size: CGFloat, color: [CGFloat], rotation: CGFloat, shape: QuadShape = .rounded) { self.origin = origin self.size = size self.color = color self.rotation = rotation - generateVertices(shape) + self.shape = shape } - func generateVertices(_ shape: Shape) { + func generateVertices(_ shape: QuadShape) -> [QuadVertex] { switch shape { case .rounded: generateRoundedQuad() @@ -39,9 +35,9 @@ class Quad { } } - func generateRoundedQuad() { + func generateRoundedQuad() -> [QuadVertex] { let halfSize = size * 0.5 - vertices = [ + return [ QuadVertex(x: origin.x - halfSize, y: origin.y - halfSize, textCoord: CGPoint(x: 0, y: 0), color: color, origin: origin, rotation: rotation), QuadVertex(x: origin.x + halfSize, y: origin.y - halfSize, textCoord: CGPoint(x: 1, y: 0), color: color, origin: origin, rotation: rotation), QuadVertex(x: origin.x - halfSize, y: origin.y + halfSize, textCoord: CGPoint(x: 0, y: 1), color: color, origin: origin, rotation: rotation), @@ -49,13 +45,12 @@ class Quad { QuadVertex(x: origin.x - halfSize, y: origin.y + halfSize, textCoord: CGPoint(x: 0, y: 1), color: color, origin: origin, rotation: rotation), QuadVertex(x: origin.x + halfSize, y: origin.y + halfSize, textCoord: CGPoint(x: 1, y: 1), color: color, origin: origin, rotation: rotation) ] - vertexCount = vertices.count } - func generateSquaredQuad() { + func generateSquaredQuad() -> [QuadVertex] { let vHalfSize = size * 0.5 let hHalfSize = size * 0.15 - vertices = [ + return [ QuadVertex(x: origin.x - hHalfSize, y: origin.y - vHalfSize, textCoord: CGPoint(x: 0, y: 0), color: color, origin: origin, rotation: rotation), QuadVertex(x: origin.x + hHalfSize, y: origin.y - vHalfSize, textCoord: CGPoint(x: 1, y: 0), color: color, origin: origin, rotation: rotation), QuadVertex(x: origin.x - hHalfSize, y: origin.y + vHalfSize, textCoord: CGPoint(x: 0, y: 1), color: color, origin: origin, rotation: rotation), @@ -63,13 +58,12 @@ class Quad { QuadVertex(x: origin.x - hHalfSize, y: origin.y + vHalfSize, textCoord: CGPoint(x: 0, y: 1), color: color, origin: origin, rotation: rotation), QuadVertex(x: origin.x + hHalfSize, y: origin.y + vHalfSize, textCoord: CGPoint(x: 1, y: 1), color: color, origin: origin, rotation: rotation) ] - vertexCount = vertices.count } - func generateCalligraphicQuad(vFactor: CGFloat, hFactor: CGFloat) { + func generateCalligraphicQuad(vFactor: CGFloat, hFactor: CGFloat) -> [QuadVertex] { let vHalfSize = size * vFactor * 0.5 let hHalfSize = size * hFactor * 0.5 - vertices = [ + return [ QuadVertex(x: origin.x - hHalfSize, y: origin.y - vHalfSize, textCoord: CGPoint(x: 0, y: 0), color: color, origin: origin, rotation: rotation), QuadVertex(x: origin.x + hHalfSize, y: origin.y - vHalfSize, textCoord: CGPoint(x: 1, y: 0), color: color, origin: origin, rotation: rotation), QuadVertex(x: origin.x - hHalfSize, y: origin.y + vHalfSize, textCoord: CGPoint(x: 0, y: 1), color: color, origin: origin, rotation: rotation), @@ -77,14 +71,13 @@ class Quad { QuadVertex(x: origin.x - hHalfSize, y: origin.y + vHalfSize, textCoord: CGPoint(x: 0, y: 1), color: color, origin: origin, rotation: rotation), QuadVertex(x: origin.x + hHalfSize, y: origin.y + vHalfSize, textCoord: CGPoint(x: 1, y: 1), color: color, origin: origin, rotation: rotation) ] - vertexCount = vertices.count } - func generateTrapezoidQuad(topFactor: CGFloat, bottomFactor: CGFloat, heightFactor: CGFloat) { + func generateTrapezoidQuad(topFactor: CGFloat, bottomFactor: CGFloat, heightFactor: CGFloat) -> [QuadVertex] { let vHalfSize = size * heightFactor * 0.5 let hTopHalfSize = size * topFactor * 0.5 let hBottomHalfSize = size * bottomFactor * 0.5 - vertices = [ + return [ QuadVertex(x: origin.x - hTopHalfSize, y: origin.y - vHalfSize, textCoord: CGPoint(x: 0, y: 0), color: color, origin: origin, rotation: rotation), QuadVertex(x: origin.x + hBottomHalfSize, y: origin.y - vHalfSize, textCoord: CGPoint(x: 1, y: 0), color: color, origin: origin, rotation: rotation), QuadVertex(x: origin.x - hTopHalfSize, y: origin.y + vHalfSize, textCoord: CGPoint(x: 0, y: 1), color: color, origin: origin, rotation: rotation), @@ -92,15 +85,12 @@ class Quad { QuadVertex(x: origin.x - hTopHalfSize, y: origin.y + vHalfSize, textCoord: CGPoint(x: 0, y: 1), color: color, origin: origin, rotation: rotation), QuadVertex(x: origin.x + hBottomHalfSize, y: origin.y + vHalfSize, textCoord: CGPoint(x: 1, y: 1), color: color, origin: origin, rotation: rotation) ] - vertexCount = vertices.count } } -extension Quad { - enum Shape { - case rounded - case squared - case calligraphic(CGFloat, CGFloat) - case trapezoid(topFactor: CGFloat, bottomFactor: CGFloat, heightFactor: CGFloat) - } +enum QuadShape: Codable { + case rounded + case squared + case calligraphic(CGFloat, CGFloat) + case trapezoid(topFactor: CGFloat, bottomFactor: CGFloat, heightFactor: CGFloat) } diff --git a/Memola/Canvas/Geometries/Primitives/StrokeQuad.swift b/Memola/Canvas/Geometries/Primitives/StrokeQuad.swift new file mode 100644 index 0000000..f07d4e4 --- /dev/null +++ b/Memola/Canvas/Geometries/Primitives/StrokeQuad.swift @@ -0,0 +1,18 @@ +// +// StrokeQuad.swift +// Memola +// +// Created by Dscyre Scotti on 5/4/24. +// + +import MetalKit +import Foundation + +class StrokeQuad: NSObject, Codable { + var quad: Quad + + init(quad: Quad) { + self.quad = quad + } +} + diff --git a/Memola/Canvas/Geometries/Stroke/Generators/SolidPointStrokeGenerator.swift b/Memola/Canvas/Geometries/Stroke/Generators/SolidPointStrokeGenerator.swift index 7a06d70..af7b511 100644 --- a/Memola/Canvas/Geometries/Stroke/Generators/SolidPointStrokeGenerator.swift +++ b/Memola/Canvas/Geometries/Stroke/Generators/SolidPointStrokeGenerator.swift @@ -27,7 +27,7 @@ struct SolidPointStrokeGenerator: StrokeGenerator { let control = CGPoint.middle(p1: start, p2: end) addCurve(from: start, to: end, by: control, on: stroke) case 3: - discardPoints(upto: stroke.vertexIndex, on: stroke) + discardVertices(upto: stroke.vertexIndex, on: stroke) let index = stroke.keyPoints.count - 1 var start = stroke.keyPoints[index - 2] var end = CGPoint.middle(p1: stroke.keyPoints[index - 2], p2: stroke.keyPoints[index - 1]) @@ -62,7 +62,7 @@ struct SolidPointStrokeGenerator: StrokeGenerator { } private func smoothOutPath(on stroke: Stroke) { - discardPoints(upto: stroke.vertexIndex, on: stroke) + discardVertices(upto: stroke.vertexIndex, on: stroke) adjustPreviousKeyPoint(on: stroke) switch stroke.keyPoints.count { case 4: @@ -106,7 +106,8 @@ struct SolidPointStrokeGenerator: StrokeGenerator { rotation = CGFloat.random(in: 0...360) * .pi / 180 } let quad = Quad(origin: point, size: stroke.thickness, color: stroke.color, rotation: rotation) - stroke.vertices.append(contentsOf: quad.vertices) + stroke._quads.append(quad) + stroke.vertices.append(contentsOf: quad.generateVertices(quad.shape)) stroke.vertexCount = stroke.vertices.endIndex } @@ -115,9 +116,9 @@ struct SolidPointStrokeGenerator: StrokeGenerator { let factor: CGFloat switch configuration.granularity { case .automatic: - factor = min(6, 1 / (stroke.thickness * 10 / 500)) + factor = min(3.5, 1 / (stroke.thickness * 10 / 300)) case .fixed: - factor = 1 / (stroke.thickness * stroke.style.stepRate) + factor = 1 / (stroke.thickness * stroke.penStyle.anyPenStyle.stepRate) case .none: factor = 1 / (stroke.thickness * 10 / 500) } @@ -134,7 +135,7 @@ struct SolidPointStrokeGenerator: StrokeGenerator { #warning("TODO: remove later") private func addLine(from start: CGPoint, to end: CGPoint, on stroke: Stroke) { let distance = end.distance(to: start) - let segments = max(distance / stroke.style.stepRate, 2) + let segments = max(distance / stroke.penStyle.anyPenStyle.stepRate, 2) for i in 0.. + @NSManaged var graphicContext: GraphicContext? + var angle: CGFloat = 0 + var penStyle: Style { + Style(rawValue: style) ?? .marker + } + + var quadIndex: Int = -1 var vertexIndex: Int = -1 var keyPoints: [CGPoint] = [] var thicknessFactor: CGFloat = 0.7 var vertices: [QuadVertex] = [] + var _quads: [Quad] = [] var vertexBuffer: MTLBuffer? var vertexCount: Int = 0 - let createdAt: Date = Date() - var texture: MTLTexture? var isEmpty: Bool { @@ -31,73 +42,38 @@ class Stroke: Codable { } var isEraserPenStyle: Bool { - style is EraserPenStyle - } - - init(color: [CGFloat], style: any PenStyle, thickness: CGFloat) { - self.color = color - self.style = style - self.thickness = thickness - } - - enum CodingKeys: CodingKey { - case color - case style - case thickness - case vertices - } - - required init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - color = try container.decode([CGFloat].self, forKey: .color) - let style: String = try container.decode(String.self, forKey: .style) - thickness = try container.decode(CGFloat.self, forKey: .thickness) - vertices = try container.decode([QuadVertex].self, forKey: .vertices) - vertexCount = vertices.count - switch style { - case "marker": - self.style = .marker - case "eraser": - self.style = .eraser - default: - throw DecodingError.valueNotFound(PenStyle.self, .init(codingPath: [CodingKeys.style], debugDescription: "There is no pen style called `\(style)`.")) - } - } - - func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(color, forKey: .color) - try container.encode(thickness, forKey: .thickness) - try container.encode(vertices, forKey: .vertices) - let styleName: String - switch style { - case is MarkerPenStyle: - styleName = "marker" - case is EraserPenStyle: - styleName = "eraser" - default: - fatalError() - } - try container.encode(styleName, forKey: .style) + penStyle == .eraser } func begin(at point: CGPoint) { - style.generator.begin(at: point, on: self) + penStyle.anyPenStyle.generator.begin(at: point, on: self) } func append(to point: CGPoint) { - style.generator.append(to: point, on: self) + penStyle.anyPenStyle.generator.append(to: point, on: self) } func finish(at point: CGPoint) { - style.generator.finish(at: point, on: self) + penStyle.anyPenStyle.generator.finish(at: point, on: self) + keyPoints.removeAll() + } + + func loadVertices() { + vertices = strokeQuads + .flatMap { $0.quad.generateVertices($0.quad.shape) } + vertexCount = vertices.endIndex + } + + func saveQuads() { + strokeQuads = _quads.map(StrokeQuad.init) + _quads.removeAll() } } extension Stroke: Drawable { func prepare(device: MTLDevice) { if texture == nil { - texture = style.loadTexture(on: device) + texture = penStyle.anyPenStyle.loadTexture(on: device) } vertexBuffer = device.makeBuffer(bytes: &vertices, length: MemoryLayout.stride * vertexCount, options: .cpuCacheModeWriteCombined) } @@ -110,3 +86,19 @@ extension Stroke: Drawable { renderEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: vertexCount) } } + +extension Stroke { + enum Style: Int16 { + case marker + case eraser + + var anyPenStyle: any PenStyle { + switch self { + case .marker: + return MarkerPenStyle.marker + case .eraser: + return EraserPenStyle.eraser + } + } + } +} diff --git a/Memola/Canvas/RenderPasses/GraphicRenderPass.swift b/Memola/Canvas/RenderPasses/GraphicRenderPass.swift index cdb4099..e22dd34 100644 --- a/Memola/Canvas/RenderPasses/GraphicRenderPass.swift +++ b/Memola/Canvas/RenderPasses/GraphicRenderPass.swift @@ -45,7 +45,8 @@ class GraphicRenderPass: RenderPass { if renderer.redrawsGraphicRender { canvas.setGraphicRenderType(.finished) - for stroke in graphicContext.strokes { + for stroke in graphicContext.strokes.array { + guard let stroke = stroke as? Stroke else { continue } if graphicContext.previousStroke === stroke || graphicContext.currentStroke === stroke { continue } diff --git a/Memola/Canvas/Tool/Pen/Core/Pen.swift b/Memola/Canvas/Tool/Pen/Core/Pen.swift index 8ef887a..d2a5079 100644 --- a/Memola/Canvas/Tool/Pen/Core/Pen.swift +++ b/Memola/Canvas/Tool/Pen/Core/Pen.swift @@ -9,7 +9,7 @@ import SwiftUI import Foundation class Pen: NSObject, ObservableObject, Identifiable { - @Published var style: PenStyle + @Published var style: any PenStyle @Published var color: [CGFloat] @Published var thickness: CGFloat @@ -18,6 +18,17 @@ class Pen: NSObject, ObservableObject, Identifiable { self.color = color self.thickness = thickness } + + var strokeStyle: Stroke.Style { + switch style { + case is MarkerPenStyle: + return .marker + case is EraserPenStyle: + return .eraser + default: + return .marker + } + } } extension Pen { diff --git a/Memola/Canvas/View/CanvasView.swift b/Memola/Canvas/View/CanvasView.swift index 3842c6d..5bc9ab9 100644 --- a/Memola/Canvas/View/CanvasView.swift +++ b/Memola/Canvas/View/CanvasView.swift @@ -8,8 +8,8 @@ import SwiftUI struct CanvasView: UIViewControllerRepresentable { + let canvas: Canvas @EnvironmentObject var tool: Tool - @EnvironmentObject var canvas: Canvas @EnvironmentObject var history: History func makeUIViewController(context: Context) -> CanvasViewController { diff --git a/Memola/Persistence/Entities/Memo.swift b/Memola/Features/Memo/Memo.swift similarity index 91% rename from Memola/Persistence/Entities/Memo.swift rename to Memola/Features/Memo/Memo.swift index 3ea1cad..31855a2 100644 --- a/Memola/Persistence/Entities/Memo.swift +++ b/Memola/Features/Memo/Memo.swift @@ -15,6 +15,7 @@ class Memo: NSManagedObject { @NSManaged var data: Data @NSManaged var createdAt: Date @NSManaged var updatedAt: Date + @NSManaged var canvas: Canvas } extension Memo: Identifiable { } diff --git a/Memola/Features/Memo/MemoView.swift b/Memola/Features/Memo/MemoView.swift index b8fe602..876f4e7 100644 --- a/Memola/Features/Memo/MemoView.swift +++ b/Memola/Features/Memo/MemoView.swift @@ -14,10 +14,10 @@ struct MemoView: View { @StateObject var tool = Tool() @StateObject var history = History() - @EnvironmentObject var canvas: Canvas + let canvas: Canvas var body: some View { - CanvasView() + CanvasView(canvas: canvas) .ignoresSafeArea() .overlay(alignment: .bottomTrailing) { PenToolView() @@ -53,9 +53,6 @@ struct MemoView: View { .environmentObject(tool) .environmentObject(canvas) .environmentObject(history) - .task { - canvas.listen(on: managedObjectContext) - } } var historyTool: some View { @@ -91,15 +88,13 @@ struct MemoView: View { } func closeMemo() { - Task(priority: .high) { - await MainActor.run { - canvas.state = .closing - } - await canvas.save(on: managedObjectContext) - await MainActor.run { - canvas.state = .closed - dismiss() + if managedObjectContext.hasChanges { + do { + try managedObjectContext.save() + } catch { + NSLog("[Memola] - \(error.localizedDescription)") } } + dismiss() } } diff --git a/Memola/Features/Memos/MemosView.swift b/Memola/Features/Memos/MemosView.swift index fbc64f6..c52813a 100644 --- a/Memola/Features/Memos/MemosView.swift +++ b/Memola/Features/Memos/MemosView.swift @@ -12,7 +12,7 @@ struct MemosView: View { @FetchRequest(sortDescriptors: []) var memos: FetchedResults - @State var canvas: Canvas? + @State var memo: Memo? var body: some View { NavigationStack { @@ -29,9 +29,8 @@ struct MemosView: View { } } } - .fullScreenCover(item: $canvas) { canvas in - MemoView() - .environmentObject(canvas) + .fullScreenCover(item: $memo) { memo in + MemoView(canvas: memo.canvas) } } @@ -59,29 +58,34 @@ struct MemosView: View { func createMemo(title: String) { do { - let data = try JSONEncoder().encode(Canvas()) let memo = Memo(context: managedObjectContext) memo.id = UUID() memo.title = title - memo.data = data memo.createdAt = .now memo.updatedAt = .now + let canvas = Canvas(context: managedObjectContext) + canvas.id = UUID() + canvas.width = 4_000 + canvas.height = 4_000 + + let graphicContext = GraphicContext(context: managedObjectContext) + graphicContext.id = UUID() + graphicContext.strokes = [] + + memo.canvas = canvas + canvas.memo = memo + canvas.graphicContext = graphicContext + graphicContext.canvas = canvas + try managedObjectContext.save() openMemo(for: memo) } catch { - NSLog("[SketchNote] - \(error.localizedDescription)") + NSLog("[Memola] - \(error.localizedDescription)") } } func openMemo(for memo: Memo) { - do { - let data = memo.data - let canvas = try JSONDecoder().decode(Canvas.self, from: data) - canvas.memo = memo - self.canvas = canvas - } catch { - NSLog("[SketchNote] - \(error.localizedDescription)") - } + self.memo = memo } } diff --git a/Memola/Persistence/Core/Persistence.swift b/Memola/Persistence/Core/Persistence.swift index 4dee7f9..505a41f 100644 --- a/Memola/Persistence/Core/Persistence.swift +++ b/Memola/Persistence/Core/Persistence.swift @@ -13,12 +13,14 @@ class Persistence { static let shared: Persistence = Persistence() - private init() { } - - var viewContext: NSManagedObjectContext { - persistentContainer.viewContext + private init() { + QuadValueTransformer.register() } + lazy var viewContext: NSManagedObjectContext = { + persistentContainer.viewContext + }() + lazy var persistentContainer: NSPersistentContainer = { let persistentStore = NSPersistentStoreDescription() persistentStore.shouldMigrateStoreAutomatically = true diff --git a/Memola/Persistence/Transformers/QuadValueTransformer.swift b/Memola/Persistence/Transformers/QuadValueTransformer.swift new file mode 100644 index 0000000..f03c9f3 --- /dev/null +++ b/Memola/Persistence/Transformers/QuadValueTransformer.swift @@ -0,0 +1,53 @@ +// +// QuadValueTransformer.swift +// Memola +// +// Created by Dscyre Scotti on 5/8/24. +// + +import CoreData +import Foundation + +@objc(QuadValueTransformer) +class QuadValueTransformer: ValueTransformer { + static let name = NSValueTransformerName(rawValue: String(describing: QuadValueTransformer.self)) + + override class func transformedValueClass() -> AnyClass { + StrokeQuad.self + } + + override func transformedValue(_ value: Any?) -> Any? { + guard let quads = value as? [StrokeQuad] else { + assertionFailure("[Memola] - Failed to transform `[Quad]` to `Data`") + return nil + } + do { + let data = try JSONEncoder().encode(quads) + return data + } catch { + print(error.localizedDescription) + assertionFailure("[Memola] - Failed to transform `Quad` to `Data`") + return nil + } + } + + override func reverseTransformedValue(_ value: Any?) -> Any? { + guard let data = value as? Data else { + assertionFailure("[Memola] - Failed to transform `Data` to `Quad`") + return nil + } + do { + let quads = try JSONDecoder().decode([StrokeQuad].self, from: data) + return quads + } catch { + print(error.localizedDescription) + assertionFailure("[Memola] - Failed to transform `Data` to `Quad`") + return nil + } + } + + static func register() { + let transformer = QuadValueTransformer() + ValueTransformer.setValueTransformer(transformer, forName: name) + } +} diff --git a/Memola/Resources/Models/MemolaModel.xcdatamodeld/MemolaModel.xcdatamodel/contents b/Memola/Resources/Models/MemolaModel.xcdatamodeld/MemolaModel.xcdatamodel/contents index bc23036..c2bf94d 100644 --- a/Memola/Resources/Models/MemolaModel.xcdatamodeld/MemolaModel.xcdatamodel/contents +++ b/Memola/Resources/Models/MemolaModel.xcdatamodeld/MemolaModel.xcdatamodel/contents @@ -1,10 +1,31 @@ - + + + + + + + + + + + + + - + + + + + + + + + + \ No newline at end of file From 0fc277ca5bfa4e03201aa1cd9f81b6a152ea27b7 Mon Sep 17 00:00:00 2001 From: dscyrescotti Date: Wed, 8 May 2024 23:18:31 +0700 Subject: [PATCH 02/11] feat: update undo and redo --- Memola/Canvas/Contexts/GraphicContext.swift | 23 +++++++++++++++++-- Memola/Features/Memo/MemoView.swift | 1 + .../MemolaModel.xcdatamodel/contents | 2 +- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/Memola/Canvas/Contexts/GraphicContext.swift b/Memola/Canvas/Contexts/GraphicContext.swift index 34486a0..0ace6d9 100644 --- a/Memola/Canvas/Contexts/GraphicContext.swift +++ b/Memola/Canvas/Contexts/GraphicContext.swift @@ -42,15 +42,27 @@ class GraphicContext: NSManagedObject { func undoGraphic() { guard let stroke = strokes.lastObject as? Stroke else { return } strokes.remove(stroke) + stroke.graphicContext = nil previousStroke = nil + do { + try Persistence.shared.viewContext.save() + } catch { + NSLog("[Memola] - \(error.localizedDescription)") + } } func redoGraphic(for event: HistoryEvent) { switch event { case .stroke(let stroke): strokes.add(stroke) + stroke.graphicContext = self previousStroke = nil } + do { + try Persistence.shared.viewContext.save() + } catch { + NSLog("[Memola] - \(error.localizedDescription)") + } } } @@ -109,8 +121,15 @@ extension GraphicContext { } func cancelStroke() { - if let stroke = strokes.lastObject { - strokes.remove(stroke) + if let stroke = strokes.lastObject as? Stroke { + do { + let viewContext = Persistence.shared.viewContext + strokes.remove(stroke) + viewContext.delete(stroke) + try viewContext.save() + } catch { + NSLog("[Memola] - \(error.localizedDescription)") + } } currentStroke = nil currentPoint = nil diff --git a/Memola/Features/Memo/MemoView.swift b/Memola/Features/Memo/MemoView.swift index 876f4e7..b933020 100644 --- a/Memola/Features/Memo/MemoView.swift +++ b/Memola/Features/Memo/MemoView.swift @@ -88,6 +88,7 @@ struct MemoView: View { } func closeMemo() { + history.resetRedo() if managedObjectContext.hasChanges { do { try managedObjectContext.save() diff --git a/Memola/Resources/Models/MemolaModel.xcdatamodeld/MemolaModel.xcdatamodel/contents b/Memola/Resources/Models/MemolaModel.xcdatamodeld/MemolaModel.xcdatamodel/contents index c2bf94d..0592f91 100644 --- a/Memola/Resources/Models/MemolaModel.xcdatamodeld/MemolaModel.xcdatamodel/contents +++ b/Memola/Resources/Models/MemolaModel.xcdatamodeld/MemolaModel.xcdatamodel/contents @@ -23,7 +23,7 @@ - + From cd6048bbd0b73250f56a8bba366fbce112fbe67c Mon Sep 17 00:00:00 2001 From: dscyrescotti Date: Thu, 9 May 2024 08:28:16 +0700 Subject: [PATCH 03/11] feat: modify entities --- Memola/Canvas/Contexts/GraphicContext.swift | 2 +- Memola/Canvas/Core/Canvas.swift | 7 ++++--- Memola/Features/Memos/MemosView.swift | 3 +++ Memola/Persistence/Transformers/QuadValueTransformer.swift | 6 +++--- .../MemolaModel.xcdatamodel/contents | 2 +- 5 files changed, 12 insertions(+), 8 deletions(-) diff --git a/Memola/Canvas/Contexts/GraphicContext.swift b/Memola/Canvas/Contexts/GraphicContext.swift index 0ace6d9..9e5f594 100644 --- a/Memola/Canvas/Contexts/GraphicContext.swift +++ b/Memola/Canvas/Contexts/GraphicContext.swift @@ -13,7 +13,7 @@ import Foundation @objc(GraphicContext) class GraphicContext: NSManagedObject { @NSManaged var id: UUID - @NSManaged var canvas: Canvas + @NSManaged var canvas: Canvas? @NSManaged var strokes: NSMutableOrderedSet var currentStroke: Stroke? diff --git a/Memola/Canvas/Core/Canvas.swift b/Memola/Canvas/Core/Canvas.swift index 4f93c28..05a845a 100644 --- a/Memola/Canvas/Core/Canvas.swift +++ b/Memola/Canvas/Core/Canvas.swift @@ -15,7 +15,7 @@ class Canvas: NSManagedObject, Identifiable { @NSManaged var id: UUID @NSManaged var width: CGFloat @NSManaged var height: CGFloat - @NSManaged var memo: Memo + @NSManaged var memo: Memo? @NSManaged var graphicContext: GraphicContext let gridContext = GridContext() @@ -43,8 +43,9 @@ class Canvas: NSManagedObject, Identifiable { extension Canvas { func load() { state = .loading - graphicContext.strokes.forEach { - ($0 as? Stroke)?.loadVertices() + graphicContext.strokes.forEach { stroke in + guard let stroke = stroke as? Stroke else { return } + stroke.loadVertices() } state = .loaded } diff --git a/Memola/Features/Memos/MemosView.swift b/Memola/Features/Memos/MemosView.swift index c52813a..4694446 100644 --- a/Memola/Features/Memos/MemosView.swift +++ b/Memola/Features/Memos/MemosView.swift @@ -31,6 +31,9 @@ struct MemosView: View { } .fullScreenCover(item: $memo) { memo in MemoView(canvas: memo.canvas) + .onDisappear { + managedObjectContext.refreshAllObjects() + } } } diff --git a/Memola/Persistence/Transformers/QuadValueTransformer.swift b/Memola/Persistence/Transformers/QuadValueTransformer.swift index f03c9f3..63b03b3 100644 --- a/Memola/Persistence/Transformers/QuadValueTransformer.swift +++ b/Memola/Persistence/Transformers/QuadValueTransformer.swift @@ -22,7 +22,7 @@ class QuadValueTransformer: ValueTransformer { return nil } do { - let data = try JSONEncoder().encode(quads) + let data = try JSONEncoder().encode(quads.map(\.quad)) return data } catch { print(error.localizedDescription) @@ -37,8 +37,8 @@ class QuadValueTransformer: ValueTransformer { return nil } do { - let quads = try JSONDecoder().decode([StrokeQuad].self, from: data) - return quads + let quads = try JSONDecoder().decode([Quad].self, from: data) + return quads.map(StrokeQuad.init) } catch { print(error.localizedDescription) assertionFailure("[Memola] - Failed to transform `Data` to `Quad`") diff --git a/Memola/Resources/Models/MemolaModel.xcdatamodeld/MemolaModel.xcdatamodel/contents b/Memola/Resources/Models/MemolaModel.xcdatamodeld/MemolaModel.xcdatamodel/contents index 0592f91..89c2561 100644 --- a/Memola/Resources/Models/MemolaModel.xcdatamodeld/MemolaModel.xcdatamodel/contents +++ b/Memola/Resources/Models/MemolaModel.xcdatamodeld/MemolaModel.xcdatamodel/contents @@ -4,7 +4,7 @@ - + From 1d91da844564ae0eb196c95f88da79b86b5f8323 Mon Sep 17 00:00:00 2001 From: dscyrescotti Date: Thu, 9 May 2024 14:30:46 +0700 Subject: [PATCH 04/11] feat: enhance vertex loading --- Memola/Canvas/Contexts/GraphicContext.swift | 2 +- Memola/Canvas/Core/Canvas.swift | 22 ++++++++++++++----- .../Canvas/Geometries/Primitives/Quad.swift | 2 +- .../Geometries/Primitives/StrokeQuad.swift | 2 +- .../SolidPointStrokeGenerator.swift | 4 ++-- Memola/Canvas/Geometries/Stroke/Stroke.swift | 5 +++-- .../ViewController/CanvasViewController.swift | 5 +++++ Memola/Features/Memo/Memo.swift | 2 +- Memola/Features/Memos/MemosView.swift | 3 --- 9 files changed, 31 insertions(+), 16 deletions(-) diff --git a/Memola/Canvas/Contexts/GraphicContext.swift b/Memola/Canvas/Contexts/GraphicContext.swift index 9e5f594..ce2b2ef 100644 --- a/Memola/Canvas/Contexts/GraphicContext.swift +++ b/Memola/Canvas/Contexts/GraphicContext.swift @@ -11,7 +11,7 @@ import CoreData import Foundation @objc(GraphicContext) -class GraphicContext: NSManagedObject { +final class GraphicContext: NSManagedObject { @NSManaged var id: UUID @NSManaged var canvas: Canvas? @NSManaged var strokes: NSMutableOrderedSet diff --git a/Memola/Canvas/Core/Canvas.swift b/Memola/Canvas/Core/Canvas.swift index 05a845a..82dff1e 100644 --- a/Memola/Canvas/Core/Canvas.swift +++ b/Memola/Canvas/Core/Canvas.swift @@ -11,7 +11,7 @@ import MetalKit import Foundation @objc(Canvas) -class Canvas: NSManagedObject, Identifiable { +final class Canvas: NSManagedObject, Identifiable { @NSManaged var id: UUID @NSManaged var width: CGFloat @NSManaged var height: CGFloat @@ -43,11 +43,23 @@ class Canvas: NSManagedObject, Identifiable { extension Canvas { func load() { state = .loading - graphicContext.strokes.forEach { stroke in - guard let stroke = stroke as? Stroke else { return } - stroke.loadVertices() + let start = Date().formatted(.dateTime.minute().second().secondFraction(.fractional(5))) + Task(priority: .high) { [start] in + await withTaskGroup(of: Void.self) { taskGroup in + for stroke in graphicContext.strokes { + guard let stroke = stroke as? Stroke else { continue } + taskGroup.addTask { + stroke.loadVertices() + } + } + } + + let end = Date().formatted(.dateTime.minute().second().secondFraction(.fractional(5))) + NSLog("[Memola] - Loaded from \(start) to \(end)") + await MainActor.run { + state = .loaded + } } - state = .loaded } } diff --git a/Memola/Canvas/Geometries/Primitives/Quad.swift b/Memola/Canvas/Geometries/Primitives/Quad.swift index 5e8eda4..9f46e04 100644 --- a/Memola/Canvas/Geometries/Primitives/Quad.swift +++ b/Memola/Canvas/Geometries/Primitives/Quad.swift @@ -22,7 +22,7 @@ struct Quad: Codable { self.shape = shape } - func generateVertices(_ shape: QuadShape) -> [QuadVertex] { + func generateVertices() -> [QuadVertex] { switch shape { case .rounded: generateRoundedQuad() diff --git a/Memola/Canvas/Geometries/Primitives/StrokeQuad.swift b/Memola/Canvas/Geometries/Primitives/StrokeQuad.swift index f07d4e4..23eae5b 100644 --- a/Memola/Canvas/Geometries/Primitives/StrokeQuad.swift +++ b/Memola/Canvas/Geometries/Primitives/StrokeQuad.swift @@ -8,7 +8,7 @@ import MetalKit import Foundation -class StrokeQuad: NSObject, Codable { +final class StrokeQuad: NSObject, Codable { var quad: Quad init(quad: Quad) { diff --git a/Memola/Canvas/Geometries/Stroke/Generators/SolidPointStrokeGenerator.swift b/Memola/Canvas/Geometries/Stroke/Generators/SolidPointStrokeGenerator.swift index af7b511..f2fde69 100644 --- a/Memola/Canvas/Geometries/Stroke/Generators/SolidPointStrokeGenerator.swift +++ b/Memola/Canvas/Geometries/Stroke/Generators/SolidPointStrokeGenerator.swift @@ -107,7 +107,7 @@ struct SolidPointStrokeGenerator: StrokeGenerator { } let quad = Quad(origin: point, size: stroke.thickness, color: stroke.color, rotation: rotation) stroke._quads.append(quad) - stroke.vertices.append(contentsOf: quad.generateVertices(quad.shape)) + stroke.vertices.append(contentsOf: quad.generateVertices()) stroke.vertexCount = stroke.vertices.endIndex } @@ -116,7 +116,7 @@ struct SolidPointStrokeGenerator: StrokeGenerator { let factor: CGFloat switch configuration.granularity { case .automatic: - factor = min(3.5, 1 / (stroke.thickness * 10 / 300)) + factor = min(3.5, 1 / (stroke.thickness * 1 / 10)) case .fixed: factor = 1 / (stroke.thickness * stroke.penStyle.anyPenStyle.stepRate) case .none: diff --git a/Memola/Canvas/Geometries/Stroke/Stroke.swift b/Memola/Canvas/Geometries/Stroke/Stroke.swift index 688a336..7f2eb73 100644 --- a/Memola/Canvas/Geometries/Stroke/Stroke.swift +++ b/Memola/Canvas/Geometries/Stroke/Stroke.swift @@ -10,7 +10,7 @@ import CoreData import Foundation @objc(Stroke) -class Stroke: NSManagedObject { +final class Stroke: NSManagedObject { @NSManaged var id: UUID @NSManaged var color: [CGFloat] @NSManaged var style: Int16 @@ -56,11 +56,12 @@ class Stroke: NSManagedObject { func finish(at point: CGPoint) { penStyle.anyPenStyle.generator.finish(at: point, on: self) keyPoints.removeAll() + NSLog("[Memola] - \(_quads.count) quads") } func loadVertices() { vertices = strokeQuads - .flatMap { $0.quad.generateVertices($0.quad.shape) } + .flatMap { $0.quad.generateVertices() } vertexCount = vertices.endIndex } diff --git a/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift b/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift index 5a3907a..117d49b 100644 --- a/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift +++ b/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift @@ -58,6 +58,11 @@ class CanvasViewController: UIViewController { renderView.draw() drawingView.enableUserInteraction() } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + Persistence.shared.viewContext.refresh(canvas, mergeChanges: false) + } } extension CanvasViewController { diff --git a/Memola/Features/Memo/Memo.swift b/Memola/Features/Memo/Memo.swift index 31855a2..5194b38 100644 --- a/Memola/Features/Memo/Memo.swift +++ b/Memola/Features/Memo/Memo.swift @@ -9,7 +9,7 @@ import CoreData import Foundation @objc(Memo) -class Memo: NSManagedObject { +final class Memo: NSManagedObject { @NSManaged var id: UUID @NSManaged var title: String @NSManaged var data: Data diff --git a/Memola/Features/Memos/MemosView.swift b/Memola/Features/Memos/MemosView.swift index 4694446..c52813a 100644 --- a/Memola/Features/Memos/MemosView.swift +++ b/Memola/Features/Memos/MemosView.swift @@ -31,9 +31,6 @@ struct MemosView: View { } .fullScreenCover(item: $memo) { memo in MemoView(canvas: memo.canvas) - .onDisappear { - managedObjectContext.refreshAllObjects() - } } } From 335d7e52e9d1177b03e933fbf8c33f9f9377650f Mon Sep 17 00:00:00 2001 From: dscyrescotti Date: Thu, 9 May 2024 22:44:58 +0700 Subject: [PATCH 05/11] feat: update stroke generator factor --- .../Stroke/Generators/SolidPointStrokeGenerator.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Memola/Canvas/Geometries/Stroke/Generators/SolidPointStrokeGenerator.swift b/Memola/Canvas/Geometries/Stroke/Generators/SolidPointStrokeGenerator.swift index f2fde69..d1a31a7 100644 --- a/Memola/Canvas/Geometries/Stroke/Generators/SolidPointStrokeGenerator.swift +++ b/Memola/Canvas/Geometries/Stroke/Generators/SolidPointStrokeGenerator.swift @@ -116,7 +116,7 @@ struct SolidPointStrokeGenerator: StrokeGenerator { let factor: CGFloat switch configuration.granularity { case .automatic: - factor = min(3.5, 1 / (stroke.thickness * 1 / 10)) + factor = min(5, 1 / (stroke.thickness * 10 / 500)) case .fixed: factor = 1 / (stroke.thickness * stroke.penStyle.anyPenStyle.stepRate) case .none: From 5109cc53fc2c249aec535b4aa77fecb6db211734 Mon Sep 17 00:00:00 2001 From: dscyrescotti Date: Thu, 9 May 2024 23:22:16 +0700 Subject: [PATCH 06/11] refactor: clean up --- Memola/Canvas/Core/Canvas.swift | 7 +++++-- Memola/Canvas/Geometries/Stroke/Stroke.swift | 1 - Memola/Canvas/View/CanvasView.swift | 2 +- Memola/Features/Memo/MemoView.swift | 4 ++-- Memola/Features/Memos/MemosView.swift | 3 ++- 5 files changed, 10 insertions(+), 7 deletions(-) diff --git a/Memola/Canvas/Core/Canvas.swift b/Memola/Canvas/Core/Canvas.swift index 82dff1e..ba73052 100644 --- a/Memola/Canvas/Core/Canvas.swift +++ b/Memola/Canvas/Core/Canvas.swift @@ -42,9 +42,12 @@ final class Canvas: NSManagedObject, Identifiable { // MARK: - Actions extension Canvas { func load() { - state = .loading let start = Date().formatted(.dateTime.minute().second().secondFraction(.fractional(5))) Task(priority: .high) { [start] in + await MainActor.run { + state = .loading + objectWillChange.send() + } await withTaskGroup(of: Void.self) { taskGroup in for stroke in graphicContext.strokes { guard let stroke = stroke as? Stroke else { continue } @@ -53,11 +56,11 @@ extension Canvas { } } } - let end = Date().formatted(.dateTime.minute().second().secondFraction(.fractional(5))) NSLog("[Memola] - Loaded from \(start) to \(end)") await MainActor.run { state = .loaded + objectWillChange.send() } } } diff --git a/Memola/Canvas/Geometries/Stroke/Stroke.swift b/Memola/Canvas/Geometries/Stroke/Stroke.swift index 7f2eb73..0ab49fb 100644 --- a/Memola/Canvas/Geometries/Stroke/Stroke.swift +++ b/Memola/Canvas/Geometries/Stroke/Stroke.swift @@ -56,7 +56,6 @@ final class Stroke: NSManagedObject { func finish(at point: CGPoint) { penStyle.anyPenStyle.generator.finish(at: point, on: self) keyPoints.removeAll() - NSLog("[Memola] - \(_quads.count) quads") } func loadVertices() { diff --git a/Memola/Canvas/View/CanvasView.swift b/Memola/Canvas/View/CanvasView.swift index 5bc9ab9..3842c6d 100644 --- a/Memola/Canvas/View/CanvasView.swift +++ b/Memola/Canvas/View/CanvasView.swift @@ -8,8 +8,8 @@ import SwiftUI struct CanvasView: UIViewControllerRepresentable { - let canvas: Canvas @EnvironmentObject var tool: Tool + @EnvironmentObject var canvas: Canvas @EnvironmentObject var history: History func makeUIViewController(context: Context) -> CanvasViewController { diff --git a/Memola/Features/Memo/MemoView.swift b/Memola/Features/Memo/MemoView.swift index b933020..0673109 100644 --- a/Memola/Features/Memo/MemoView.swift +++ b/Memola/Features/Memo/MemoView.swift @@ -14,10 +14,10 @@ struct MemoView: View { @StateObject var tool = Tool() @StateObject var history = History() - let canvas: Canvas + @EnvironmentObject var canvas: Canvas var body: some View { - CanvasView(canvas: canvas) + CanvasView() .ignoresSafeArea() .overlay(alignment: .bottomTrailing) { PenToolView() diff --git a/Memola/Features/Memos/MemosView.swift b/Memola/Features/Memos/MemosView.swift index c52813a..c0972d9 100644 --- a/Memola/Features/Memos/MemosView.swift +++ b/Memola/Features/Memos/MemosView.swift @@ -30,7 +30,8 @@ struct MemosView: View { } } .fullScreenCover(item: $memo) { memo in - MemoView(canvas: memo.canvas) + MemoView() + .environmentObject(memo.canvas) } } From 74297b762787643d0b7ad6ccc6a2031ba4f12ab1 Mon Sep 17 00:00:00 2001 From: dscyrescotti Date: Fri, 10 May 2024 18:15:30 +0700 Subject: [PATCH 07/11] feat: add quad entity instead of storing in array --- Memola.xcodeproj/project.pbxproj | 20 +---- Memola/App/MemolaApp.swift | 2 +- Memola/Canvas/Contexts/GraphicContext.swift | 32 ++----- Memola/Canvas/Core/Canvas.swift | 21 ++--- .../Canvas/Geometries/Primitives/Quad.swift | 87 ++++++------------- .../Geometries/Primitives/QuadShape.swift | 13 +++ .../Geometries/Primitives/StrokeQuad.swift | 18 ---- .../SolidPointStrokeGenerator.swift | 31 +++++-- Memola/Canvas/Geometries/Stroke/Stroke.swift | 25 ++++-- Memola/Canvas/History/History.swift | 9 ++ .../ViewController/CanvasViewController.swift | 5 +- Memola/Persistence/Core/Persistence.swift | 24 ++++- .../Transformers/QuadValueTransformer.swift | 53 ----------- .../MemolaModel.xcdatamodel/contents | 11 ++- 14 files changed, 142 insertions(+), 209 deletions(-) create mode 100644 Memola/Canvas/Geometries/Primitives/QuadShape.swift delete mode 100644 Memola/Canvas/Geometries/Primitives/StrokeQuad.swift delete mode 100644 Memola/Persistence/Transformers/QuadValueTransformer.swift diff --git a/Memola.xcodeproj/project.pbxproj b/Memola.xcodeproj/project.pbxproj index a060075..384123b 100644 --- a/Memola.xcodeproj/project.pbxproj +++ b/Memola.xcodeproj/project.pbxproj @@ -8,7 +8,6 @@ /* Begin PBXBuildFile section */ EC4538892BEBCAE000A86FEC /* Quad.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC4538882BEBCAE000A86FEC /* Quad.swift */; }; - EC771E602BEB6EE50053CC68 /* QuadValueTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC771E5F2BEB6EE50053CC68 /* QuadValueTransformer.swift */; }; 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 */; }; @@ -63,16 +62,15 @@ ECA738F22BE6128F00A4542E /* Collection++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738F12BE6128F00A4542E /* Collection++.swift */; }; ECA738F42BE612A000A4542E /* Array++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738F32BE612A000A4542E /* Array++.swift */; }; ECA738F62BE612B700A4542E /* MTLDevice++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738F52BE612B700A4542E /* MTLDevice++.swift */; }; - ECA738F82BE612EB00A4542E /* StrokeQuad.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738F72BE612EB00A4542E /* StrokeQuad.swift */; }; ECA738FC2BE61C5200A4542E /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738FB2BE61C5200A4542E /* Persistence.swift */; }; ECA739012BE61D9C00A4542E /* MemolaModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = ECA738FF2BE61D9C00A4542E /* MemolaModel.xcdatamodeld */; }; ECA739052BE61E3100A4542E /* Memo.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA739042BE61E3100A4542E /* Memo.swift */; }; ECA739082BE623F300A4542E /* PenToolView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA739072BE623F300A4542E /* PenToolView.swift */; }; + ECEC01A82BEE11BA006DA24C /* QuadShape.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECEC01A72BEE11BA006DA24C /* QuadShape.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ EC4538882BEBCAE000A86FEC /* Quad.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Quad.swift; sourceTree = ""; }; - EC771E5F2BEB6EE50053CC68 /* QuadValueTransformer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuadValueTransformer.swift; sourceTree = ""; }; EC7F6BE82BE5E6E300A34A7B /* Memola.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Memola.app; sourceTree = BUILT_PRODUCTS_DIR; }; EC7F6BEB2BE5E6E300A34A7B /* MemolaApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemolaApp.swift; sourceTree = ""; }; EC7F6BEF2BE5E6E400A34A7B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -128,11 +126,11 @@ ECA738F12BE6128F00A4542E /* Collection++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection++.swift"; sourceTree = ""; }; ECA738F32BE612A000A4542E /* Array++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array++.swift"; sourceTree = ""; }; ECA738F52BE612B700A4542E /* MTLDevice++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MTLDevice++.swift"; sourceTree = ""; }; - ECA738F72BE612EB00A4542E /* StrokeQuad.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StrokeQuad.swift; sourceTree = ""; }; ECA738FB2BE61C5200A4542E /* Persistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = ""; }; ECA739002BE61D9C00A4542E /* MemolaModel.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MemolaModel.xcdatamodel; sourceTree = ""; }; ECA739042BE61E3100A4542E /* Memo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Memo.swift; sourceTree = ""; }; ECA739072BE623F300A4542E /* PenToolView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PenToolView.swift; sourceTree = ""; }; + ECEC01A72BEE11BA006DA24C /* QuadShape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuadShape.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -162,14 +160,6 @@ path = ViewController; sourceTree = ""; }; - EC771E5C2BEB37FC0053CC68 /* Transformers */ = { - isa = PBXGroup; - children = ( - EC771E5F2BEB6EE50053CC68 /* QuadValueTransformer.swift */, - ); - path = Transformers; - sourceTree = ""; - }; EC7F6BDF2BE5E6E300A34A7B = { isa = PBXGroup; children = ( @@ -304,8 +294,8 @@ ECA738982BE6015700A4542E /* Primitives */ = { isa = PBXGroup; children = ( - ECA738F72BE612EB00A4542E /* StrokeQuad.swift */, EC4538882BEBCAE000A86FEC /* Quad.swift */, + ECEC01A72BEE11BA006DA24C /* QuadShape.swift */, ); path = Primitives; sourceTree = ""; @@ -463,7 +453,6 @@ ECA738FA2BE61B1700A4542E /* Persistence */ = { isa = PBXGroup; children = ( - EC771E5C2BEB37FC0053CC68 /* Transformers */, ECA739022BE61DE700A4542E /* Core */, ); path = Persistence; @@ -572,7 +561,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - EC771E602BEB6EE50053CC68 /* QuadValueTransformer.swift in Sources */, ECA738B02BE60D0B00A4542E /* CanvasViewController.swift in Sources */, ECA738E42BE6110800A4542E /* Drawable.swift in Sources */, ECA738AD2BE60CC600A4542E /* DrawingView.swift in Sources */, @@ -594,6 +582,7 @@ ECA738A62BE6023F00A4542E /* GridUniforms.swift in Sources */, ECA738D72BE60FC100A4542E /* SolidPointStrokeGenerator.swift in Sources */, ECA738832BE5FEFE00A4542E /* RenderPass.swift in Sources */, + ECEC01A82BEE11BA006DA24C /* QuadShape.swift in Sources */, ECA738862BE5FF2500A4542E /* Canvas.swift in Sources */, ECA738882BE5FF4400A4542E /* Renderer.swift in Sources */, ECA738D42BE60F9100A4542E /* StrokeGenerator.swift in Sources */, @@ -628,7 +617,6 @@ EC7F6BEC2BE5E6E300A34A7B /* MemolaApp.swift in Sources */, ECA738A02BE601E400A4542E /* ViewPortVertex.swift in Sources */, ECA738BC2BE60E0300A4542E /* Tool.swift in Sources */, - ECA738F82BE612EB00A4542E /* StrokeQuad.swift in Sources */, ECA738972BE6014200A4542E /* Graphic.metal in Sources */, ECA7388A2BE6006A00A4542E /* PipelineStates.swift in Sources */, ); diff --git a/Memola/App/MemolaApp.swift b/Memola/App/MemolaApp.swift index 119dd03..9f81d25 100644 --- a/Memola/App/MemolaApp.swift +++ b/Memola/App/MemolaApp.swift @@ -12,7 +12,7 @@ struct MemolaApp: App { var body: some Scene { WindowGroup { MemosView() - .environment(\.managedObjectContext, Persistence.shared.viewContext) + .environment(\.managedObjectContext, Persistence.context) } } } diff --git a/Memola/Canvas/Contexts/GraphicContext.swift b/Memola/Canvas/Contexts/GraphicContext.swift index ce2b2ef..6775eb7 100644 --- a/Memola/Canvas/Contexts/GraphicContext.swift +++ b/Memola/Canvas/Contexts/GraphicContext.swift @@ -44,11 +44,7 @@ final class GraphicContext: NSManagedObject { strokes.remove(stroke) stroke.graphicContext = nil previousStroke = nil - do { - try Persistence.shared.viewContext.save() - } catch { - NSLog("[Memola] - \(error.localizedDescription)") - } + Persistence.saveIfNeeded() } func redoGraphic(for event: HistoryEvent) { @@ -58,11 +54,7 @@ final class GraphicContext: NSManagedObject { stroke.graphicContext = self previousStroke = nil } - do { - try Persistence.shared.viewContext.save() - } catch { - NSLog("[Memola] - \(error.localizedDescription)") - } + Persistence.saveIfNeeded() } } @@ -84,13 +76,13 @@ extension GraphicContext: Drawable { extension GraphicContext { func beginStroke(at point: CGPoint, pen: Pen) -> Stroke { - let stroke = Stroke(context: Persistence.shared.viewContext) + let stroke = Stroke(context: Persistence.context) stroke.id = UUID() stroke.color = pen.color stroke.style = pen.strokeStyle.rawValue stroke.thickness = pen.thickness stroke.createdAt = .now - stroke.strokeQuads = [] + stroke.quads = [] stroke.graphicContext = self strokes.add(stroke) currentStroke = stroke @@ -109,12 +101,7 @@ extension GraphicContext { func endStroke(at point: CGPoint) { guard currentPoint != nil, let currentStroke else { return } currentStroke.finish(at: point) - currentStroke.saveQuads() - do { - try Persistence.shared.viewContext.save() - } catch { - NSLog("[Memola] - \(error.localizedDescription)") - } + Persistence.saveIfNeeded() previousStroke = currentStroke self.currentStroke = nil self.currentPoint = nil @@ -122,14 +109,11 @@ extension GraphicContext { func cancelStroke() { if let stroke = strokes.lastObject as? Stroke { - do { - let viewContext = Persistence.shared.viewContext + Persistence.performe { context in strokes.remove(stroke) - viewContext.delete(stroke) - try viewContext.save() - } catch { - NSLog("[Memola] - \(error.localizedDescription)") + context.delete(stroke) } + Persistence.saveIfNeeded() } currentStroke = nil currentPoint = nil diff --git a/Memola/Canvas/Core/Canvas.swift b/Memola/Canvas/Core/Canvas.swift index 82dff1e..2c555e3 100644 --- a/Memola/Canvas/Core/Canvas.swift +++ b/Memola/Canvas/Core/Canvas.swift @@ -44,22 +44,15 @@ extension Canvas { func load() { state = .loading let start = Date().formatted(.dateTime.minute().second().secondFraction(.fractional(5))) - Task(priority: .high) { [start] in - await withTaskGroup(of: Void.self) { taskGroup in - for stroke in graphicContext.strokes { - guard let stroke = stroke as? Stroke else { continue } - taskGroup.addTask { - stroke.loadVertices() - } - } - } - - let end = Date().formatted(.dateTime.minute().second().secondFraction(.fractional(5))) - NSLog("[Memola] - Loaded from \(start) to \(end)") - await MainActor.run { - state = .loaded + for stroke in graphicContext.strokes { + if let stroke = stroke as? Stroke { + stroke.loadVertices() + NSLog("[Memola] - \(stroke.quads.count) quads") } } + let end = Date().formatted(.dateTime.minute().second().secondFraction(.fractional(5))) + NSLog("[Memola] - Loaded from \(start) to \(end)") + state = .loaded } } diff --git a/Memola/Canvas/Geometries/Primitives/Quad.swift b/Memola/Canvas/Geometries/Primitives/Quad.swift index 9f46e04..f5f5742 100644 --- a/Memola/Canvas/Geometries/Primitives/Quad.swift +++ b/Memola/Canvas/Geometries/Primitives/Quad.swift @@ -5,37 +5,38 @@ // Created by Dscyre Scotti on 5/8/24. // +import CoreData import Foundation -struct Quad: Codable { - var origin: CGPoint - var color: [CGFloat] - var size: CGFloat - var rotation: CGFloat - var shape: QuadShape +@objc(Quad) +class Quad: NSManagedObject { + @NSManaged var id: UUID + @NSManaged var originX: CGFloat + @NSManaged var originY: CGFloat + @NSManaged var size: CGFloat + @NSManaged var rotation: CGFloat + @NSManaged var shape: Int16 + @NSManaged var stroke: Stroke? - init(origin: CGPoint, size: CGFloat, color: [CGFloat], rotation: CGFloat, shape: QuadShape = .rounded) { - self.origin = origin - self.size = size - self.color = color - self.rotation = rotation - self.shape = shape - } - - func generateVertices() -> [QuadVertex] { - switch shape { - case .rounded: - generateRoundedQuad() - case .squared: - generateSquaredQuad() - case let .calligraphic(vFactor, hFactor): - generateCalligraphicQuad(vFactor: vFactor, hFactor: hFactor) - case let .trapezoid(topFactor, bottomFactor, heightFactor): - generateTrapezoidQuad(topFactor: topFactor, bottomFactor: bottomFactor, heightFactor: heightFactor) + var origin: CGPoint { + get { CGPoint(x: originX, y: originY) } + set { + originX = newValue.x + originY = newValue.y } } - func generateRoundedQuad() -> [QuadVertex] { + func generateVertices(_ color: [CGFloat]) -> [QuadVertex] { + guard let shape = QuadShape.init(rawValue: shape) else { return [] } + switch shape { + case .rounded: + return generateRoundedQuad(color) + case .squared: + return generateSquaredQuad(color) + } + } + + func generateRoundedQuad(_ color: [CGFloat]) -> [QuadVertex] { let halfSize = size * 0.5 return [ QuadVertex(x: origin.x - halfSize, y: origin.y - halfSize, textCoord: CGPoint(x: 0, y: 0), color: color, origin: origin, rotation: rotation), @@ -47,7 +48,7 @@ struct Quad: Codable { ] } - func generateSquaredQuad() -> [QuadVertex] { + func generateSquaredQuad(_ color: [CGFloat]) -> [QuadVertex] { let vHalfSize = size * 0.5 let hHalfSize = size * 0.15 return [ @@ -59,38 +60,4 @@ struct Quad: Codable { QuadVertex(x: origin.x + hHalfSize, y: origin.y + vHalfSize, textCoord: CGPoint(x: 1, y: 1), color: color, origin: origin, rotation: rotation) ] } - - func generateCalligraphicQuad(vFactor: CGFloat, hFactor: CGFloat) -> [QuadVertex] { - let vHalfSize = size * vFactor * 0.5 - let hHalfSize = size * hFactor * 0.5 - return [ - QuadVertex(x: origin.x - hHalfSize, y: origin.y - vHalfSize, textCoord: CGPoint(x: 0, y: 0), color: color, origin: origin, rotation: rotation), - QuadVertex(x: origin.x + hHalfSize, y: origin.y - vHalfSize, textCoord: CGPoint(x: 1, y: 0), color: color, origin: origin, rotation: rotation), - QuadVertex(x: origin.x - hHalfSize, y: origin.y + vHalfSize, textCoord: CGPoint(x: 0, y: 1), color: color, origin: origin, rotation: rotation), - QuadVertex(x: origin.x + hHalfSize, y: origin.y - vHalfSize, textCoord: CGPoint(x: 1, y: 0), color: color, origin: origin, rotation: rotation), - QuadVertex(x: origin.x - hHalfSize, y: origin.y + vHalfSize, textCoord: CGPoint(x: 0, y: 1), color: color, origin: origin, rotation: rotation), - QuadVertex(x: origin.x + hHalfSize, y: origin.y + vHalfSize, textCoord: CGPoint(x: 1, y: 1), color: color, origin: origin, rotation: rotation) - ] - } - - func generateTrapezoidQuad(topFactor: CGFloat, bottomFactor: CGFloat, heightFactor: CGFloat) -> [QuadVertex] { - let vHalfSize = size * heightFactor * 0.5 - let hTopHalfSize = size * topFactor * 0.5 - let hBottomHalfSize = size * bottomFactor * 0.5 - return [ - QuadVertex(x: origin.x - hTopHalfSize, y: origin.y - vHalfSize, textCoord: CGPoint(x: 0, y: 0), color: color, origin: origin, rotation: rotation), - QuadVertex(x: origin.x + hBottomHalfSize, y: origin.y - vHalfSize, textCoord: CGPoint(x: 1, y: 0), color: color, origin: origin, rotation: rotation), - QuadVertex(x: origin.x - hTopHalfSize, y: origin.y + vHalfSize, textCoord: CGPoint(x: 0, y: 1), color: color, origin: origin, rotation: rotation), - QuadVertex(x: origin.x + hBottomHalfSize, y: origin.y - vHalfSize, textCoord: CGPoint(x: 1, y: 0), color: color, origin: origin, rotation: rotation), - QuadVertex(x: origin.x - hTopHalfSize, y: origin.y + vHalfSize, textCoord: CGPoint(x: 0, y: 1), color: color, origin: origin, rotation: rotation), - QuadVertex(x: origin.x + hBottomHalfSize, y: origin.y + vHalfSize, textCoord: CGPoint(x: 1, y: 1), color: color, origin: origin, rotation: rotation) - ] - } -} - -enum QuadShape: Codable { - case rounded - case squared - case calligraphic(CGFloat, CGFloat) - case trapezoid(topFactor: CGFloat, bottomFactor: CGFloat, heightFactor: CGFloat) } diff --git a/Memola/Canvas/Geometries/Primitives/QuadShape.swift b/Memola/Canvas/Geometries/Primitives/QuadShape.swift new file mode 100644 index 0000000..07ed3e7 --- /dev/null +++ b/Memola/Canvas/Geometries/Primitives/QuadShape.swift @@ -0,0 +1,13 @@ +// +// QuadShape.swift +// Memola +// +// Created by Dscyre Scotti on 5/10/24. +// + +import Foundation + +enum QuadShape: Int16 { + case rounded + case squared +} diff --git a/Memola/Canvas/Geometries/Primitives/StrokeQuad.swift b/Memola/Canvas/Geometries/Primitives/StrokeQuad.swift deleted file mode 100644 index 23eae5b..0000000 --- a/Memola/Canvas/Geometries/Primitives/StrokeQuad.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// StrokeQuad.swift -// Memola -// -// Created by Dscyre Scotti on 5/4/24. -// - -import MetalKit -import Foundation - -final class StrokeQuad: NSObject, Codable { - var quad: Quad - - init(quad: Quad) { - self.quad = quad - } -} - diff --git a/Memola/Canvas/Geometries/Stroke/Generators/SolidPointStrokeGenerator.swift b/Memola/Canvas/Geometries/Stroke/Generators/SolidPointStrokeGenerator.swift index f2fde69..30d8c31 100644 --- a/Memola/Canvas/Geometries/Stroke/Generators/SolidPointStrokeGenerator.swift +++ b/Memola/Canvas/Geometries/Stroke/Generators/SolidPointStrokeGenerator.swift @@ -27,7 +27,7 @@ struct SolidPointStrokeGenerator: StrokeGenerator { let control = CGPoint.middle(p1: start, p2: end) addCurve(from: start, to: end, by: control, on: stroke) case 3: - discardVertices(upto: stroke.vertexIndex, on: stroke) + discardVertices(upto: stroke.vertexIndex, quadIndex: stroke.quadIndex, on: stroke) let index = stroke.keyPoints.count - 1 var start = stroke.keyPoints[index - 2] var end = CGPoint.middle(p1: stroke.keyPoints[index - 2], p2: stroke.keyPoints[index - 1]) @@ -62,7 +62,7 @@ struct SolidPointStrokeGenerator: StrokeGenerator { } private func smoothOutPath(on stroke: Stroke) { - discardVertices(upto: stroke.vertexIndex, on: stroke) + discardVertices(upto: stroke.vertexIndex, quadIndex: stroke.quadIndex, on: stroke) adjustPreviousKeyPoint(on: stroke) switch stroke.keyPoints.count { case 4: @@ -79,6 +79,7 @@ struct SolidPointStrokeGenerator: StrokeGenerator { 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.vertexIndex = stroke.vertices.endIndex - 1 } @@ -105,9 +106,8 @@ struct SolidPointStrokeGenerator: StrokeGenerator { case .random: rotation = CGFloat.random(in: 0...360) * .pi / 180 } - let quad = Quad(origin: point, size: stroke.thickness, color: stroke.color, rotation: rotation) - stroke._quads.append(quad) - stroke.vertices.append(contentsOf: quad.generateVertices()) + let quad = stroke.addQuad(at: point, rotation: rotation, shape: .rounded) + stroke.vertices.append(contentsOf: quad.generateVertices(stroke.color)) stroke.vertexCount = stroke.vertices.endIndex } @@ -116,7 +116,7 @@ struct SolidPointStrokeGenerator: StrokeGenerator { let factor: CGFloat switch configuration.granularity { case .automatic: - factor = min(3.5, 1 / (stroke.thickness * 1 / 10)) + factor = min(5, 1 / (stroke.thickness * 1 / 50)) case .fixed: factor = 1 / (stroke.thickness * stroke.penStyle.anyPenStyle.stepRate) case .none: @@ -145,15 +145,28 @@ struct SolidPointStrokeGenerator: StrokeGenerator { } } - private func discardVertices(upto index: Int, on stroke: Stroke) { + private func discardVertices(upto index: Int, quadIndex: Int, on stroke: Stroke) { if index < 0 { stroke.vertices.removeAll() - stroke._quads.removeAll() + discardQuads(from: quadIndex + 1, on: stroke) } else { let count = stroke.vertices.endIndex let dropCount = count - (max(0, index) + 1) stroke.vertices.removeLast(dropCount) - stroke._quads.removeLast(dropCount / 6) + discardQuads(from: quadIndex + 1, on: stroke) + } + } + + private func discardQuads(from start: Int, on stroke: Stroke) { + let quads = stroke.quads.array + Persistence.performe { context in + for index in start.. + @NSManaged var quads: NSMutableOrderedSet @NSManaged var graphicContext: GraphicContext? var angle: CGFloat = 0 @@ -31,7 +31,6 @@ final class Stroke: NSManagedObject { var thicknessFactor: CGFloat = 0.7 var vertices: [QuadVertex] = [] - var _quads: [Quad] = [] var vertexBuffer: MTLBuffer? var vertexCount: Int = 0 @@ -56,18 +55,25 @@ final class Stroke: NSManagedObject { func finish(at point: CGPoint) { penStyle.anyPenStyle.generator.finish(at: point, on: self) keyPoints.removeAll() - NSLog("[Memola] - \(_quads.count) quads") + NSLog("[Memola] - \(quads.count) quads") } func loadVertices() { - vertices = strokeQuads - .flatMap { $0.quad.generateVertices() } + vertices = quads + .flatMap { ($0 as? Quad)?.generateVertices(color) ?? [] } vertexCount = vertices.endIndex } - func saveQuads() { - strokeQuads = _quads.map(StrokeQuad.init) - _quads.removeAll() + func addQuad(at point: CGPoint, rotation: CGFloat, shape: QuadShape) -> Quad { + let quad = Quad(context: Persistence.context) + quad.id = UUID() + quad.origin = point + quad.rotation = rotation + quad.size = thickness + quad.shape = shape.rawValue + quads.add(quad) + quad.stroke = self + return quad } } @@ -80,6 +86,9 @@ extension Stroke: Drawable { } func draw(device: MTLDevice, renderEncoder: MTLRenderCommandEncoder) { + if isEmpty { + loadVertices() + } guard !isEmpty else { return } prepare(device: device) renderEncoder.setFragmentTexture(texture, index: 0) diff --git a/Memola/Canvas/History/History.swift b/Memola/Canvas/History/History.swift index 87f6911..1fe8651 100644 --- a/Memola/Canvas/History/History.swift +++ b/Memola/Canvas/History/History.swift @@ -49,6 +49,15 @@ class History: ObservableObject { func resetRedo() { redoCache = redoStack + for event in redoStack { + switch event { + case .stroke(let stroke): + Persistence.performe { context in + context.delete(stroke) + } + } + } + Persistence.saveIfNeeded() redoStack.removeAll() } diff --git a/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift b/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift index 117d49b..c4df853 100644 --- a/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift +++ b/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift @@ -61,7 +61,10 @@ class CanvasViewController: UIViewController { override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) - Persistence.shared.viewContext.refresh(canvas, mergeChanges: false) + history.resetRedo() + Persistence.performe { context in + context.refresh(canvas, mergeChanges: false) + } } } diff --git a/Memola/Persistence/Core/Persistence.swift b/Memola/Persistence/Core/Persistence.swift index 505a41f..a488b68 100644 --- a/Memola/Persistence/Core/Persistence.swift +++ b/Memola/Persistence/Core/Persistence.swift @@ -13,11 +13,13 @@ class Persistence { static let shared: Persistence = Persistence() - private init() { - QuadValueTransformer.register() - } + private init() { } - lazy var viewContext: NSManagedObjectContext = { + static var context: NSManagedObjectContext = { + shared.persistentContainer.viewContext + }() + + private lazy var viewContext: NSManagedObjectContext = { persistentContainer.viewContext }() @@ -65,4 +67,18 @@ class Persistence { fatalError("[Memola]: \(error.localizedDescription)") } }() + + static func performe(_ action: (NSManagedObjectContext) -> Void) { + action(shared.viewContext) + } + + static func saveIfNeeded() { + if context.hasChanges { + do { + try context.save() + } catch { + NSLog("[Memola] - \(error.localizedDescription)") + } + } + } } diff --git a/Memola/Persistence/Transformers/QuadValueTransformer.swift b/Memola/Persistence/Transformers/QuadValueTransformer.swift deleted file mode 100644 index 63b03b3..0000000 --- a/Memola/Persistence/Transformers/QuadValueTransformer.swift +++ /dev/null @@ -1,53 +0,0 @@ -// -// QuadValueTransformer.swift -// Memola -// -// Created by Dscyre Scotti on 5/8/24. -// - -import CoreData -import Foundation - -@objc(QuadValueTransformer) -class QuadValueTransformer: ValueTransformer { - static let name = NSValueTransformerName(rawValue: String(describing: QuadValueTransformer.self)) - - override class func transformedValueClass() -> AnyClass { - StrokeQuad.self - } - - override func transformedValue(_ value: Any?) -> Any? { - guard let quads = value as? [StrokeQuad] else { - assertionFailure("[Memola] - Failed to transform `[Quad]` to `Data`") - return nil - } - do { - let data = try JSONEncoder().encode(quads.map(\.quad)) - return data - } catch { - print(error.localizedDescription) - assertionFailure("[Memola] - Failed to transform `Quad` to `Data`") - return nil - } - } - - override func reverseTransformedValue(_ value: Any?) -> Any? { - guard let data = value as? Data else { - assertionFailure("[Memola] - Failed to transform `Data` to `Quad`") - return nil - } - do { - let quads = try JSONDecoder().decode([Quad].self, from: data) - return quads.map(StrokeQuad.init) - } catch { - print(error.localizedDescription) - assertionFailure("[Memola] - Failed to transform `Data` to `Quad`") - return nil - } - } - - static func register() { - let transformer = QuadValueTransformer() - ValueTransformer.setValueTransformer(transformer, forName: name) - } -} diff --git a/Memola/Resources/Models/MemolaModel.xcdatamodeld/MemolaModel.xcdatamodel/contents b/Memola/Resources/Models/MemolaModel.xcdatamodeld/MemolaModel.xcdatamodel/contents index 89c2561..2d521cc 100644 --- a/Memola/Resources/Models/MemolaModel.xcdatamodeld/MemolaModel.xcdatamodel/contents +++ b/Memola/Resources/Models/MemolaModel.xcdatamodeld/MemolaModel.xcdatamodel/contents @@ -19,13 +19,22 @@ + + + + + + + + + - + \ No newline at end of file From a903a5eed3f5a42bff2196772f2d1ccf968cdad4 Mon Sep 17 00:00:00 2001 From: dscyrescotti Date: Sat, 11 May 2024 10:41:59 +0700 Subject: [PATCH 08/11] refactor: clean up --- Memola/Canvas/Core/Canvas.swift | 4 ++-- Memola/Canvas/Geometries/Stroke/Stroke.swift | 3 --- Memola/Persistence/Core/Persistence.swift | 6 ++++++ 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/Memola/Canvas/Core/Canvas.swift b/Memola/Canvas/Core/Canvas.swift index c94e09a..51a56c3 100644 --- a/Memola/Canvas/Core/Canvas.swift +++ b/Memola/Canvas/Core/Canvas.swift @@ -20,7 +20,7 @@ final class Canvas: NSManagedObject, Identifiable { let gridContext = GridContext() let viewPortContext = ViewPortContext() - let maximumZoomScale: CGFloat = 25 + let maximumZoomScale: CGFloat = 28 let minimumZoomScale: CGFloat = 3.1 var transform: simd_float4x4 = .init() @@ -42,11 +42,11 @@ final class Canvas: NSManagedObject, Identifiable { // MARK: - Actions extension Canvas { func load() { + state = .loading let start = Date().formatted(.dateTime.minute().second().secondFraction(.fractional(5))) for stroke in graphicContext.strokes { if let stroke = stroke as? Stroke { stroke.loadVertices() - NSLog("[Memola] - \(stroke.quads.count) quads") } } let end = Date().formatted(.dateTime.minute().second().secondFraction(.fractional(5))) diff --git a/Memola/Canvas/Geometries/Stroke/Stroke.swift b/Memola/Canvas/Geometries/Stroke/Stroke.swift index 6832aa1..f5e0759 100644 --- a/Memola/Canvas/Geometries/Stroke/Stroke.swift +++ b/Memola/Canvas/Geometries/Stroke/Stroke.swift @@ -85,9 +85,6 @@ extension Stroke: Drawable { } func draw(device: MTLDevice, renderEncoder: MTLRenderCommandEncoder) { - if isEmpty { - loadVertices() - } guard !isEmpty else { return } prepare(device: device) renderEncoder.setFragmentTexture(texture, index: 0) diff --git a/Memola/Persistence/Core/Persistence.swift b/Memola/Persistence/Core/Persistence.swift index a488b68..9005f5f 100644 --- a/Memola/Persistence/Core/Persistence.swift +++ b/Memola/Persistence/Core/Persistence.swift @@ -19,6 +19,12 @@ class Persistence { shared.persistentContainer.viewContext }() + static var backgroundContext: NSManagedObjectContext = { + let context = shared.persistentContainer.newBackgroundContext() + context.automaticallyMergesChangesFromParent = true + return context + }() + private lazy var viewContext: NSManagedObjectContext = { persistentContainer.viewContext }() From 10e7350511d705bbff2677a6923747192e4bbd93 Mon Sep 17 00:00:00 2001 From: dscyrescotti Date: Sat, 11 May 2024 21:30:58 +0700 Subject: [PATCH 09/11] feat: execute core data related logic in background context --- Memola.xcodeproj/project.pbxproj | 32 ++++++- Memola/Canvas/Contexts/GraphicContext.swift | 92 +++++++++++++------ Memola/Canvas/Core/Canvas.swift | 41 +++++---- .../Canvas/Geometries/Primitives/Quad.swift | 31 +++++-- .../SolidPointStrokeGenerator.swift | 29 +----- Memola/Canvas/Geometries/Stroke/Stroke.swift | 85 +++++++++++++---- Memola/Canvas/History/History.swift | 8 +- .../Canvas/RenderPasses/CacheRenderPass.swift | 3 +- .../RenderPasses/GraphicRenderPass.swift | 5 +- .../ViewController/CanvasViewController.swift | 3 - .../View/Bridge/Views/DrawingView.swift | 2 +- Memola/Features/Memo/Memo.swift | 21 ----- Memola/Features/Memo/MemoView.swift | 9 +- Memola/Features/Memos/MemosView.swift | 48 +++++----- Memola/Persistence/Core/Persistence.swift | 24 ++++- Memola/Persistence/Objects/CanvasObject.swift | 22 +++++ .../Objects/GraphicContextObject.swift | 15 +++ Memola/Persistence/Objects/MemoObject.swift | 18 ++++ Memola/Persistence/Objects/QuadObject.swift | 19 ++++ Memola/Persistence/Objects/StrokeObject.swift | 19 ++++ .../MemolaModel.xcdatamodel/contents | 31 +++---- 21 files changed, 373 insertions(+), 184 deletions(-) delete mode 100644 Memola/Features/Memo/Memo.swift create mode 100644 Memola/Persistence/Objects/CanvasObject.swift create mode 100644 Memola/Persistence/Objects/GraphicContextObject.swift create mode 100644 Memola/Persistence/Objects/MemoObject.swift create mode 100644 Memola/Persistence/Objects/QuadObject.swift create mode 100644 Memola/Persistence/Objects/StrokeObject.swift diff --git a/Memola.xcodeproj/project.pbxproj b/Memola.xcodeproj/project.pbxproj index 384123b..cf282ed 100644 --- a/Memola.xcodeproj/project.pbxproj +++ b/Memola.xcodeproj/project.pbxproj @@ -64,9 +64,13 @@ ECA738F62BE612B700A4542E /* MTLDevice++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738F52BE612B700A4542E /* MTLDevice++.swift */; }; ECA738FC2BE61C5200A4542E /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738FB2BE61C5200A4542E /* Persistence.swift */; }; ECA739012BE61D9C00A4542E /* MemolaModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = ECA738FF2BE61D9C00A4542E /* MemolaModel.xcdatamodeld */; }; - ECA739052BE61E3100A4542E /* Memo.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA739042BE61E3100A4542E /* Memo.swift */; }; ECA739082BE623F300A4542E /* PenToolView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA739072BE623F300A4542E /* PenToolView.swift */; }; ECEC01A82BEE11BA006DA24C /* QuadShape.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECEC01A72BEE11BA006DA24C /* QuadShape.swift */; }; + ECFA15202BEF21EF00455818 /* MemoObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECFA151F2BEF21EF00455818 /* MemoObject.swift */; }; + ECFA15222BEF21F500455818 /* CanvasObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECFA15212BEF21F500455818 /* CanvasObject.swift */; }; + ECFA15242BEF223300455818 /* GraphicContextObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECFA15232BEF223300455818 /* GraphicContextObject.swift */; }; + ECFA15262BEF224900455818 /* StrokeObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECFA15252BEF224900455818 /* StrokeObject.swift */; }; + ECFA15282BEF225000455818 /* QuadObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECFA15272BEF225000455818 /* QuadObject.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -128,9 +132,13 @@ ECA738F52BE612B700A4542E /* MTLDevice++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MTLDevice++.swift"; sourceTree = ""; }; ECA738FB2BE61C5200A4542E /* Persistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = ""; }; ECA739002BE61D9C00A4542E /* MemolaModel.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MemolaModel.xcdatamodel; sourceTree = ""; }; - ECA739042BE61E3100A4542E /* Memo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Memo.swift; sourceTree = ""; }; ECA739072BE623F300A4542E /* PenToolView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PenToolView.swift; sourceTree = ""; }; ECEC01A72BEE11BA006DA24C /* QuadShape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuadShape.swift; sourceTree = ""; }; + ECFA151F2BEF21EF00455818 /* MemoObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoObject.swift; sourceTree = ""; }; + ECFA15212BEF21F500455818 /* CanvasObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CanvasObject.swift; sourceTree = ""; }; + ECFA15232BEF223300455818 /* GraphicContextObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphicContextObject.swift; sourceTree = ""; }; + ECFA15252BEF224900455818 /* StrokeObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StrokeObject.swift; sourceTree = ""; }; + ECFA15272BEF225000455818 /* QuadObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuadObject.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -226,7 +234,6 @@ ECA7387B2BE5EF3500A4542E /* Memo */ = { isa = PBXGroup; children = ( - ECA739042BE61E3100A4542E /* Memo.swift */, ECA7387C2BE5EF4B00A4542E /* MemoView.swift */, ECA739072BE623F300A4542E /* PenToolView.swift */, ); @@ -453,6 +460,7 @@ ECA738FA2BE61B1700A4542E /* Persistence */ = { isa = PBXGroup; children = ( + ECFA151E2BEF21BE00455818 /* Objects */, ECA739022BE61DE700A4542E /* Core */, ); path = Persistence; @@ -491,6 +499,18 @@ path = Core; sourceTree = ""; }; + ECFA151E2BEF21BE00455818 /* Objects */ = { + isa = PBXGroup; + children = ( + ECFA151F2BEF21EF00455818 /* MemoObject.swift */, + ECFA15212BEF21F500455818 /* CanvasObject.swift */, + ECFA15232BEF223300455818 /* GraphicContextObject.swift */, + ECFA15252BEF224900455818 /* StrokeObject.swift */, + ECFA15272BEF225000455818 /* QuadObject.swift */, + ); + path = Objects; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -569,14 +589,17 @@ ECA7389C2BE601AF00A4542E /* GridVertex.swift in Sources */, ECA738A82BE6025900A4542E /* GraphicUniforms.swift in Sources */, ECA738E62BE611FD00A4542E /* CGRect++.swift in Sources */, + ECFA15262BEF224900455818 /* StrokeObject.swift in Sources */, ECA738E82BE6120F00A4542E /* Color++.swift in Sources */, ECA738FC2BE61C5200A4542E /* Persistence.swift in Sources */, ECA7387A2BE5EF0400A4542E /* MemosView.swift in Sources */, ECA738BA2BE60DEF00A4542E /* HistoryAction.swift in Sources */, + ECFA15222BEF21F500455818 /* CanvasObject.swift in Sources */, ECA738AA2BE6026D00A4542E /* Uniforms.swift in Sources */, ECA7387D2BE5EF4B00A4542E /* MemoView.swift in Sources */, ECA738DA2BE60FF100A4542E /* CacheRenderPass.swift in Sources */, ECA738CD2BE60F2F00A4542E /* GridContext.swift in Sources */, + ECFA15202BEF21EF00455818 /* MemoObject.swift in Sources */, ECA738C62BE60E9D00A4542E /* EraserPenStyle.swift in Sources */, ECA738EA2BE6122E00A4542E /* CGPoint++.swift in Sources */, ECA738A62BE6023F00A4542E /* GridUniforms.swift in Sources */, @@ -594,11 +617,11 @@ ECA738952BE6012D00A4542E /* ViewPort.metal in Sources */, ECA739012BE61D9C00A4542E /* MemolaModel.xcdatamodeld in Sources */, ECA738F02BE6127700A4542E /* CGSize++.swift in Sources */, + ECFA15242BEF223300455818 /* GraphicContextObject.swift in Sources */, ECA738EC2BE6124E00A4542E /* CGAffineTransform++.swift in Sources */, ECA738E22BE610D000A4542E /* GraphicRenderPass.swift in Sources */, ECA738DC2BE6108D00A4542E /* StrokeRenderPass.swift in Sources */, ECA738F42BE612A000A4542E /* Array++.swift in Sources */, - ECA739052BE61E3100A4542E /* Memo.swift in Sources */, EC4538892BEBCAE000A86FEC /* Quad.swift in Sources */, ECA7388F2BE600DA00A4542E /* Grid.metal in Sources */, ECA738C92BE60EF700A4542E /* GraphicContext.swift in Sources */, @@ -607,6 +630,7 @@ ECA738B32BE60D9E00A4542E /* CanvasView.swift in Sources */, ECA738C42BE60E8800A4542E /* MarkerPenStyle.swift in Sources */, ECA738BF2BE60E3400A4542E /* Pen.swift in Sources */, + ECFA15282BEF225000455818 /* QuadObject.swift in Sources */, ECA738932BE6011100A4542E /* Stroke.metal in Sources */, ECA738B62BE60DCD00A4542E /* History.swift in Sources */, ECA738D22BE60F7B00A4542E /* Stroke.swift in Sources */, diff --git a/Memola/Canvas/Contexts/GraphicContext.swift b/Memola/Canvas/Contexts/GraphicContext.swift index 6775eb7..5e5b67a 100644 --- a/Memola/Canvas/Contexts/GraphicContext.swift +++ b/Memola/Canvas/Contexts/GraphicContext.swift @@ -10,11 +10,9 @@ import MetalKit import CoreData import Foundation -@objc(GraphicContext) -final class GraphicContext: NSManagedObject { - @NSManaged var id: UUID - @NSManaged var canvas: Canvas? - @NSManaged var strokes: NSMutableOrderedSet +final class GraphicContext: @unchecked Sendable { + var strokes: [Stroke] = [] + var object: GraphicContextObject? var currentStroke: Stroke? var previousStroke: Stroke? @@ -24,8 +22,7 @@ final class GraphicContext: NSManagedObject { var vertexCount: Int = 4 var vertexBuffer: MTLBuffer? - override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) { - super.init(entity: entity, insertInto: context) + init() { setViewPortVertices() } @@ -40,21 +37,37 @@ final class GraphicContext: NSManagedObject { } func undoGraphic() { - guard let stroke = strokes.lastObject as? Stroke else { return } - strokes.remove(stroke) - stroke.graphicContext = nil + guard !strokes.isEmpty else { return } + let stroke = strokes.removeLast() + Persistence.backgroundContext.perform { + stroke.object?.graphicContext = nil + Persistence.saveIfNeededInBackground() + } previousStroke = nil - Persistence.saveIfNeeded() } func redoGraphic(for event: HistoryEvent) { switch event { case .stroke(let stroke): - strokes.add(stroke) - stroke.graphicContext = self + strokes.append(stroke) + Persistence.backgroundContext.perform { [weak self] in + stroke.object?.graphicContext = self?.object + Persistence.saveIfNeededInBackground() + } previousStroke = nil } - Persistence.saveIfNeeded() + } +} + +extension GraphicContext { + func load() { + 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.loadVertices() + return _stroke + } } } @@ -76,15 +89,24 @@ extension GraphicContext: Drawable { extension GraphicContext { func beginStroke(at point: CGPoint, pen: Pen) -> Stroke { - let stroke = Stroke(context: Persistence.context) - stroke.id = UUID() - stroke.color = pen.color - stroke.style = pen.strokeStyle.rawValue - stroke.thickness = pen.thickness - stroke.createdAt = .now - stroke.quads = [] - stroke.graphicContext = self - strokes.add(stroke) + let stroke = Stroke( + color: pen.color, + style: pen.strokeStyle.rawValue, + createdAt: .now, + thickness: pen.thickness + ) + Persistence.backgroundContext.perform { [graphicContext = object, _stroke = stroke] in + let stroke = StrokeObject(context: Persistence.backgroundContext) + stroke.color = _stroke.color + stroke.style = _stroke.style + stroke.thickness = _stroke.thickness + stroke.createdAt = _stroke.createdAt + stroke.quads = [] + stroke.graphicContext = graphicContext + graphicContext?.strokes.add(stroke) + _stroke.object = stroke + } + strokes.append(stroke) currentStroke = stroke currentPoint = point currentStroke?.begin(at: point) @@ -101,19 +123,31 @@ extension GraphicContext { func endStroke(at point: CGPoint) { guard currentPoint != nil, let currentStroke else { return } currentStroke.finish(at: point) - Persistence.saveIfNeeded() + let saveIndex = currentStroke.batchIndex + let quads = Array(currentStroke.quads[saveIndex.. 80 @@ -42,16 +44,21 @@ final class Canvas: NSManagedObject, Identifiable { // MARK: - Actions extension Canvas { func load() { - state = .loading - let start = Date().formatted(.dateTime.minute().second().secondFraction(.fractional(5))) - for stroke in graphicContext.strokes { - if let stroke = stroke as? Stroke { - stroke.loadVertices() + Persistence.backgroundContext.perform { [weak self, canvasID] in + DispatchQueue.main.async { [weak self] in + self?.state = .loading + } + guard let canvas = Persistence.backgroundContext.object(with: canvasID) as? CanvasObject else { + return + } + let graphicContext = canvas.graphicContext + self?.graphicContext.object = graphicContext + self?.graphicContext.load() + Persistence.backgroundContext.refresh(canvas, mergeChanges: false) + DispatchQueue.main.async { [weak self] in + self?.state = .loaded } } - let end = Date().formatted(.dateTime.minute().second().secondFraction(.fractional(5))) - NSLog("[Memola] - Loaded from \(start) to \(end)") - state = .loaded } } @@ -104,7 +111,7 @@ extension Canvas { } func getNewlyAddedStroke() -> Stroke? { - graphicContext.strokes.lastObject as? Stroke + graphicContext.strokes.last } } diff --git a/Memola/Canvas/Geometries/Primitives/Quad.swift b/Memola/Canvas/Geometries/Primitives/Quad.swift index f5f5742..029fbb0 100644 --- a/Memola/Canvas/Geometries/Primitives/Quad.swift +++ b/Memola/Canvas/Geometries/Primitives/Quad.swift @@ -8,15 +8,28 @@ import CoreData import Foundation -@objc(Quad) -class Quad: NSManagedObject { - @NSManaged var id: UUID - @NSManaged var originX: CGFloat - @NSManaged var originY: CGFloat - @NSManaged var size: CGFloat - @NSManaged var rotation: CGFloat - @NSManaged var shape: Int16 - @NSManaged var stroke: Stroke? +struct Quad { + var originX: CGFloat + var originY: CGFloat + var size: CGFloat + var rotation: CGFloat + var shape: Int16 + + init(object: QuadObject) { + self.originX = object.originX + self.originY = object.originY + self.size = object.size + self.rotation = object.rotation + self.shape = object.shape + } + + init(origin: CGPoint, size: CGFloat, rotation: CGFloat, shape: Int16) { + self.originX = origin.x + self.originY = origin.y + self.size = size + self.rotation = rotation + self.shape = shape + } var origin: CGPoint { get { CGPoint(x: originX, y: originY) } diff --git a/Memola/Canvas/Geometries/Stroke/Generators/SolidPointStrokeGenerator.swift b/Memola/Canvas/Geometries/Stroke/Generators/SolidPointStrokeGenerator.swift index 30d8c31..94d8e4a 100644 --- a/Memola/Canvas/Geometries/Stroke/Generators/SolidPointStrokeGenerator.swift +++ b/Memola/Canvas/Geometries/Stroke/Generators/SolidPointStrokeGenerator.swift @@ -132,42 +132,15 @@ struct SolidPointStrokeGenerator: StrokeGenerator { } } - #warning("TODO: remove later") - private func addLine(from start: CGPoint, to end: CGPoint, on stroke: Stroke) { - let distance = end.distance(to: start) - let segments = max(distance / stroke.penStyle.anyPenStyle.stepRate, 2) - for i in 0.. Quad { - let quad = Quad(context: Persistence.context) - quad.id = UUID() - quad.origin = point - quad.rotation = rotation - quad.size = thickness - quad.shape = shape.rawValue - quads.add(quad) - quad.stroke = self + let quad = Quad( + origin: point, + size: thickness, + rotation: rotation, + shape: shape.rawValue + ) + quads.append(quad) return quad } + + func removeQuads(from index: Int) { + let dropCount = quads.endIndex - max(1, index) + quads.removeLast(dropCount) + let quads = Array(quads[batchIndex.. Bool { diff --git a/Memola/Features/Memo/Memo.swift b/Memola/Features/Memo/Memo.swift deleted file mode 100644 index 5194b38..0000000 --- a/Memola/Features/Memo/Memo.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// Memo.swift -// Memola -// -// Created by Dscyre Scotti on 5/4/24. -// - -import CoreData -import Foundation - -@objc(Memo) -final class Memo: NSManagedObject { - @NSManaged var id: UUID - @NSManaged var title: String - @NSManaged var data: Data - @NSManaged var createdAt: Date - @NSManaged var updatedAt: Date - @NSManaged var canvas: Canvas -} - -extension Memo: Identifiable { } diff --git a/Memola/Features/Memo/MemoView.swift b/Memola/Features/Memo/MemoView.swift index 0673109..c6e2277 100644 --- a/Memola/Features/Memo/MemoView.swift +++ b/Memola/Features/Memo/MemoView.swift @@ -6,15 +6,22 @@ // import SwiftUI +import CoreData struct MemoView: View { @Environment(\.dismiss) var dismiss @Environment(\.managedObjectContext) var managedObjectContext @StateObject var tool = Tool() + @StateObject var canvas: Canvas @StateObject var history = History() - @EnvironmentObject var canvas: Canvas + let memo: MemoObject + + init(memo: MemoObject) { + self.memo = memo + self._canvas = StateObject(wrappedValue: Canvas(size: memo.canvas.size, canvasID: memo.canvas.objectID)) + } var body: some View { CanvasView() diff --git a/Memola/Features/Memos/MemosView.swift b/Memola/Features/Memos/MemosView.swift index c0972d9..57e70aa 100644 --- a/Memola/Features/Memos/MemosView.swift +++ b/Memola/Features/Memos/MemosView.swift @@ -10,9 +10,9 @@ import SwiftUI struct MemosView: View { @Environment(\.managedObjectContext) var managedObjectContext - @FetchRequest(sortDescriptors: []) var memos: FetchedResults + @FetchRequest(sortDescriptors: []) var memoObjects: FetchedResults - @State var memo: Memo? + @State var memo: MemoObject? var body: some View { NavigationStack { @@ -30,15 +30,14 @@ struct MemosView: View { } } .fullScreenCover(item: $memo) { memo in - MemoView() - .environmentObject(memo.canvas) + MemoView(memo: memo) } } var memoGrid: some View { ScrollView { LazyVGrid(columns: .init(repeating: GridItem(.flexible()), count: 3)) { - ForEach(memos) { memo in + ForEach(memoObjects) { memo in memoCard(memo) } } @@ -46,47 +45,44 @@ struct MemosView: View { } } - func memoCard(_ memo: Memo) -> some View { + func memoCard(_ memoObject: MemoObject) -> some View { VStack(alignment: .leading) { Rectangle() .frame(height: 150) - Text(memo.title) + Text(memoObject.title) } .onTapGesture { - openMemo(for: memo) + openMemo(for: memoObject) } } func createMemo(title: String) { do { - let memo = Memo(context: managedObjectContext) - memo.id = UUID() - memo.title = title - memo.createdAt = .now - memo.updatedAt = .now + let memoObject = MemoObject(context: managedObjectContext) + memoObject.title = title + memoObject.createdAt = .now + memoObject.updatedAt = .now - let canvas = Canvas(context: managedObjectContext) - canvas.id = UUID() - canvas.width = 4_000 - canvas.height = 4_000 + let canvasObject = CanvasObject(context: managedObjectContext) + canvasObject.width = 4_000 + canvasObject.height = 4_000 - let graphicContext = GraphicContext(context: managedObjectContext) - graphicContext.id = UUID() - graphicContext.strokes = [] + let graphicContextObject = GraphicContextObject(context: managedObjectContext) + graphicContextObject.strokes = [] - memo.canvas = canvas - canvas.memo = memo - canvas.graphicContext = graphicContext - graphicContext.canvas = canvas + memoObject.canvas = canvasObject + canvasObject.memo = memoObject + canvasObject.graphicContext = graphicContextObject + graphicContextObject.canvas = canvasObject try managedObjectContext.save() - openMemo(for: memo) + openMemo(for: memoObject) } catch { NSLog("[Memola] - \(error.localizedDescription)") } } - func openMemo(for memo: Memo) { + func openMemo(for memo: MemoObject) { self.memo = memo } } diff --git a/Memola/Persistence/Core/Persistence.swift b/Memola/Persistence/Core/Persistence.swift index 9005f5f..e87f914 100644 --- a/Memola/Persistence/Core/Persistence.swift +++ b/Memola/Persistence/Core/Persistence.swift @@ -21,7 +21,9 @@ class Persistence { static var backgroundContext: NSManagedObjectContext = { let context = shared.persistentContainer.newBackgroundContext() - context.automaticallyMergesChangesFromParent = true + context.undoManager = nil + +// context.automaticallyMergesC hangesFromParent = true return context }() @@ -87,4 +89,24 @@ class Persistence { } } } + + static func saveIfNeededInBackground() { + if backgroundContext.hasChanges { + do { + try backgroundContext.save() + } catch { + NSLog("[Memola] - \(error.localizedDescription)") + } + } + } +} + +extension Persistence { + static func background(_ task: @escaping (NSManagedObjectContext) async throws -> Void, errorHandler: ((Error) async -> Void)? = nil) async { + do { + try await task(backgroundContext) + } catch { + await errorHandler?(error) + } + } } diff --git a/Memola/Persistence/Objects/CanvasObject.swift b/Memola/Persistence/Objects/CanvasObject.swift new file mode 100644 index 0000000..6204db1 --- /dev/null +++ b/Memola/Persistence/Objects/CanvasObject.swift @@ -0,0 +1,22 @@ +// +// CanvasObject.swift +// Memola +// +// Created by Dscyre Scotti on 5/11/24. +// + +import CoreData +import Foundation + + +@objc(CanvasObject) +final class CanvasObject: NSManagedObject { + @NSManaged var width: CGFloat + @NSManaged var height: CGFloat + @NSManaged var memo: MemoObject? + @NSManaged var graphicContext: GraphicContextObject + + var size: CGSize { + CGSize(width: width, height: height) + } +} diff --git a/Memola/Persistence/Objects/GraphicContextObject.swift b/Memola/Persistence/Objects/GraphicContextObject.swift new file mode 100644 index 0000000..f62a765 --- /dev/null +++ b/Memola/Persistence/Objects/GraphicContextObject.swift @@ -0,0 +1,15 @@ +// +// GraphicContextObject.swift +// Memola +// +// Created by Dscyre Scotti on 5/11/24. +// + +import CoreData +import Foundation + +@objc(GraphicContextObject) +final class GraphicContextObject: NSManagedObject { + @NSManaged var canvas: CanvasObject? + @NSManaged var strokes: NSMutableOrderedSet +} diff --git a/Memola/Persistence/Objects/MemoObject.swift b/Memola/Persistence/Objects/MemoObject.swift new file mode 100644 index 0000000..d3a6b61 --- /dev/null +++ b/Memola/Persistence/Objects/MemoObject.swift @@ -0,0 +1,18 @@ +// +// MemoObject.swift +// Memola +// +// Created by Dscyre Scotti on 5/11/24. +// + +import CoreData +import Foundation + +@objc(MemoObject) +final class MemoObject: NSManagedObject, Identifiable { + @NSManaged var title: String + @NSManaged var data: Data + @NSManaged var createdAt: Date + @NSManaged var updatedAt: Date + @NSManaged var canvas: CanvasObject +} diff --git a/Memola/Persistence/Objects/QuadObject.swift b/Memola/Persistence/Objects/QuadObject.swift new file mode 100644 index 0000000..f1c051f --- /dev/null +++ b/Memola/Persistence/Objects/QuadObject.swift @@ -0,0 +1,19 @@ +// +// QuadObject.swift +// Memola +// +// Created by Dscyre Scotti on 5/11/24. +// + +import CoreData +import Foundation + +@objc(QuadObject) +final class QuadObject: NSManagedObject { + @NSManaged var originX: CGFloat + @NSManaged var originY: CGFloat + @NSManaged var size: CGFloat + @NSManaged var rotation: CGFloat + @NSManaged var shape: Int16 + @NSManaged var stroke: StrokeObject? +} diff --git a/Memola/Persistence/Objects/StrokeObject.swift b/Memola/Persistence/Objects/StrokeObject.swift new file mode 100644 index 0000000..ffa049b --- /dev/null +++ b/Memola/Persistence/Objects/StrokeObject.swift @@ -0,0 +1,19 @@ +// +// StrokeObject.swift +// Memola +// +// Created by Dscyre Scotti on 5/11/24. +// + +import CoreData +import Foundation + +@objc(StrokeObject) +final class StrokeObject: NSManagedObject { + @NSManaged var color: [CGFloat] + @NSManaged var style: Int16 + @NSManaged var createdAt: Date + @NSManaged var thickness: CGFloat + @NSManaged var quads: NSMutableOrderedSet + @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 2d521cc..095d1d2 100644 --- a/Memola/Resources/Models/MemolaModel.xcdatamodeld/MemolaModel.xcdatamodel/contents +++ b/Memola/Resources/Models/MemolaModel.xcdatamodeld/MemolaModel.xcdatamodel/contents @@ -1,40 +1,35 @@ - + - - - + + - - - - + + + - + - - + - - + - + - + - - - + + \ No newline at end of file From de9deb7fafce3a250a53bf82522c783bd3509b9a Mon Sep 17 00:00:00 2001 From: dscyrescotti Date: Sat, 11 May 2024 22:57:26 +0700 Subject: [PATCH 10/11] refactor: clean up persistence related code --- Memola.xcodeproj/project.pbxproj | 12 ++++ Memola/App/MemolaApp.swift | 2 +- Memola/Canvas/Contexts/GraphicContext.swift | 24 ++++---- Memola/Canvas/Core/Canvas.swift | 6 +- Memola/Canvas/Geometries/Stroke/Stroke.swift | 4 +- Memola/Canvas/History/History.swift | 6 +- Memola/Extensions/NSManagedObject++.swift | 15 +++++ .../Extensions/NSManagedObjectContext++.swift | 16 ++++++ Memola/Extensions/View++.swift | 16 ++++++ Memola/Persistence/Core/Persistence.swift | 57 +++++-------------- 10 files changed, 95 insertions(+), 63 deletions(-) create mode 100644 Memola/Extensions/NSManagedObject++.swift create mode 100644 Memola/Extensions/NSManagedObjectContext++.swift create mode 100644 Memola/Extensions/View++.swift diff --git a/Memola.xcodeproj/project.pbxproj b/Memola.xcodeproj/project.pbxproj index cf282ed..2dc59b4 100644 --- a/Memola.xcodeproj/project.pbxproj +++ b/Memola.xcodeproj/project.pbxproj @@ -7,6 +7,9 @@ objects = { /* Begin PBXBuildFile section */ + EC3565522BEFC65F00A4E0BF /* NSManagedObjectContext++.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC3565512BEFC65F00A4E0BF /* NSManagedObjectContext++.swift */; }; + EC3565542BEFC6AD00A4E0BF /* View++.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC3565532BEFC6AD00A4E0BF /* View++.swift */; }; + EC3565562BEFC7B300A4E0BF /* NSManagedObject++.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC3565552BEFC7B300A4E0BF /* NSManagedObject++.swift */; }; EC4538892BEBCAE000A86FEC /* Quad.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC4538882BEBCAE000A86FEC /* Quad.swift */; }; EC7F6BEC2BE5E6E300A34A7B /* MemolaApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC7F6BEB2BE5E6E300A34A7B /* MemolaApp.swift */; }; EC7F6BF02BE5E6E400A34A7B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = EC7F6BEF2BE5E6E400A34A7B /* Assets.xcassets */; }; @@ -74,6 +77,9 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + EC3565512BEFC65F00A4E0BF /* NSManagedObjectContext++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSManagedObjectContext++.swift"; sourceTree = ""; }; + EC3565532BEFC6AD00A4E0BF /* View++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View++.swift"; sourceTree = ""; }; + EC3565552BEFC7B300A4E0BF /* NSManagedObject++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSManagedObject++.swift"; sourceTree = ""; }; EC4538882BEBCAE000A86FEC /* Quad.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Quad.swift; sourceTree = ""; }; EC7F6BE82BE5E6E300A34A7B /* Memola.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Memola.app; sourceTree = BUILT_PRODUCTS_DIR; }; EC7F6BEB2BE5E6E300A34A7B /* MemolaApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemolaApp.swift; sourceTree = ""; }; @@ -339,6 +345,9 @@ ECA738E72BE6120F00A4542E /* Color++.swift */, ECA738F52BE612B700A4542E /* MTLDevice++.swift */, ECA738ED2BE6125D00A4542E /* simd_float4x4++.swift */, + EC3565512BEFC65F00A4E0BF /* NSManagedObjectContext++.swift */, + EC3565532BEFC6AD00A4E0BF /* View++.swift */, + EC3565552BEFC7B300A4E0BF /* NSManagedObject++.swift */, ); path = Extensions; sourceTree = ""; @@ -594,6 +603,7 @@ ECA738FC2BE61C5200A4542E /* Persistence.swift in Sources */, ECA7387A2BE5EF0400A4542E /* MemosView.swift in Sources */, ECA738BA2BE60DEF00A4542E /* HistoryAction.swift in Sources */, + EC3565522BEFC65F00A4E0BF /* NSManagedObjectContext++.swift in Sources */, ECFA15222BEF21F500455818 /* CanvasObject.swift in Sources */, ECA738AA2BE6026D00A4542E /* Uniforms.swift in Sources */, ECA7387D2BE5EF4B00A4542E /* MemoView.swift in Sources */, @@ -604,6 +614,7 @@ ECA738EA2BE6122E00A4542E /* CGPoint++.swift in Sources */, ECA738A62BE6023F00A4542E /* GridUniforms.swift in Sources */, ECA738D72BE60FC100A4542E /* SolidPointStrokeGenerator.swift in Sources */, + EC3565542BEFC6AD00A4E0BF /* View++.swift in Sources */, ECA738832BE5FEFE00A4542E /* RenderPass.swift in Sources */, ECEC01A82BEE11BA006DA24C /* QuadShape.swift in Sources */, ECA738862BE5FF2500A4542E /* Canvas.swift in Sources */, @@ -618,6 +629,7 @@ ECA739012BE61D9C00A4542E /* MemolaModel.xcdatamodeld in Sources */, ECA738F02BE6127700A4542E /* CGSize++.swift in Sources */, ECFA15242BEF223300455818 /* GraphicContextObject.swift in Sources */, + EC3565562BEFC7B300A4E0BF /* NSManagedObject++.swift in Sources */, ECA738EC2BE6124E00A4542E /* CGAffineTransform++.swift in Sources */, ECA738E22BE610D000A4542E /* GraphicRenderPass.swift in Sources */, ECA738DC2BE6108D00A4542E /* StrokeRenderPass.swift in Sources */, diff --git a/Memola/App/MemolaApp.swift b/Memola/App/MemolaApp.swift index 9f81d25..68edd1f 100644 --- a/Memola/App/MemolaApp.swift +++ b/Memola/App/MemolaApp.swift @@ -12,7 +12,7 @@ struct MemolaApp: App { var body: some Scene { WindowGroup { MemosView() - .environment(\.managedObjectContext, Persistence.context) + .persistence(\.viewContext) } } } diff --git a/Memola/Canvas/Contexts/GraphicContext.swift b/Memola/Canvas/Contexts/GraphicContext.swift index 5e5b67a..3a0705f 100644 --- a/Memola/Canvas/Contexts/GraphicContext.swift +++ b/Memola/Canvas/Contexts/GraphicContext.swift @@ -39,9 +39,9 @@ final class GraphicContext: @unchecked Sendable { func undoGraphic() { guard !strokes.isEmpty else { return } let stroke = strokes.removeLast() - Persistence.backgroundContext.perform { + withPersistence(\.backgroundContext) { [stroke] context in stroke.object?.graphicContext = nil - Persistence.saveIfNeededInBackground() + try context.saveIfNeeded() } previousStroke = nil } @@ -50,9 +50,9 @@ final class GraphicContext: @unchecked Sendable { switch event { case .stroke(let stroke): strokes.append(stroke) - Persistence.backgroundContext.perform { [weak self] in + withPersistence(\.backgroundContext) { [weak self, stroke] context in stroke.object?.graphicContext = self?.object - Persistence.saveIfNeededInBackground() + try context.saveIfNeeded() } previousStroke = nil } @@ -95,8 +95,8 @@ extension GraphicContext { createdAt: .now, thickness: pen.thickness ) - Persistence.backgroundContext.perform { [graphicContext = object, _stroke = stroke] in - let stroke = StrokeObject(context: Persistence.backgroundContext) + withPersistence(\.backgroundContext) { [graphicContext = object, _stroke = stroke] context in + let stroke = StrokeObject(\.backgroundContext) stroke.color = _stroke.color stroke.style = _stroke.style stroke.thickness = _stroke.thickness @@ -125,12 +125,12 @@ extension GraphicContext { currentStroke.finish(at: point) let saveIndex = currentStroke.batchIndex let quads = Array(currentStroke.quads[saveIndex..) { + self.init(context: Persistence.shared[keyPath: keyPath]) + } +} diff --git a/Memola/Extensions/NSManagedObjectContext++.swift b/Memola/Extensions/NSManagedObjectContext++.swift new file mode 100644 index 0000000..946da37 --- /dev/null +++ b/Memola/Extensions/NSManagedObjectContext++.swift @@ -0,0 +1,16 @@ +// +// NSManagedObjectContext++.swift +// Memola +// +// Created by Dscyre Scotti on 5/11/24. +// + +import CoreData + +extension NSManagedObjectContext { + func saveIfNeeded() throws { + if hasChanges { + try save() + } + } +} diff --git a/Memola/Extensions/View++.swift b/Memola/Extensions/View++.swift new file mode 100644 index 0000000..732a860 --- /dev/null +++ b/Memola/Extensions/View++.swift @@ -0,0 +1,16 @@ +// +// View++.swift +// Memola +// +// Created by Dscyre Scotti on 5/11/24. +// + +import SwiftUI +import CoreData +import Foundation + +extension View { + func persistence(_ keyPath: KeyPath) -> some View { + environment(\.managedObjectContext, Persistence.shared[keyPath: keyPath]) + } +} diff --git a/Memola/Persistence/Core/Persistence.swift b/Memola/Persistence/Core/Persistence.swift index e87f914..4cb4a9a 100644 --- a/Memola/Persistence/Core/Persistence.swift +++ b/Memola/Persistence/Core/Persistence.swift @@ -8,29 +8,24 @@ import CoreData import Foundation -class Persistence { +final class Persistence { private let modelName = "MemolaModel" static let shared: Persistence = Persistence() private init() { } - static var context: NSManagedObjectContext = { - shared.persistentContainer.viewContext - }() - - static var backgroundContext: NSManagedObjectContext = { - let context = shared.persistentContainer.newBackgroundContext() - context.undoManager = nil - -// context.automaticallyMergesC hangesFromParent = true - return context - }() - - private lazy var viewContext: NSManagedObjectContext = { + lazy var viewContext: NSManagedObjectContext = { persistentContainer.viewContext }() + lazy var backgroundContext: NSManagedObjectContext = { + let context = persistentContainer.newBackgroundContext() + context.undoManager = nil + context.automaticallyMergesChangesFromParent = true + return context + }() + lazy var persistentContainer: NSPersistentContainer = { let persistentStore = NSPersistentStoreDescription() persistentStore.shouldMigrateStoreAutomatically = true @@ -75,38 +70,16 @@ class Persistence { fatalError("[Memola]: \(error.localizedDescription)") } }() - - static func performe(_ action: (NSManagedObjectContext) -> Void) { - action(shared.viewContext) - } - - static func saveIfNeeded() { - if context.hasChanges { - do { - try context.save() - } catch { - NSLog("[Memola] - \(error.localizedDescription)") - } - } - } - - static func saveIfNeededInBackground() { - if backgroundContext.hasChanges { - do { - try backgroundContext.save() - } catch { - NSLog("[Memola] - \(error.localizedDescription)") - } - } - } } -extension Persistence { - static func background(_ task: @escaping (NSManagedObjectContext) async throws -> Void, errorHandler: ((Error) async -> Void)? = nil) async { +// MARK: - Global Method +func withPersistence(_ keypath: KeyPath, _ task: @escaping (NSManagedObjectContext) throws -> Void) { + let context = Persistence.shared[keyPath: keypath] + context.perform { do { - try await task(backgroundContext) + try task(context) } catch { - await errorHandler?(error) + NSLog("[Memola] - \(error.localizedDescription)") } } } From 6356b88c9a8191e1bba6b78e8b5cf9b94deb8213 Mon Sep 17 00:00:00 2001 From: dscyrescotti Date: Sun, 12 May 2024 23:12:07 +0700 Subject: [PATCH 11/11] feat: generate vertices in kernel shader --- Memola.xcodeproj/project.pbxproj | 8 ++ Memola/Canvas/Contexts/GraphicContext.swift | 8 +- Memola/Canvas/Core/Canvas.swift | 2 +- Memola/Canvas/Core/PipelineStates.swift | 9 ++ .../Canvas/Geometries/Primitives/Quad.swift | 92 ++++++++----------- .../SolidPointStrokeGenerator.swift | 19 +--- Memola/Canvas/Geometries/Stroke/Stroke.swift | 35 ++++--- .../RenderPasses/StrokeRenderPass.swift | 32 ++++++- Memola/Canvas/Shaders/Quad.metal | 54 +++++++++++ .../ViewController/CanvasViewController.swift | 3 + Memola/Extensions/Float++.swift | 14 +++ Memola/Features/Memos/MemosView.swift | 5 + Memola/Persistence/Objects/QuadObject.swift | 1 + .../MemolaModel.xcdatamodel/contents | 1 + 14 files changed, 186 insertions(+), 97 deletions(-) create mode 100644 Memola/Canvas/Shaders/Quad.metal create mode 100644 Memola/Extensions/Float++.swift diff --git a/Memola.xcodeproj/project.pbxproj b/Memola.xcodeproj/project.pbxproj index 2dc59b4..7401d7e 100644 --- a/Memola.xcodeproj/project.pbxproj +++ b/Memola.xcodeproj/project.pbxproj @@ -10,6 +10,8 @@ EC3565522BEFC65F00A4E0BF /* NSManagedObjectContext++.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC3565512BEFC65F00A4E0BF /* NSManagedObjectContext++.swift */; }; EC3565542BEFC6AD00A4E0BF /* View++.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC3565532BEFC6AD00A4E0BF /* View++.swift */; }; EC3565562BEFC7B300A4E0BF /* NSManagedObject++.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC3565552BEFC7B300A4E0BF /* NSManagedObject++.swift */; }; + EC35655A2BF060D900A4E0BF /* Quad.metal in Sources */ = {isa = PBXBuildFile; fileRef = EC3565592BF060D900A4E0BF /* Quad.metal */; }; + EC35655C2BF0712A00A4E0BF /* Float++.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC35655B2BF0712A00A4E0BF /* Float++.swift */; }; EC4538892BEBCAE000A86FEC /* Quad.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC4538882BEBCAE000A86FEC /* Quad.swift */; }; EC7F6BEC2BE5E6E300A34A7B /* MemolaApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC7F6BEB2BE5E6E300A34A7B /* MemolaApp.swift */; }; EC7F6BF02BE5E6E400A34A7B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = EC7F6BEF2BE5E6E400A34A7B /* Assets.xcassets */; }; @@ -80,6 +82,8 @@ EC3565512BEFC65F00A4E0BF /* NSManagedObjectContext++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSManagedObjectContext++.swift"; sourceTree = ""; }; EC3565532BEFC6AD00A4E0BF /* View++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View++.swift"; sourceTree = ""; }; EC3565552BEFC7B300A4E0BF /* NSManagedObject++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSManagedObject++.swift"; sourceTree = ""; }; + EC3565592BF060D900A4E0BF /* Quad.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; path = Quad.metal; sourceTree = ""; }; + EC35655B2BF0712A00A4E0BF /* Float++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Float++.swift"; sourceTree = ""; }; EC4538882BEBCAE000A86FEC /* Quad.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Quad.swift; sourceTree = ""; }; EC7F6BE82BE5E6E300A34A7B /* Memola.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Memola.app; sourceTree = BUILT_PRODUCTS_DIR; }; EC7F6BEB2BE5E6E300A34A7B /* MemolaApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemolaApp.swift; sourceTree = ""; }; @@ -300,6 +304,7 @@ ECA738922BE6011100A4542E /* Stroke.metal */, ECA738942BE6012D00A4542E /* ViewPort.metal */, ECA738962BE6014200A4542E /* Graphic.metal */, + EC3565592BF060D900A4E0BF /* Quad.metal */, ); path = Shaders; sourceTree = ""; @@ -348,6 +353,7 @@ EC3565512BEFC65F00A4E0BF /* NSManagedObjectContext++.swift */, EC3565532BEFC6AD00A4E0BF /* View++.swift */, EC3565552BEFC7B300A4E0BF /* NSManagedObject++.swift */, + EC35655B2BF0712A00A4E0BF /* Float++.swift */, ); path = Extensions; sourceTree = ""; @@ -594,6 +600,7 @@ ECA738E42BE6110800A4542E /* Drawable.swift in Sources */, ECA738AD2BE60CC600A4542E /* DrawingView.swift in Sources */, ECA738E02BE610B900A4542E /* EraserRenderPass.swift in Sources */, + EC35655A2BF060D900A4E0BF /* Quad.metal in Sources */, ECA738912BE600F500A4542E /* Cache.metal in Sources */, ECA7389C2BE601AF00A4542E /* GridVertex.swift in Sources */, ECA738A82BE6025900A4542E /* GraphicUniforms.swift in Sources */, @@ -631,6 +638,7 @@ ECFA15242BEF223300455818 /* GraphicContextObject.swift in Sources */, EC3565562BEFC7B300A4E0BF /* NSManagedObject++.swift in Sources */, ECA738EC2BE6124E00A4542E /* CGAffineTransform++.swift in Sources */, + EC35655C2BF0712A00A4E0BF /* Float++.swift in Sources */, ECA738E22BE610D000A4542E /* GraphicRenderPass.swift in Sources */, ECA738DC2BE6108D00A4542E /* StrokeRenderPass.swift in Sources */, ECA738F42BE612A000A4542E /* Array++.swift in Sources */, diff --git a/Memola/Canvas/Contexts/GraphicContext.swift b/Memola/Canvas/Contexts/GraphicContext.swift index 3a0705f..56a8ff1 100644 --- a/Memola/Canvas/Contexts/GraphicContext.swift +++ b/Memola/Canvas/Contexts/GraphicContext.swift @@ -60,12 +60,15 @@ final class GraphicContext: @unchecked Sendable { } extension GraphicContext { - func load() { + func loadStrokes() { 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.loadVertices() + _stroke.loadQuads() + withPersistence(\.backgroundContext) { [stroke] context in + context.refresh(stroke, mergeChanges: false) + } return _stroke } } @@ -129,7 +132,6 @@ extension GraphicContext { currentStroke.saveQuads(for: quads) try context.saveIfNeeded() if let stroke = currentStroke.object { - currentStroke.quads.removeAll() context.refresh(stroke, mergeChanges: false) } } diff --git a/Memola/Canvas/Core/Canvas.swift b/Memola/Canvas/Core/Canvas.swift index a2830cf..93805ba 100644 --- a/Memola/Canvas/Core/Canvas.swift +++ b/Memola/Canvas/Core/Canvas.swift @@ -53,7 +53,7 @@ extension Canvas { } let graphicContext = canvas.graphicContext self?.graphicContext.object = graphicContext - self?.graphicContext.load() + self?.graphicContext.loadStrokes() context.refresh(canvas, mergeChanges: false) DispatchQueue.main.async { [weak self] in self?.state = .loaded diff --git a/Memola/Canvas/Core/PipelineStates.swift b/Memola/Canvas/Core/PipelineStates.swift index 7486acd..8082693 100644 --- a/Memola/Canvas/Core/PipelineStates.swift +++ b/Memola/Canvas/Core/PipelineStates.swift @@ -131,4 +131,13 @@ struct PipelineStates { } return try? device.makeComputePipelineState(function: function) } + + static func createQuadPipelineState(from renderer: Renderer) -> MTLComputePipelineState? { + let device = renderer.device + let library = renderer.library + guard let function = library.makeFunction(name: "generate_stroke_vertices") else { + return nil + } + return try? device.makeComputePipelineState(function: function) + } } diff --git a/Memola/Canvas/Geometries/Primitives/Quad.swift b/Memola/Canvas/Geometries/Primitives/Quad.swift index 029fbb0..585cce3 100644 --- a/Memola/Canvas/Geometries/Primitives/Quad.swift +++ b/Memola/Canvas/Geometries/Primitives/Quad.swift @@ -6,71 +6,51 @@ // import CoreData +import MetalKit import Foundation struct Quad { - var originX: CGFloat - var originY: CGFloat - var size: CGFloat - var rotation: CGFloat + var originX: Float + var originY: Float + var size: Float + var rotation: Float var shape: Int16 + var color: vector_float4 init(object: QuadObject) { - self.originX = object.originX - self.originY = object.originY - self.size = object.size - self.rotation = object.rotation + self.originX = object.originX.float + self.originY = object.originY.float + self.size = object.size.float + self.rotation = object.rotation.float self.shape = object.shape + self.color = [ + object.color[0].float, + object.color[1].float, + object.color[2].float, + object.color[3].float + ] } - init(origin: CGPoint, size: CGFloat, rotation: CGFloat, shape: Int16) { - self.originX = origin.x - self.originY = origin.y - self.size = size - self.rotation = rotation + init(origin: CGPoint, size: CGFloat, rotation: CGFloat, shape: Int16, color: [CGFloat]) { + self.originX = origin.x.float + self.originY = origin.y.float + self.size = size.float + self.rotation = rotation.float self.shape = shape - } - - var origin: CGPoint { - get { CGPoint(x: originX, y: originY) } - set { - originX = newValue.x - originY = newValue.y - } - } - - func generateVertices(_ color: [CGFloat]) -> [QuadVertex] { - guard let shape = QuadShape.init(rawValue: shape) else { return [] } - switch shape { - case .rounded: - return generateRoundedQuad(color) - case .squared: - return generateSquaredQuad(color) - } - } - - func generateRoundedQuad(_ color: [CGFloat]) -> [QuadVertex] { - let halfSize = size * 0.5 - return [ - QuadVertex(x: origin.x - halfSize, y: origin.y - halfSize, textCoord: CGPoint(x: 0, y: 0), color: color, origin: origin, rotation: rotation), - QuadVertex(x: origin.x + halfSize, y: origin.y - halfSize, textCoord: CGPoint(x: 1, y: 0), color: color, origin: origin, rotation: rotation), - QuadVertex(x: origin.x - halfSize, y: origin.y + halfSize, textCoord: CGPoint(x: 0, y: 1), color: color, origin: origin, rotation: rotation), - QuadVertex(x: origin.x + halfSize, y: origin.y - halfSize, textCoord: CGPoint(x: 1, y: 0), color: color, origin: origin, rotation: rotation), - QuadVertex(x: origin.x - halfSize, y: origin.y + halfSize, textCoord: CGPoint(x: 0, y: 1), color: color, origin: origin, rotation: rotation), - QuadVertex(x: origin.x + halfSize, y: origin.y + halfSize, textCoord: CGPoint(x: 1, y: 1), color: color, origin: origin, rotation: rotation) - ] - } - - func generateSquaredQuad(_ color: [CGFloat]) -> [QuadVertex] { - let vHalfSize = size * 0.5 - let hHalfSize = size * 0.15 - return [ - QuadVertex(x: origin.x - hHalfSize, y: origin.y - vHalfSize, textCoord: CGPoint(x: 0, y: 0), color: color, origin: origin, rotation: rotation), - QuadVertex(x: origin.x + hHalfSize, y: origin.y - vHalfSize, textCoord: CGPoint(x: 1, y: 0), color: color, origin: origin, rotation: rotation), - QuadVertex(x: origin.x - hHalfSize, y: origin.y + vHalfSize, textCoord: CGPoint(x: 0, y: 1), color: color, origin: origin, rotation: rotation), - QuadVertex(x: origin.x + hHalfSize, y: origin.y - vHalfSize, textCoord: CGPoint(x: 1, y: 0), color: color, origin: origin, rotation: rotation), - QuadVertex(x: origin.x - hHalfSize, y: origin.y + vHalfSize, textCoord: CGPoint(x: 0, y: 1), color: color, origin: origin, rotation: rotation), - QuadVertex(x: origin.x + hHalfSize, y: origin.y + vHalfSize, textCoord: CGPoint(x: 1, y: 1), color: color, origin: origin, rotation: rotation) - ] + self.color = [color[0].float, color[1].float, color[2].float, color[3].float] + } +} + +extension Quad { + var origin: CGPoint { + get { CGPoint(x: originX.cgFloat, y: originY.cgFloat) } + set { + originX = newValue.x.float + originY = newValue.y.float + } + } + + func getColor() -> [CGFloat] { + [color.x.cgFloat, color.y.cgFloat, color.z.cgFloat, color.w.cgFloat] } } diff --git a/Memola/Canvas/Geometries/Stroke/Generators/SolidPointStrokeGenerator.swift b/Memola/Canvas/Geometries/Stroke/Generators/SolidPointStrokeGenerator.swift index 94d8e4a..1fef8b3 100644 --- a/Memola/Canvas/Geometries/Stroke/Generators/SolidPointStrokeGenerator.swift +++ b/Memola/Canvas/Geometries/Stroke/Generators/SolidPointStrokeGenerator.swift @@ -27,7 +27,7 @@ struct SolidPointStrokeGenerator: StrokeGenerator { let control = CGPoint.middle(p1: start, p2: end) addCurve(from: start, to: end, by: control, on: stroke) case 3: - discardVertices(upto: stroke.vertexIndex, quadIndex: stroke.quadIndex, on: stroke) + stroke.removeQuads(from: stroke.quadIndex + 1) let index = stroke.keyPoints.count - 1 var start = stroke.keyPoints[index - 2] var end = CGPoint.middle(p1: stroke.keyPoints[index - 2], p2: stroke.keyPoints[index - 1]) @@ -62,7 +62,7 @@ struct SolidPointStrokeGenerator: StrokeGenerator { } private func smoothOutPath(on stroke: Stroke) { - discardVertices(upto: stroke.vertexIndex, quadIndex: stroke.quadIndex, on: stroke) + stroke.removeQuads(from: stroke.quadIndex + 1) adjustPreviousKeyPoint(on: stroke) switch stroke.keyPoints.count { case 4: @@ -80,7 +80,6 @@ struct SolidPointStrokeGenerator: StrokeGenerator { addCurve(from: start, to: end, by: control, on: stroke) } stroke.quadIndex = stroke.quads.count - 1 - stroke.vertexIndex = stroke.vertices.endIndex - 1 } private func adjustPreviousKeyPoint(on stroke: Stroke) { @@ -107,8 +106,7 @@ struct SolidPointStrokeGenerator: StrokeGenerator { rotation = CGFloat.random(in: 0...360) * .pi / 180 } let quad = stroke.addQuad(at: point, rotation: rotation, shape: .rounded) - stroke.vertices.append(contentsOf: quad.generateVertices(stroke.color)) - stroke.vertexCount = stroke.vertices.endIndex + stroke.quads.append(quad) } private func addCurve(from start: CGPoint, to end: CGPoint, by control: CGPoint, on stroke: Stroke) { @@ -131,17 +129,6 @@ struct SolidPointStrokeGenerator: StrokeGenerator { addPoint(point, on: stroke) } } - - private func discardVertices(upto index: Int, quadIndex: Int, on stroke: Stroke) { - if index < 0 { - stroke.vertices.removeAll() - } else { - let count = stroke.vertices.endIndex - let dropCount = count - (max(0, index) + 1) - stroke.vertices.removeLast(dropCount) - } - stroke.removeQuads(from: quadIndex + 1) - } } extension SolidPointStrokeGenerator { diff --git a/Memola/Canvas/Geometries/Stroke/Stroke.swift b/Memola/Canvas/Geometries/Stroke/Stroke.swift index 883b648..29d7e17 100644 --- a/Memola/Canvas/Geometries/Stroke/Stroke.swift +++ b/Memola/Canvas/Geometries/Stroke/Stroke.swift @@ -41,27 +41,21 @@ final class Stroke: @unchecked Sendable { } var angle: CGFloat = 0 - var penStyle: Style { Style(rawValue: style) ?? .marker } var batchIndex: Int = 0 var quadIndex: Int = -1 - var vertexIndex: Int = -1 var keyPoints: [CGPoint] = [] var thicknessFactor: CGFloat = 0.7 - var vertices: [QuadVertex] = [] var vertexBuffer: MTLBuffer? - var vertexCount: Int = 0 - var texture: MTLTexture? var isEmpty: Bool { - vertices.isEmpty + quads.isEmpty } - var isEraserPenStyle: Bool { penStyle == .eraser } @@ -78,14 +72,15 @@ final class Stroke: @unchecked Sendable { penStyle.anyPenStyle.generator.finish(at: point, on: self) keyPoints.removeAll() } +} - func loadVertices() { +extension Stroke { + func loadQuads() { guard let object else { return } - for quad in object.quads { - guard let quad = quad as? QuadObject else { continue } - vertices.append(contentsOf: Quad(object: quad).generateVertices(object.color)) + quads = object.quads.compactMap { quad in + guard let quad = quad as? QuadObject else { return nil } + return Quad(object: quad) } - vertexCount = vertices.endIndex } func addQuad(at point: CGPoint, rotation: CGFloat, shape: QuadShape) -> Quad { @@ -93,7 +88,8 @@ final class Stroke: @unchecked Sendable { origin: point, size: thickness, rotation: rotation, - shape: shape.rawValue + shape: shape.rawValue, + color: color ) quads.append(quad) return quad @@ -112,11 +108,12 @@ final class Stroke: @unchecked Sendable { func saveQuads(for quads: [Quad]) { for _quad in quads { let quad = QuadObject(\.backgroundContext) - quad.originX = _quad.originX - quad.originY = _quad.originY - quad.size = _quad.size - quad.rotation = _quad.rotation + 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) } @@ -128,7 +125,6 @@ extension Stroke: Drawable { if texture == nil { texture = penStyle.anyPenStyle.loadTexture(on: device) } - vertexBuffer = device.makeBuffer(bytes: &vertices, length: MemoryLayout.stride * vertexCount, options: .cpuCacheModeWriteCombined) } func draw(device: MTLDevice, renderEncoder: MTLRenderCommandEncoder) { @@ -136,7 +132,8 @@ extension Stroke: Drawable { prepare(device: device) renderEncoder.setFragmentTexture(texture, index: 0) renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0) - renderEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: vertexCount) + renderEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: quads.endIndex * 6) + vertexBuffer = nil } } diff --git a/Memola/Canvas/RenderPasses/StrokeRenderPass.swift b/Memola/Canvas/RenderPasses/StrokeRenderPass.swift index d1c5de9..5d33aca 100644 --- a/Memola/Canvas/RenderPasses/StrokeRenderPass.swift +++ b/Memola/Canvas/RenderPasses/StrokeRenderPass.swift @@ -15,6 +15,7 @@ class StrokeRenderPass: RenderPass { weak var graphicDescriptor: MTLRenderPassDescriptor? var strokePipelineState: MTLRenderPipelineState? + var quadPipelineState: MTLComputePipelineState? weak var graphicPipelineState: MTLRenderPipelineState? var stroke: Stroke? @@ -23,6 +24,7 @@ class StrokeRenderPass: RenderPass { init(renderer: Renderer) { descriptor = MTLRenderPassDescriptor() strokePipelineState = PipelineStates.createStrokePipelineState(from: renderer) + quadPipelineState = PipelineStates.createQuadPipelineState(from: renderer) } func resize(on view: MTKView, to size: CGSize, with renderer: Renderer) { @@ -33,6 +35,8 @@ class StrokeRenderPass: RenderPass { func draw(on canvas: Canvas, with renderer: Renderer) { guard let descriptor else { return } + generateVertexBuffer(on: canvas, with: renderer) + guard let strokeTexture else { return } descriptor.colorAttachments[0].texture = strokeTexture descriptor.colorAttachments[0].clearColor = MTLClearColor(red: 1, green: 1, blue: 1, alpha: 0) @@ -50,14 +54,38 @@ class StrokeRenderPass: RenderPass { canvas.setUniformsBuffer(device: renderer.device, renderEncoder: renderEncoder) stroke?.draw(device: renderer.device, renderEncoder: renderEncoder) - renderEncoder.endEncoding() commandBuffer.commit() drawStrokeTexture(on: canvas, with: renderer) } - func drawStrokeTexture(on canvas: Canvas, with renderer: Renderer) { + private func generateVertexBuffer(on canvas: Canvas, with renderer: Renderer) { + guard let stroke, !stroke.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 vertexBuffer = renderer.device.makeBuffer(length: MemoryLayout.stride * quadCount * 6, options: []) + + computeEncoder.setComputePipelineState(quadPipelineState) + computeEncoder.setBuffer(quadBuffer, offset: 0, index: 0) + computeEncoder.setBuffer(vertexBuffer, offset: 0, index: 1) + + stroke.vertexBuffer = vertexBuffer + + let threadsPerGroup = MTLSize(width: 1, height: 1, depth: 1) + let numThreadgroups = MTLSize(width: quadCount + 1, height: 1, depth: 1) + computeEncoder.dispatchThreadgroups(numThreadgroups, threadsPerThreadgroup: threadsPerGroup) + computeEncoder.endEncoding() + quadCommandBuffer.commit() + } + + private func drawStrokeTexture(on canvas: Canvas, with renderer: Renderer) { guard let stroke else { return } guard let graphicDescriptor, let graphicPipelineState else { return } diff --git a/Memola/Canvas/Shaders/Quad.metal b/Memola/Canvas/Shaders/Quad.metal new file mode 100644 index 0000000..8bd9cc8 --- /dev/null +++ b/Memola/Canvas/Shaders/Quad.metal @@ -0,0 +1,54 @@ +// +// Quad.metal +// Memola +// +// Created by Dscyre Scotti on 5/12/24. +// + +#include +using namespace metal; + +struct Quad { + float originX; + float originY; + float size; + float rotation; + int shape; + float4 color; +}; + +struct Vertex { + float4 position; + float2 textCoord; + float4 color; + float2 origin; + float rotation; +}; + +Vertex createVertex(Quad quad, float2 factor, float2 textCoord) { + Vertex output; + float x = quad.originX + factor.x; + float y = quad.originY + factor.y; + output.position = float4(x, y, 0, 1); + output.textCoord = textCoord; + output.color = quad.color; + output.origin = float2(quad.originX, quad.originY); + output.rotation = quad.rotation; + return output; +} + +kernel void generate_stroke_vertices( + device Quad *quads [[buffer(0)]], + device Vertex *vertices [[buffer(1)]], + uint gid [[thread_position_in_grid]] +) { + uint index = gid * 6; + Quad quad = quads[gid]; + float halfSize = quad.size * 0.5; + vertices[index] = createVertex(quad, float2(-halfSize, -halfSize), float2(0, 0)); + vertices[index + 1] = createVertex(quad, float2(halfSize, -halfSize), float2(1, 0)); + vertices[index + 2] = createVertex(quad, float2(-halfSize, halfSize), float2(0, 1)); + vertices[index + 3] = createVertex(quad, float2(halfSize, -halfSize), float2(1, 0)); + vertices[index + 4] = createVertex(quad, float2(-halfSize, halfSize), float2(0, 1)); + vertices[index + 5] = createVertex(quad, float2(halfSize, halfSize), float2(1, 1)); +} diff --git a/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift b/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift index 8f9d5a0..69faa94 100644 --- a/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift +++ b/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift @@ -62,6 +62,9 @@ class CanvasViewController: UIViewController { override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) history.resetRedo() + withPersistence(\.backgroundContext) { context in + context.refreshAllObjects() + } } } diff --git a/Memola/Extensions/Float++.swift b/Memola/Extensions/Float++.swift new file mode 100644 index 0000000..7073bbf --- /dev/null +++ b/Memola/Extensions/Float++.swift @@ -0,0 +1,14 @@ +// +// Float++.swift +// Memola +// +// Created by Dscyre Scotti on 5/12/24. +// + +import Foundation + +extension Float { + var cgFloat: CGFloat { + CGFloat(self) + } +} diff --git a/Memola/Features/Memos/MemosView.swift b/Memola/Features/Memos/MemosView.swift index 57e70aa..1b4f079 100644 --- a/Memola/Features/Memos/MemosView.swift +++ b/Memola/Features/Memos/MemosView.swift @@ -31,6 +31,11 @@ struct MemosView: View { } .fullScreenCover(item: $memo) { memo in MemoView(memo: memo) + .onDisappear { + withPersistence(\.viewContext) { context in + context.refreshAllObjects() + } + } } } diff --git a/Memola/Persistence/Objects/QuadObject.swift b/Memola/Persistence/Objects/QuadObject.swift index f1c051f..636e944 100644 --- a/Memola/Persistence/Objects/QuadObject.swift +++ b/Memola/Persistence/Objects/QuadObject.swift @@ -15,5 +15,6 @@ final class QuadObject: NSManagedObject { @NSManaged var size: CGFloat @NSManaged var rotation: CGFloat @NSManaged var shape: Int16 + @NSManaged var color: [CGFloat] @NSManaged var stroke: StrokeObject? } diff --git a/Memola/Resources/Models/MemolaModel.xcdatamodeld/MemolaModel.xcdatamodel/contents b/Memola/Resources/Models/MemolaModel.xcdatamodeld/MemolaModel.xcdatamodel/contents index 095d1d2..72e5132 100644 --- a/Memola/Resources/Models/MemolaModel.xcdatamodeld/MemolaModel.xcdatamodel/contents +++ b/Memola/Resources/Models/MemolaModel.xcdatamodeld/MemolaModel.xcdatamodel/contents @@ -17,6 +17,7 @@ +