Merge branch 'feature/quad-entity' into feature/memory

# Conflicts:
#	Memola/Canvas/Core/Canvas.swift
#	Memola/Canvas/Geometries/Stroke/Generators/SolidPointStrokeGenerator.swift
#	Memola/Canvas/Geometries/Stroke/Stroke.swift
This commit is contained in:
dscyrescotti
2024-05-10 18:17:07 +07:00
14 changed files with 141 additions and 212 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,37 +5,38 @@
// Created by Dscyre Scotti on 5/8/24.
//
import CoreData
import Foundation
struct Quad: Codable {
var origin: CGPoint
var color: [CGFloat]
var size: CGFloat
var rotation: CGFloat
var shape: QuadShape
@objc(Quad)
class Quad: NSManagedObject {
@NSManaged var id: UUID
@NSManaged var originX: CGFloat
@NSManaged var originY: CGFloat
@NSManaged var size: CGFloat
@NSManaged var rotation: CGFloat
@NSManaged var shape: Int16
@NSManaged var stroke: Stroke?
init(origin: CGPoint, size: CGFloat, color: [CGFloat], rotation: CGFloat, shape: QuadShape = .rounded) {
self.origin = origin
self.size = size
self.color = color
self.rotation = rotation
self.shape = shape
}
func generateVertices() -> [QuadVertex] {
switch shape {
case .rounded:
generateRoundedQuad()
case .squared:
generateSquaredQuad()
case let .calligraphic(vFactor, hFactor):
generateCalligraphicQuad(vFactor: vFactor, hFactor: hFactor)
case let .trapezoid(topFactor, bottomFactor, heightFactor):
generateTrapezoidQuad(topFactor: topFactor, bottomFactor: bottomFactor, heightFactor: heightFactor)
var origin: CGPoint {
get { CGPoint(x: originX, y: originY) }
set {
originX = newValue.x
originY = newValue.y
}
}
func generateRoundedQuad() -> [QuadVertex] {
func generateVertices(_ color: [CGFloat]) -> [QuadVertex] {
guard let shape = QuadShape.init(rawValue: shape) else { return [] }
switch shape {
case .rounded:
return generateRoundedQuad(color)
case .squared:
return generateSquaredQuad(color)
}
}
func generateRoundedQuad(_ color: [CGFloat]) -> [QuadVertex] {
let halfSize = size * 0.5
return [
QuadVertex(x: origin.x - halfSize, y: origin.y - halfSize, textCoord: CGPoint(x: 0, y: 0), color: color, origin: origin, rotation: rotation),
@@ -47,7 +48,7 @@ struct Quad: Codable {
]
}
func generateSquaredQuad() -> [QuadVertex] {
func generateSquaredQuad(_ color: [CGFloat]) -> [QuadVertex] {
let vHalfSize = size * 0.5
let hHalfSize = size * 0.15
return [
@@ -59,38 +60,4 @@ struct Quad: Codable {
QuadVertex(x: origin.x + hHalfSize, y: origin.y + vHalfSize, textCoord: CGPoint(x: 1, y: 1), color: color, origin: origin, rotation: rotation)
]
}
func generateCalligraphicQuad(vFactor: CGFloat, hFactor: CGFloat) -> [QuadVertex] {
let vHalfSize = size * vFactor * 0.5
let hHalfSize = size * hFactor * 0.5
return [
QuadVertex(x: origin.x - hHalfSize, y: origin.y - vHalfSize, textCoord: CGPoint(x: 0, y: 0), color: color, origin: origin, rotation: rotation),
QuadVertex(x: origin.x + hHalfSize, y: origin.y - vHalfSize, textCoord: CGPoint(x: 1, y: 0), color: color, origin: origin, rotation: rotation),
QuadVertex(x: origin.x - hHalfSize, y: origin.y + vHalfSize, textCoord: CGPoint(x: 0, y: 1), color: color, origin: origin, rotation: rotation),
QuadVertex(x: origin.x + hHalfSize, y: origin.y - vHalfSize, textCoord: CGPoint(x: 1, y: 0), color: color, origin: origin, rotation: rotation),
QuadVertex(x: origin.x - hHalfSize, y: origin.y + vHalfSize, textCoord: CGPoint(x: 0, y: 1), color: color, origin: origin, rotation: rotation),
QuadVertex(x: origin.x + hHalfSize, y: origin.y + vHalfSize, textCoord: CGPoint(x: 1, y: 1), color: color, origin: origin, rotation: rotation)
]
}
func generateTrapezoidQuad(topFactor: CGFloat, bottomFactor: CGFloat, heightFactor: CGFloat) -> [QuadVertex] {
let vHalfSize = size * heightFactor * 0.5
let hTopHalfSize = size * topFactor * 0.5
let hBottomHalfSize = size * bottomFactor * 0.5
return [
QuadVertex(x: origin.x - hTopHalfSize, y: origin.y - vHalfSize, textCoord: CGPoint(x: 0, y: 0), color: color, origin: origin, rotation: rotation),
QuadVertex(x: origin.x + hBottomHalfSize, y: origin.y - vHalfSize, textCoord: CGPoint(x: 1, y: 0), color: color, origin: origin, rotation: rotation),
QuadVertex(x: origin.x - hTopHalfSize, y: origin.y + vHalfSize, textCoord: CGPoint(x: 0, y: 1), color: color, origin: origin, rotation: rotation),
QuadVertex(x: origin.x + hBottomHalfSize, y: origin.y - vHalfSize, textCoord: CGPoint(x: 1, y: 0), color: color, origin: origin, rotation: rotation),
QuadVertex(x: origin.x - hTopHalfSize, y: origin.y + vHalfSize, textCoord: CGPoint(x: 0, y: 1), color: color, origin: origin, rotation: rotation),
QuadVertex(x: origin.x + hBottomHalfSize, y: origin.y + vHalfSize, textCoord: CGPoint(x: 1, y: 1), color: color, origin: origin, rotation: rotation)
]
}
}
enum QuadShape: Codable {
case rounded
case squared
case calligraphic(CGFloat, CGFloat)
case trapezoid(topFactor: CGFloat, bottomFactor: CGFloat, heightFactor: CGFloat)
}

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)
addCurve(from: start, to: end, by: control, on: stroke)
case 3:
discardVertices(upto: stroke.vertexIndex, on: stroke)
discardVertices(upto: stroke.vertexIndex, quadIndex: stroke.quadIndex, on: stroke)
let index = stroke.keyPoints.count - 1
var start = stroke.keyPoints[index - 2]
var end = CGPoint.middle(p1: stroke.keyPoints[index - 2], p2: stroke.keyPoints[index - 1])
@@ -62,7 +62,7 @@ struct SolidPointStrokeGenerator: StrokeGenerator {
}
private func smoothOutPath(on stroke: Stroke) {
discardVertices(upto: stroke.vertexIndex, on: stroke)
discardVertices(upto: stroke.vertexIndex, quadIndex: stroke.quadIndex, on: stroke)
adjustPreviousKeyPoint(on: stroke)
switch stroke.keyPoints.count {
case 4:
@@ -79,6 +79,7 @@ struct SolidPointStrokeGenerator: StrokeGenerator {
let end = CGPoint.middle(p1: stroke.keyPoints[index - 1], p2: stroke.keyPoints[index])
addCurve(from: start, to: end, by: control, on: stroke)
}
stroke.quadIndex = stroke.quads.count - 1
stroke.vertexIndex = stroke.vertices.endIndex - 1
}
@@ -105,9 +106,8 @@ struct SolidPointStrokeGenerator: StrokeGenerator {
case .random:
rotation = CGFloat.random(in: 0...360) * .pi / 180
}
let quad = Quad(origin: point, size: stroke.thickness, color: stroke.color, rotation: rotation)
stroke._quads.append(quad)
stroke.vertices.append(contentsOf: quad.generateVertices())
let quad = stroke.addQuad(at: point, rotation: rotation, shape: .rounded)
stroke.vertices.append(contentsOf: quad.generateVertices(stroke.color))
stroke.vertexCount = stroke.vertices.endIndex
}
@@ -116,7 +116,7 @@ struct SolidPointStrokeGenerator: StrokeGenerator {
let factor: CGFloat
switch configuration.granularity {
case .automatic:
factor = min(5, 1 / (stroke.thickness * 10 / 500))
factor = min(5, 1 / (stroke.thickness * 1 / 50))
case .fixed:
factor = 1 / (stroke.thickness * stroke.penStyle.anyPenStyle.stepRate)
case .none:
@@ -145,15 +145,28 @@ struct SolidPointStrokeGenerator: StrokeGenerator {
}
}
private func discardVertices(upto index: Int, on stroke: Stroke) {
private func discardVertices(upto index: Int, quadIndex: Int, on stroke: Stroke) {
if index < 0 {
stroke.vertices.removeAll()
stroke._quads.removeAll()
discardQuads(from: quadIndex + 1, on: stroke)
} else {
let count = stroke.vertices.endIndex
let dropCount = count - (max(0, index) + 1)
stroke.vertices.removeLast(dropCount)
stroke._quads.removeLast(dropCount / 6)
discardQuads(from: quadIndex + 1, on: stroke)
}
}
private func discardQuads(from start: Int, on stroke: Stroke) {
let quads = stroke.quads.array
Persistence.performe { context in
for index in start..<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 createdAt: Date
@NSManaged var thickness: CGFloat
@NSManaged var strokeQuads: Array<StrokeQuad>
@NSManaged var quads: NSMutableOrderedSet
@NSManaged var graphicContext: GraphicContext?
var angle: CGFloat = 0
@@ -31,7 +31,6 @@ final class Stroke: NSManagedObject {
var thicknessFactor: CGFloat = 0.7
var vertices: [QuadVertex] = []
var _quads: [Quad] = []
var vertexBuffer: MTLBuffer?
var vertexCount: Int = 0
@@ -59,14 +58,21 @@ final class Stroke: NSManagedObject {
}
func loadVertices() {
vertices = strokeQuads
.flatMap { $0.quad.generateVertices() }
vertices = quads
.flatMap { ($0 as? Quad)?.generateVertices(color) ?? [] }
vertexCount = vertices.endIndex
}
func saveQuads() {
strokeQuads = _quads.map(StrokeQuad.init)
_quads.removeAll()
func addQuad(at point: CGPoint, rotation: CGFloat, shape: QuadShape) -> Quad {
let quad = Quad(context: Persistence.context)
quad.id = UUID()
quad.origin = point
quad.rotation = rotation
quad.size = thickness
quad.shape = shape.rawValue
quads.add(quad)
quad.stroke = self
return quad
}
}
@@ -79,6 +85,9 @@ extension Stroke: Drawable {
}
func draw(device: MTLDevice, renderEncoder: MTLRenderCommandEncoder) {
if isEmpty {
loadVertices()
}
guard !isEmpty else { return }
prepare(device: device)
renderEncoder.setFragmentTexture(texture, index: 0)

View File

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

View File

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

View File

@@ -13,11 +13,13 @@ class Persistence {
static let shared: Persistence = Persistence()
private init() {
QuadValueTransformer.register()
}
private init() { }
lazy var viewContext: NSManagedObjectContext = {
static var context: NSManagedObjectContext = {
shared.persistentContainer.viewContext
}()
private lazy var viewContext: NSManagedObjectContext = {
persistentContainer.viewContext
}()
@@ -65,4 +67,18 @@ class Persistence {
fatalError("[Memola]: \(error.localizedDescription)")
}
}()
static func performe(_ action: (NSManagedObjectContext) -> Void) {
action(shared.viewContext)
}
static func saveIfNeeded() {
if context.hasChanges {
do {
try context.save()
} catch {
NSLog("[Memola] - \(error.localizedDescription)")
}
}
}
}

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"/>
<relationship name="canvas" maxCount="1" deletionRule="Cascade" destinationEntity="Canvas" inverseName="memo" inverseEntity="Canvas"/>
</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">
<attribute name="color" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromDataTransformer" customClassName="[CGFloat]"/>
<attribute name="createdAt" attributeType="Date" 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="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="quads" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="Quad" inverseName="stroke" inverseEntity="Quad"/>
</entity>
</model>