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