feat: add quad entity instead of storing in array

This commit is contained in:
dscyrescotti
2024-05-10 18:15:30 +07:00
parent 1d91da8445
commit 74297b7627
14 changed files with 142 additions and 209 deletions

View File

@@ -8,7 +8,6 @@
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
EC4538892BEBCAE000A86FEC /* Quad.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC4538882BEBCAE000A86FEC /* Quad.swift */; }; 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 */; }; EC7F6BEC2BE5E6E300A34A7B /* MemolaApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC7F6BEB2BE5E6E300A34A7B /* MemolaApp.swift */; };
EC7F6BF02BE5E6E400A34A7B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = EC7F6BEF2BE5E6E400A34A7B /* Assets.xcassets */; }; EC7F6BF02BE5E6E400A34A7B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = EC7F6BEF2BE5E6E400A34A7B /* Assets.xcassets */; };
EC7F6BF32BE5E6E400A34A7B /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = EC7F6BF22BE5E6E400A34A7B /* Preview 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 */; }; ECA738F22BE6128F00A4542E /* Collection++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738F12BE6128F00A4542E /* Collection++.swift */; };
ECA738F42BE612A000A4542E /* Array++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738F32BE612A000A4542E /* Array++.swift */; }; ECA738F42BE612A000A4542E /* Array++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738F32BE612A000A4542E /* Array++.swift */; };
ECA738F62BE612B700A4542E /* MTLDevice++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738F52BE612B700A4542E /* MTLDevice++.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 */; }; ECA738FC2BE61C5200A4542E /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738FB2BE61C5200A4542E /* Persistence.swift */; };
ECA739012BE61D9C00A4542E /* MemolaModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = ECA738FF2BE61D9C00A4542E /* MemolaModel.xcdatamodeld */; }; ECA739012BE61D9C00A4542E /* MemolaModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = ECA738FF2BE61D9C00A4542E /* MemolaModel.xcdatamodeld */; };
ECA739052BE61E3100A4542E /* Memo.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA739042BE61E3100A4542E /* Memo.swift */; }; ECA739052BE61E3100A4542E /* Memo.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA739042BE61E3100A4542E /* Memo.swift */; };
ECA739082BE623F300A4542E /* PenToolView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA739072BE623F300A4542E /* PenToolView.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 */ /* End PBXBuildFile section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
EC4538882BEBCAE000A86FEC /* Quad.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Quad.swift; sourceTree = "<group>"; }; EC4538882BEBCAE000A86FEC /* Quad.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Quad.swift; sourceTree = "<group>"; };
EC771E5F2BEB6EE50053CC68 /* QuadValueTransformer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuadValueTransformer.swift; sourceTree = "<group>"; };
EC7F6BE82BE5E6E300A34A7B /* Memola.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Memola.app; sourceTree = BUILT_PRODUCTS_DIR; }; 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 = "<group>"; }; EC7F6BEB2BE5E6E300A34A7B /* MemolaApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemolaApp.swift; sourceTree = "<group>"; };
EC7F6BEF2BE5E6E400A34A7B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; }; EC7F6BEF2BE5E6E400A34A7B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
@@ -128,11 +126,11 @@
ECA738F12BE6128F00A4542E /* Collection++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection++.swift"; sourceTree = "<group>"; }; ECA738F12BE6128F00A4542E /* Collection++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection++.swift"; sourceTree = "<group>"; };
ECA738F32BE612A000A4542E /* Array++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array++.swift"; sourceTree = "<group>"; }; ECA738F32BE612A000A4542E /* Array++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array++.swift"; sourceTree = "<group>"; };
ECA738F52BE612B700A4542E /* MTLDevice++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MTLDevice++.swift"; sourceTree = "<group>"; }; ECA738F52BE612B700A4542E /* MTLDevice++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MTLDevice++.swift"; sourceTree = "<group>"; };
ECA738F72BE612EB00A4542E /* StrokeQuad.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StrokeQuad.swift; sourceTree = "<group>"; };
ECA738FB2BE61C5200A4542E /* Persistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = "<group>"; }; ECA738FB2BE61C5200A4542E /* Persistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = "<group>"; };
ECA739002BE61D9C00A4542E /* MemolaModel.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MemolaModel.xcdatamodel; sourceTree = "<group>"; }; ECA739002BE61D9C00A4542E /* MemolaModel.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MemolaModel.xcdatamodel; sourceTree = "<group>"; };
ECA739042BE61E3100A4542E /* Memo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Memo.swift; sourceTree = "<group>"; }; ECA739042BE61E3100A4542E /* Memo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Memo.swift; sourceTree = "<group>"; };
ECA739072BE623F300A4542E /* PenToolView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PenToolView.swift; sourceTree = "<group>"; }; ECA739072BE623F300A4542E /* PenToolView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PenToolView.swift; sourceTree = "<group>"; };
ECEC01A72BEE11BA006DA24C /* QuadShape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuadShape.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@@ -162,14 +160,6 @@
path = ViewController; path = ViewController;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
EC771E5C2BEB37FC0053CC68 /* Transformers */ = {
isa = PBXGroup;
children = (
EC771E5F2BEB6EE50053CC68 /* QuadValueTransformer.swift */,
);
path = Transformers;
sourceTree = "<group>";
};
EC7F6BDF2BE5E6E300A34A7B = { EC7F6BDF2BE5E6E300A34A7B = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -304,8 +294,8 @@
ECA738982BE6015700A4542E /* Primitives */ = { ECA738982BE6015700A4542E /* Primitives */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
ECA738F72BE612EB00A4542E /* StrokeQuad.swift */,
EC4538882BEBCAE000A86FEC /* Quad.swift */, EC4538882BEBCAE000A86FEC /* Quad.swift */,
ECEC01A72BEE11BA006DA24C /* QuadShape.swift */,
); );
path = Primitives; path = Primitives;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -463,7 +453,6 @@
ECA738FA2BE61B1700A4542E /* Persistence */ = { ECA738FA2BE61B1700A4542E /* Persistence */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
EC771E5C2BEB37FC0053CC68 /* Transformers */,
ECA739022BE61DE700A4542E /* Core */, ECA739022BE61DE700A4542E /* Core */,
); );
path = Persistence; path = Persistence;
@@ -572,7 +561,6 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
EC771E602BEB6EE50053CC68 /* QuadValueTransformer.swift in Sources */,
ECA738B02BE60D0B00A4542E /* CanvasViewController.swift in Sources */, ECA738B02BE60D0B00A4542E /* CanvasViewController.swift in Sources */,
ECA738E42BE6110800A4542E /* Drawable.swift in Sources */, ECA738E42BE6110800A4542E /* Drawable.swift in Sources */,
ECA738AD2BE60CC600A4542E /* DrawingView.swift in Sources */, ECA738AD2BE60CC600A4542E /* DrawingView.swift in Sources */,
@@ -594,6 +582,7 @@
ECA738A62BE6023F00A4542E /* GridUniforms.swift in Sources */, ECA738A62BE6023F00A4542E /* GridUniforms.swift in Sources */,
ECA738D72BE60FC100A4542E /* SolidPointStrokeGenerator.swift in Sources */, ECA738D72BE60FC100A4542E /* SolidPointStrokeGenerator.swift in Sources */,
ECA738832BE5FEFE00A4542E /* RenderPass.swift in Sources */, ECA738832BE5FEFE00A4542E /* RenderPass.swift in Sources */,
ECEC01A82BEE11BA006DA24C /* QuadShape.swift in Sources */,
ECA738862BE5FF2500A4542E /* Canvas.swift in Sources */, ECA738862BE5FF2500A4542E /* Canvas.swift in Sources */,
ECA738882BE5FF4400A4542E /* Renderer.swift in Sources */, ECA738882BE5FF4400A4542E /* Renderer.swift in Sources */,
ECA738D42BE60F9100A4542E /* StrokeGenerator.swift in Sources */, ECA738D42BE60F9100A4542E /* StrokeGenerator.swift in Sources */,
@@ -628,7 +617,6 @@
EC7F6BEC2BE5E6E300A34A7B /* MemolaApp.swift in Sources */, EC7F6BEC2BE5E6E300A34A7B /* MemolaApp.swift in Sources */,
ECA738A02BE601E400A4542E /* ViewPortVertex.swift in Sources */, ECA738A02BE601E400A4542E /* ViewPortVertex.swift in Sources */,
ECA738BC2BE60E0300A4542E /* Tool.swift in Sources */, ECA738BC2BE60E0300A4542E /* Tool.swift in Sources */,
ECA738F82BE612EB00A4542E /* StrokeQuad.swift in Sources */,
ECA738972BE6014200A4542E /* Graphic.metal in Sources */, ECA738972BE6014200A4542E /* Graphic.metal in Sources */,
ECA7388A2BE6006A00A4542E /* PipelineStates.swift in Sources */, ECA7388A2BE6006A00A4542E /* PipelineStates.swift in Sources */,
); );

View File

@@ -12,7 +12,7 @@ struct MemolaApp: App {
var body: some Scene { var body: some Scene {
WindowGroup { WindowGroup {
MemosView() MemosView()
.environment(\.managedObjectContext, Persistence.shared.viewContext) .environment(\.managedObjectContext, Persistence.context)
} }
} }
} }

View File

@@ -44,11 +44,7 @@ final class GraphicContext: NSManagedObject {
strokes.remove(stroke) strokes.remove(stroke)
stroke.graphicContext = nil stroke.graphicContext = nil
previousStroke = nil previousStroke = nil
do { Persistence.saveIfNeeded()
try Persistence.shared.viewContext.save()
} catch {
NSLog("[Memola] - \(error.localizedDescription)")
}
} }
func redoGraphic(for event: HistoryEvent) { func redoGraphic(for event: HistoryEvent) {
@@ -58,11 +54,7 @@ final class GraphicContext: NSManagedObject {
stroke.graphicContext = self stroke.graphicContext = self
previousStroke = nil previousStroke = nil
} }
do { Persistence.saveIfNeeded()
try Persistence.shared.viewContext.save()
} catch {
NSLog("[Memola] - \(error.localizedDescription)")
}
} }
} }
@@ -84,13 +76,13 @@ extension GraphicContext: Drawable {
extension GraphicContext { extension GraphicContext {
func beginStroke(at point: CGPoint, pen: Pen) -> Stroke { 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.id = UUID()
stroke.color = pen.color stroke.color = pen.color
stroke.style = pen.strokeStyle.rawValue stroke.style = pen.strokeStyle.rawValue
stroke.thickness = pen.thickness stroke.thickness = pen.thickness
stroke.createdAt = .now stroke.createdAt = .now
stroke.strokeQuads = [] stroke.quads = []
stroke.graphicContext = self stroke.graphicContext = self
strokes.add(stroke) strokes.add(stroke)
currentStroke = stroke currentStroke = stroke
@@ -109,12 +101,7 @@ extension GraphicContext {
func endStroke(at point: CGPoint) { func endStroke(at point: CGPoint) {
guard currentPoint != nil, let currentStroke else { return } guard currentPoint != nil, let currentStroke else { return }
currentStroke.finish(at: point) currentStroke.finish(at: point)
currentStroke.saveQuads() Persistence.saveIfNeeded()
do {
try Persistence.shared.viewContext.save()
} catch {
NSLog("[Memola] - \(error.localizedDescription)")
}
previousStroke = currentStroke previousStroke = currentStroke
self.currentStroke = nil self.currentStroke = nil
self.currentPoint = nil self.currentPoint = nil
@@ -122,14 +109,11 @@ extension GraphicContext {
func cancelStroke() { func cancelStroke() {
if let stroke = strokes.lastObject as? Stroke { if let stroke = strokes.lastObject as? Stroke {
do { Persistence.performe { context in
let viewContext = Persistence.shared.viewContext
strokes.remove(stroke) strokes.remove(stroke)
viewContext.delete(stroke) context.delete(stroke)
try viewContext.save()
} catch {
NSLog("[Memola] - \(error.localizedDescription)")
} }
Persistence.saveIfNeeded()
} }
currentStroke = nil currentStroke = nil
currentPoint = nil currentPoint = nil

View File

@@ -44,22 +44,15 @@ extension Canvas {
func load() { func load() {
state = .loading state = .loading
let start = Date().formatted(.dateTime.minute().second().secondFraction(.fractional(5))) let start = Date().formatted(.dateTime.minute().second().secondFraction(.fractional(5)))
Task(priority: .high) { [start] in for stroke in graphicContext.strokes {
await withTaskGroup(of: Void.self) { taskGroup in if let stroke = stroke as? Stroke {
for stroke in graphicContext.strokes { stroke.loadVertices()
guard let stroke = stroke as? Stroke else { continue } NSLog("[Memola] - \(stroke.quads.count) quads")
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
} }
} }
let end = Date().formatted(.dateTime.minute().second().secondFraction(.fractional(5)))
NSLog("[Memola] - Loaded from \(start) to \(end)")
state = .loaded
} }
} }

View File

@@ -5,37 +5,38 @@
// Created by Dscyre Scotti on 5/8/24. // Created by Dscyre Scotti on 5/8/24.
// //
import CoreData
import Foundation import Foundation
struct Quad: Codable { @objc(Quad)
var origin: CGPoint class Quad: NSManagedObject {
var color: [CGFloat] @NSManaged var id: UUID
var size: CGFloat @NSManaged var originX: CGFloat
var rotation: CGFloat @NSManaged var originY: CGFloat
var shape: QuadShape @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) { var origin: CGPoint {
self.origin = origin get { CGPoint(x: originX, y: originY) }
self.size = size set {
self.color = color originX = newValue.x
self.rotation = rotation originY = newValue.y
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)
} }
} }
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 let halfSize = size * 0.5
return [ 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: 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 vHalfSize = size * 0.5
let hHalfSize = size * 0.15 let hHalfSize = size * 0.15
return [ 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) 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)
} }

View File

@@ -0,0 +1,13 @@
//
// QuadShape.swift
// Memola
//
// Created by Dscyre Scotti on 5/10/24.
//
import Foundation
enum QuadShape: Int16 {
case rounded
case squared
}

View File

@@ -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
}
}

View File

@@ -27,7 +27,7 @@ struct SolidPointStrokeGenerator: StrokeGenerator {
let control = CGPoint.middle(p1: start, p2: end) let control = CGPoint.middle(p1: start, p2: end)
addCurve(from: start, to: end, by: control, on: stroke) addCurve(from: start, to: end, by: control, on: stroke)
case 3: case 3:
discardVertices(upto: stroke.vertexIndex, on: stroke) discardVertices(upto: stroke.vertexIndex, quadIndex: stroke.quadIndex, on: stroke)
let index = stroke.keyPoints.count - 1 let index = stroke.keyPoints.count - 1
var start = stroke.keyPoints[index - 2] var start = stroke.keyPoints[index - 2]
var end = CGPoint.middle(p1: stroke.keyPoints[index - 2], p2: stroke.keyPoints[index - 1]) 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) { private func smoothOutPath(on stroke: Stroke) {
discardVertices(upto: stroke.vertexIndex, on: stroke) discardVertices(upto: stroke.vertexIndex, quadIndex: stroke.quadIndex, on: stroke)
adjustPreviousKeyPoint(on: stroke) adjustPreviousKeyPoint(on: stroke)
switch stroke.keyPoints.count { switch stroke.keyPoints.count {
case 4: case 4:
@@ -79,6 +79,7 @@ struct SolidPointStrokeGenerator: StrokeGenerator {
let end = CGPoint.middle(p1: stroke.keyPoints[index - 1], p2: stroke.keyPoints[index]) let end = CGPoint.middle(p1: stroke.keyPoints[index - 1], p2: stroke.keyPoints[index])
addCurve(from: start, to: end, by: control, on: stroke) addCurve(from: start, to: end, by: control, on: stroke)
} }
stroke.quadIndex = stroke.quads.count - 1
stroke.vertexIndex = stroke.vertices.endIndex - 1 stroke.vertexIndex = stroke.vertices.endIndex - 1
} }
@@ -105,9 +106,8 @@ struct SolidPointStrokeGenerator: StrokeGenerator {
case .random: case .random:
rotation = CGFloat.random(in: 0...360) * .pi / 180 rotation = CGFloat.random(in: 0...360) * .pi / 180
} }
let quad = Quad(origin: point, size: stroke.thickness, color: stroke.color, rotation: rotation) let quad = stroke.addQuad(at: point, rotation: rotation, shape: .rounded)
stroke._quads.append(quad) stroke.vertices.append(contentsOf: quad.generateVertices(stroke.color))
stroke.vertices.append(contentsOf: quad.generateVertices())
stroke.vertexCount = stroke.vertices.endIndex stroke.vertexCount = stroke.vertices.endIndex
} }
@@ -116,7 +116,7 @@ struct SolidPointStrokeGenerator: StrokeGenerator {
let factor: CGFloat let factor: CGFloat
switch configuration.granularity { switch configuration.granularity {
case .automatic: case .automatic:
factor = min(3.5, 1 / (stroke.thickness * 1 / 10)) factor = min(5, 1 / (stroke.thickness * 1 / 50))
case .fixed: case .fixed:
factor = 1 / (stroke.thickness * stroke.penStyle.anyPenStyle.stepRate) factor = 1 / (stroke.thickness * stroke.penStyle.anyPenStyle.stepRate)
case .none: 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 { if index < 0 {
stroke.vertices.removeAll() stroke.vertices.removeAll()
stroke._quads.removeAll() discardQuads(from: quadIndex + 1, on: stroke)
} else { } else {
let count = stroke.vertices.endIndex let count = stroke.vertices.endIndex
let dropCount = count - (max(0, index) + 1) let dropCount = count - (max(0, index) + 1)
stroke.vertices.removeLast(dropCount) 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..<quads.count {
if let quad = quads[index] as? Quad {
quad.stroke = nil
context.delete(quad)
stroke.quads.remove(quad)
}
}
} }
} }
} }

View File

@@ -16,7 +16,7 @@ final class Stroke: NSManagedObject {
@NSManaged var style: Int16 @NSManaged var style: Int16
@NSManaged var createdAt: Date @NSManaged var createdAt: Date
@NSManaged var thickness: CGFloat @NSManaged var thickness: CGFloat
@NSManaged var strokeQuads: Array<StrokeQuad> @NSManaged var quads: NSMutableOrderedSet
@NSManaged var graphicContext: GraphicContext? @NSManaged var graphicContext: GraphicContext?
var angle: CGFloat = 0 var angle: CGFloat = 0
@@ -31,7 +31,6 @@ final class Stroke: NSManagedObject {
var thicknessFactor: CGFloat = 0.7 var thicknessFactor: CGFloat = 0.7
var vertices: [QuadVertex] = [] var vertices: [QuadVertex] = []
var _quads: [Quad] = []
var vertexBuffer: MTLBuffer? var vertexBuffer: MTLBuffer?
var vertexCount: Int = 0 var vertexCount: Int = 0
@@ -56,18 +55,25 @@ final class Stroke: NSManagedObject {
func finish(at point: CGPoint) { func finish(at point: CGPoint) {
penStyle.anyPenStyle.generator.finish(at: point, on: self) penStyle.anyPenStyle.generator.finish(at: point, on: self)
keyPoints.removeAll() keyPoints.removeAll()
NSLog("[Memola] - \(_quads.count) quads") NSLog("[Memola] - \(quads.count) quads")
} }
func loadVertices() { func loadVertices() {
vertices = strokeQuads vertices = quads
.flatMap { $0.quad.generateVertices() } .flatMap { ($0 as? Quad)?.generateVertices(color) ?? [] }
vertexCount = vertices.endIndex vertexCount = vertices.endIndex
} }
func saveQuads() { func addQuad(at point: CGPoint, rotation: CGFloat, shape: QuadShape) -> Quad {
strokeQuads = _quads.map(StrokeQuad.init) let quad = Quad(context: Persistence.context)
_quads.removeAll() 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) { func draw(device: MTLDevice, renderEncoder: MTLRenderCommandEncoder) {
if isEmpty {
loadVertices()
}
guard !isEmpty else { return } guard !isEmpty else { return }
prepare(device: device) prepare(device: device)
renderEncoder.setFragmentTexture(texture, index: 0) renderEncoder.setFragmentTexture(texture, index: 0)

View File

@@ -49,6 +49,15 @@ class History: ObservableObject {
func resetRedo() { func resetRedo() {
redoCache = redoStack redoCache = redoStack
for event in redoStack {
switch event {
case .stroke(let stroke):
Persistence.performe { context in
context.delete(stroke)
}
}
}
Persistence.saveIfNeeded()
redoStack.removeAll() redoStack.removeAll()
} }

View File

@@ -61,7 +61,10 @@ class CanvasViewController: UIViewController {
override func viewDidDisappear(_ animated: Bool) { override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated) super.viewDidDisappear(animated)
Persistence.shared.viewContext.refresh(canvas, mergeChanges: false) history.resetRedo()
Persistence.performe { context in
context.refresh(canvas, mergeChanges: false)
}
} }
} }

View File

@@ -13,11 +13,13 @@ class Persistence {
static let shared: Persistence = Persistence() static let shared: Persistence = Persistence()
private init() { private init() { }
QuadValueTransformer.register()
}
lazy var viewContext: NSManagedObjectContext = { static var context: NSManagedObjectContext = {
shared.persistentContainer.viewContext
}()
private lazy var viewContext: NSManagedObjectContext = {
persistentContainer.viewContext persistentContainer.viewContext
}() }()
@@ -65,4 +67,18 @@ class Persistence {
fatalError("[Memola]: \(error.localizedDescription)") 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)")
}
}
}
} }

View File

@@ -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)
}
}

View File

@@ -19,13 +19,22 @@
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/> <attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
<relationship name="canvas" maxCount="1" deletionRule="Cascade" destinationEntity="Canvas" inverseName="memo" inverseEntity="Canvas"/> <relationship name="canvas" maxCount="1" deletionRule="Cascade" destinationEntity="Canvas" inverseName="memo" inverseEntity="Canvas"/>
</entity> </entity>
<entity name="Quad" representedClassName="Quad" syncable="YES">
<attribute name="id" attributeType="UUID" usesScalarValueType="NO"/>
<attribute name="originX" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="originY" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="rotation" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="shape" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="size" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
<relationship name="stroke" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Stroke" inverseName="quads" inverseEntity="Stroke"/>
</entity>
<entity name="Stroke" representedClassName="Stroke" syncable="YES"> <entity name="Stroke" representedClassName="Stroke" syncable="YES">
<attribute name="color" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromDataTransformer" customClassName="[CGFloat]"/> <attribute name="color" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromDataTransformer" customClassName="[CGFloat]"/>
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/> <attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="id" attributeType="UUID" usesScalarValueType="NO"/> <attribute name="id" attributeType="UUID" usesScalarValueType="NO"/>
<attribute name="strokeQuads" attributeType="Transformable" valueTransformerName="QuadValueTransformer" customClassName="[StrokeQuad]"/>
<attribute name="style" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/> <attribute name="style" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="thickness" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/> <attribute name="thickness" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
<relationship name="graphicContext" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="GraphicContext" inverseName="strokes" inverseEntity="GraphicContext"/> <relationship name="graphicContext" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="GraphicContext" inverseName="strokes" inverseEntity="GraphicContext"/>
<relationship name="quads" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="Quad" inverseName="stroke" inverseEntity="Quad"/>
</entity> </entity>
</model> </model>