diff --git a/Memola.xcodeproj/project.pbxproj b/Memola.xcodeproj/project.pbxproj index 9ba2c66..7401d7e 100644 --- a/Memola.xcodeproj/project.pbxproj +++ b/Memola.xcodeproj/project.pbxproj @@ -7,6 +7,12 @@ 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 */; }; + 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 */; }; EC7F6BF32BE5E6E400A34A7B /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = EC7F6BF22BE5E6E400A34A7B /* Preview Assets.xcassets */; }; @@ -61,14 +67,24 @@ 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 */; }; 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 */ + 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 = ""; }; EC7F6BEF2BE5E6E400A34A7B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -124,11 +140,15 @@ 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 = ""; }; 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 */ @@ -284,6 +304,7 @@ ECA738922BE6011100A4542E /* Stroke.metal */, ECA738942BE6012D00A4542E /* ViewPort.metal */, ECA738962BE6014200A4542E /* Graphic.metal */, + EC3565592BF060D900A4E0BF /* Quad.metal */, ); path = Shaders; sourceTree = ""; @@ -291,7 +312,8 @@ ECA738982BE6015700A4542E /* Primitives */ = { isa = PBXGroup; children = ( - ECA738F72BE612EB00A4542E /* Quad.swift */, + EC4538882BEBCAE000A86FEC /* Quad.swift */, + ECEC01A72BEE11BA006DA24C /* QuadShape.swift */, ); path = Primitives; sourceTree = ""; @@ -328,6 +350,10 @@ ECA738E72BE6120F00A4542E /* Color++.swift */, ECA738F52BE612B700A4542E /* MTLDevice++.swift */, ECA738ED2BE6125D00A4542E /* simd_float4x4++.swift */, + EC3565512BEFC65F00A4E0BF /* NSManagedObjectContext++.swift */, + EC3565532BEFC6AD00A4E0BF /* View++.swift */, + EC3565552BEFC7B300A4E0BF /* NSManagedObject++.swift */, + EC35655B2BF0712A00A4E0BF /* Float++.swift */, ); path = Extensions; sourceTree = ""; @@ -449,7 +475,7 @@ ECA738FA2BE61B1700A4542E /* Persistence */ = { isa = PBXGroup; children = ( - ECA739032BE61E2600A4542E /* Entities */, + ECFA151E2BEF21BE00455818 /* Objects */, ECA739022BE61DE700A4542E /* Core */, ); path = Persistence; @@ -479,14 +505,6 @@ path = Core; sourceTree = ""; }; - ECA739032BE61E2600A4542E /* Entities */ = { - isa = PBXGroup; - children = ( - ECA739042BE61E3100A4542E /* Memo.swift */, - ); - path = Entities; - sourceTree = ""; - }; ECA739062BE61F7500A4542E /* Core */ = { isa = PBXGroup; children = ( @@ -496,6 +514,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 */ @@ -570,23 +600,30 @@ 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 */, 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 */, + EC3565522BEFC65F00A4E0BF /* NSManagedObjectContext++.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 */, 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 */, ECA738882BE5FF4400A4542E /* Renderer.swift in Sources */, ECA738D42BE60F9100A4542E /* StrokeGenerator.swift in Sources */, @@ -598,11 +635,14 @@ ECA738952BE6012D00A4542E /* ViewPort.metal in Sources */, 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 */, + EC35655C2BF0712A00A4E0BF /* Float++.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 */, ECA738F62BE612B700A4542E /* MTLDevice++.swift in Sources */, @@ -610,6 +650,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 */, @@ -620,7 +661,6 @@ EC7F6BEC2BE5E6E300A34A7B /* MemolaApp.swift in Sources */, ECA738A02BE601E400A4542E /* ViewPortVertex.swift in Sources */, ECA738BC2BE60E0300A4542E /* Tool.swift in Sources */, - ECA738F82BE612EB00A4542E /* Quad.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/App/MemolaApp.swift b/Memola/App/MemolaApp.swift index 119dd03..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.shared.viewContext) + .persistence(\.viewContext) } } } diff --git a/Memola/Canvas/Contexts/GraphicContext.swift b/Memola/Canvas/Contexts/GraphicContext.swift index 39d5e24..56a8ff1 100644 --- a/Memola/Canvas/Contexts/GraphicContext.swift +++ b/Memola/Canvas/Contexts/GraphicContext.swift @@ -7,45 +7,25 @@ import Combine import MetalKit +import CoreData import Foundation -protocol GraphicContextDelegate: AnyObject { - var didUpdate: PassthroughSubject { get set } -} - -class GraphicContext: Codable { +final class GraphicContext: @unchecked Sendable { var strokes: [Stroke] = [] + var object: GraphicContextObject? + 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() { 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 = [ @@ -58,18 +38,39 @@ class GraphicContext: Codable { func undoGraphic() { guard !strokes.isEmpty else { return } - strokes.removeLast() + let stroke = strokes.removeLast() + withPersistence(\.backgroundContext) { [stroke] context in + stroke.object?.graphicContext = nil + try context.saveIfNeeded() + } previousStroke = nil - delegate?.didUpdate.send() } func redoGraphic(for event: HistoryEvent) { switch event { case .stroke(let stroke): strokes.append(stroke) + withPersistence(\.backgroundContext) { [weak self, stroke] context in + stroke.object?.graphicContext = self?.object + try context.saveIfNeeded() + } previousStroke = nil } - delegate?.didUpdate.send() + } +} + +extension GraphicContext { + 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.loadQuads() + withPersistence(\.backgroundContext) { [stroke] context in + context.refresh(stroke, mergeChanges: false) + } + return _stroke + } } } @@ -93,9 +94,21 @@ extension GraphicContext { func beginStroke(at point: CGPoint, pen: Pen) -> Stroke { let stroke = Stroke( color: pen.color, - style: pen.style, + style: pen.strokeStyle.rawValue, + createdAt: .now, thickness: pen.thickness ) + withPersistence(\.backgroundContext) { [graphicContext = object, _stroke = stroke] context in + let stroke = StrokeObject(\.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 @@ -105,23 +118,38 @@ 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) + let saveIndex = currentStroke.batchIndex + let quads = Array(currentStroke.quads[saveIndex.. GraphicContext)? + init(size: CGSize, canvasID: NSManagedObjectID) { + self.size = size + self.canvasID = canvasID + } @Published var state: State = .initial - lazy var didUpdate = PassthroughSubject() var hasValidStroke: Bool { if let currentStroke = graphicContext.currentStroke { @@ -38,65 +39,27 @@ final class Canvas: NSObject, ObservableObject, Identifiable, Codable, GraphicCo } 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 + withPersistence(\.backgroundContext) { [weak self, canvasID] context in + DispatchQueue.main.async { [weak self] in + 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 - } + guard let canvas = context.object(with: canvasID) as? CanvasObject else { + return + } + let graphicContext = canvas.graphicContext + self?.graphicContext.object = graphicContext + self?.graphicContext.loadStrokes() + context.refresh(canvas, mergeChanges: false) + DispatchQueue.main.async { [weak self] in + self?.state = .loaded } } } - - 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) -// } -// } - } } // MARK: - Dimension 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 fda3f2d..585cce3 100644 --- a/Memola/Canvas/Geometries/Primitives/Quad.swift +++ b/Memola/Canvas/Geometries/Primitives/Quad.swift @@ -2,105 +2,55 @@ // Quad.swift // Memola // -// Created by Dscyre Scotti on 5/4/24. +// Created by Dscyre Scotti on 5/8/24. // +import CoreData import MetalKit import Foundation -class Quad { - var origin: CGPoint - var color: [CGFloat] - var size: CGFloat - var rotation: CGFloat - var vertices: [QuadVertex] = [] +struct Quad { + var originX: Float + var originY: Float + var size: Float + var rotation: Float + var shape: Int16 + var color: vector_float4 - var vertexBuffer: MTLBuffer? - var vertexCount: Int = 0 - - init(origin: CGPoint, size: CGFloat, color: [CGFloat], rotation: CGFloat, shape: Shape = .rounded) { - self.origin = origin - self.size = size - self.color = color - self.rotation = rotation - generateVertices(shape) - } - - func generateVertices(_ shape: Shape) { - 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) - } - } - - func generateRoundedQuad() { - let halfSize = size * 0.5 - vertices = [ - 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) + init(object: QuadObject) { + 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 ] - vertexCount = vertices.count } - func generateSquaredQuad() { - let vHalfSize = size * 0.5 - let hHalfSize = size * 0.15 - vertices = [ - 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) - ] - vertexCount = vertices.count - } - - func generateCalligraphicQuad(vFactor: CGFloat, hFactor: CGFloat) { - let vHalfSize = size * vFactor * 0.5 - let hHalfSize = size * hFactor * 0.5 - vertices = [ - 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) - ] - vertexCount = vertices.count - } - - func generateTrapezoidQuad(topFactor: CGFloat, bottomFactor: CGFloat, heightFactor: CGFloat) { - let vHalfSize = size * heightFactor * 0.5 - let hTopHalfSize = size * topFactor * 0.5 - let hBottomHalfSize = size * bottomFactor * 0.5 - vertices = [ - 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) - ] - vertexCount = vertices.count + 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 + self.color = [color[0].float, color[1].float, color[2].float, color[3].float] } } extension Quad { - enum Shape { - case rounded - case squared - case calligraphic(CGFloat, CGFloat) - case trapezoid(topFactor: CGFloat, bottomFactor: CGFloat, heightFactor: CGFloat) + 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/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/Stroke/Generators/SolidPointStrokeGenerator.swift b/Memola/Canvas/Geometries/Stroke/Generators/SolidPointStrokeGenerator.swift index 7a06d70..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: - discardPoints(upto: stroke.vertexIndex, 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) { - discardPoints(upto: stroke.vertexIndex, on: stroke) + stroke.removeQuads(from: stroke.quadIndex + 1) adjustPreviousKeyPoint(on: stroke) switch stroke.keyPoints.count { case 4: @@ -79,7 +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.vertexIndex = stroke.vertices.endIndex - 1 + stroke.quadIndex = stroke.quads.count - 1 } private func adjustPreviousKeyPoint(on stroke: Stroke) { @@ -105,9 +105,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.vertices.append(contentsOf: quad.vertices) - stroke.vertexCount = stroke.vertices.endIndex + let quad = stroke.addQuad(at: point, rotation: rotation, shape: .rounded) + stroke.quads.append(quad) } private func addCurve(from start: CGPoint, to end: CGPoint, by control: CGPoint, on stroke: Stroke) { @@ -115,9 +114,9 @@ struct SolidPointStrokeGenerator: StrokeGenerator { let factor: CGFloat switch configuration.granularity { case .automatic: - factor = min(6, 1 / (stroke.thickness * 10 / 500)) + factor = min(5, 1 / (stroke.thickness * 1 / 50)) 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) } @@ -130,29 +129,6 @@ struct SolidPointStrokeGenerator: StrokeGenerator { addPoint(point, on: stroke) } } - - #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) - for i in 0.. Quad { + let quad = Quad( + origin: point, + size: thickness, + rotation: rotation, + shape: shape.rawValue, + color: color + ) + 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...stride * vertexCount, options: .cpuCacheModeWriteCombined) } func draw(device: MTLDevice, renderEncoder: MTLRenderCommandEncoder) { @@ -107,6 +132,23 @@ 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 + } +} + +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/History/History.swift b/Memola/Canvas/History/History.swift index 87f6911..36590dd 100644 --- a/Memola/Canvas/History/History.swift +++ b/Memola/Canvas/History/History.swift @@ -49,6 +49,17 @@ class History: ObservableObject { func resetRedo() { redoCache = redoStack + for event in redoStack { + switch event { + case .stroke(let _stroke): + withPersistence(\.backgroundContext) { context in + if let stroke = _stroke.object { + context.delete(stroke) + } + try context.saveIfNeeded() + } + } + } redoStack.removeAll() } diff --git a/Memola/Canvas/RenderPasses/CacheRenderPass.swift b/Memola/Canvas/RenderPasses/CacheRenderPass.swift index 5b3ba3e..ebc4210 100644 --- a/Memola/Canvas/RenderPasses/CacheRenderPass.swift +++ b/Memola/Canvas/RenderPasses/CacheRenderPass.swift @@ -44,7 +44,8 @@ class CacheRenderPass: RenderPass { descriptor.colorAttachments[0].loadAction = clearsTexture ? .clear : .load descriptor.colorAttachments[0].storeAction = .store - if let stroke = canvas.graphicContext.currentStroke { + let graphicContext = canvas.graphicContext + if let stroke = graphicContext.currentStroke { if stroke.isEraserPenStyle { eraserRenderPass.stroke = stroke eraserRenderPass.descriptor = descriptor diff --git a/Memola/Canvas/RenderPasses/GraphicRenderPass.swift b/Memola/Canvas/RenderPasses/GraphicRenderPass.swift index cdb4099..fd685de 100644 --- a/Memola/Canvas/RenderPasses/GraphicRenderPass.swift +++ b/Memola/Canvas/RenderPasses/GraphicRenderPass.swift @@ -36,13 +36,13 @@ class GraphicRenderPass: RenderPass { guard let descriptor else { return } guard let graphicPipelineState else { return } - let graphicContext = canvas.graphicContext guard let graphicTexture else { return } descriptor.colorAttachments[0].texture = graphicTexture descriptor.colorAttachments[0].clearColor = MTLClearColor(red: 1, green: 1, blue: 1, alpha: 0) descriptor.colorAttachments[0].storeAction = .store + let graphicContext = canvas.graphicContext if renderer.redrawsGraphicRender { canvas.setGraphicRenderType(.finished) for stroke in graphicContext.strokes { 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/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/Bridge/ViewController/CanvasViewController.swift b/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift index 5a3907a..69faa94 100644 --- a/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift +++ b/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift @@ -58,6 +58,14 @@ class CanvasViewController: UIViewController { renderView.draw() drawingView.enableUserInteraction() } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + history.resetRedo() + withPersistence(\.backgroundContext) { context in + context.refreshAllObjects() + } + } } extension CanvasViewController { diff --git a/Memola/Canvas/View/Bridge/Views/DrawingView.swift b/Memola/Canvas/View/Bridge/Views/DrawingView.swift index f47077b..0b798e2 100644 --- a/Memola/Canvas/View/Bridge/Views/DrawingView.swift +++ b/Memola/Canvas/View/Bridge/Views/DrawingView.swift @@ -32,7 +32,7 @@ class DrawingView: UIView { } func updateDrawableSize(with size: CGSize) { - renderView.drawableSize = size.multiply(by: 2.5) + renderView.drawableSize = size.multiply(by: 2.0) } override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { 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/Extensions/NSManagedObject++.swift b/Memola/Extensions/NSManagedObject++.swift new file mode 100644 index 0000000..a12b0f5 --- /dev/null +++ b/Memola/Extensions/NSManagedObject++.swift @@ -0,0 +1,15 @@ +// +// NSManagedObject++.swift +// Memola +// +// Created by Dscyre Scotti on 5/11/24. +// + +import CoreData +import Foundation + +extension NSManagedObject { + convenience init(_ keyPath: KeyPath) { + 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/Features/Memo/MemoView.swift b/Memola/Features/Memo/MemoView.swift index b8fe602..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() @@ -53,9 +60,6 @@ struct MemoView: View { .environmentObject(tool) .environmentObject(canvas) .environmentObject(history) - .task { - canvas.listen(on: managedObjectContext) - } } var historyTool: some View { @@ -91,15 +95,14 @@ 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() + history.resetRedo() + 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..1b4f079 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 canvas: Canvas? + @State var memo: MemoObject? var body: some View { NavigationStack { @@ -29,16 +29,20 @@ struct MemosView: View { } } } - .fullScreenCover(item: $canvas) { canvas in - MemoView() - .environmentObject(canvas) + .fullScreenCover(item: $memo) { memo in + MemoView(memo: memo) + .onDisappear { + withPersistence(\.viewContext) { context in + context.refreshAllObjects() + } + } } } var memoGrid: some View { ScrollView { LazyVGrid(columns: .init(repeating: GridItem(.flexible()), count: 3)) { - ForEach(memos) { memo in + ForEach(memoObjects) { memo in memoCard(memo) } } @@ -46,42 +50,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 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 memoObject = MemoObject(context: managedObjectContext) + memoObject.title = title + memoObject.createdAt = .now + memoObject.updatedAt = .now + + let canvasObject = CanvasObject(context: managedObjectContext) + canvasObject.width = 4_000 + canvasObject.height = 4_000 + + let graphicContextObject = GraphicContextObject(context: managedObjectContext) + graphicContextObject.strokes = [] + + memoObject.canvas = canvasObject + canvasObject.memo = memoObject + canvasObject.graphicContext = graphicContextObject + graphicContextObject.canvas = canvasObject try managedObjectContext.save() - openMemo(for: memo) + openMemo(for: memoObject) } 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)") - } + func openMemo(for memo: MemoObject) { + self.memo = memo } } diff --git a/Memola/Persistence/Core/Persistence.swift b/Memola/Persistence/Core/Persistence.swift index 4dee7f9..4cb4a9a 100644 --- a/Memola/Persistence/Core/Persistence.swift +++ b/Memola/Persistence/Core/Persistence.swift @@ -8,16 +8,23 @@ import CoreData import Foundation -class Persistence { +final class Persistence { private let modelName = "MemolaModel" static let shared: Persistence = Persistence() private init() { } - 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() @@ -64,3 +71,15 @@ class Persistence { } }() } + +// MARK: - Global Method +func withPersistence(_ keypath: KeyPath, _ task: @escaping (NSManagedObjectContext) throws -> Void) { + let context = Persistence.shared[keyPath: keypath] + context.perform { + do { + try task(context) + } catch { + NSLog("[Memola] - \(error.localizedDescription)") + } + } +} 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/Entities/Memo.swift b/Memola/Persistence/Objects/MemoObject.swift similarity index 52% rename from Memola/Persistence/Entities/Memo.swift rename to Memola/Persistence/Objects/MemoObject.swift index 3ea1cad..d3a6b61 100644 --- a/Memola/Persistence/Entities/Memo.swift +++ b/Memola/Persistence/Objects/MemoObject.swift @@ -1,20 +1,18 @@ // -// Memo.swift +// MemoObject.swift // Memola // -// Created by Dscyre Scotti on 5/4/24. +// Created by Dscyre Scotti on 5/11/24. // import CoreData import Foundation -@objc(Memo) -class Memo: NSManagedObject { - @NSManaged var id: UUID +@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 } - -extension Memo: Identifiable { } diff --git a/Memola/Persistence/Objects/QuadObject.swift b/Memola/Persistence/Objects/QuadObject.swift new file mode 100644 index 0000000..636e944 --- /dev/null +++ b/Memola/Persistence/Objects/QuadObject.swift @@ -0,0 +1,20 @@ +// +// 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 color: [CGFloat] + @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 bc23036..72e5132 100644 --- a/Memola/Resources/Models/MemolaModel.xcdatamodeld/MemolaModel.xcdatamodel/contents +++ b/Memola/Resources/Models/MemolaModel.xcdatamodeld/MemolaModel.xcdatamodel/contents @@ -1,10 +1,36 @@ - - + + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + \ No newline at end of file