mirror of
https://github.com/dscyrescotti/Memola.git
synced 2026-05-11 02:10:05 +02:00
feat: execute core data related logic in background context
This commit is contained in:
@@ -64,9 +64,13 @@
|
||||
ECA738F62BE612B700A4542E /* MTLDevice++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738F52BE612B700A4542E /* MTLDevice++.swift */; };
|
||||
ECA738FC2BE61C5200A4542E /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738FB2BE61C5200A4542E /* Persistence.swift */; };
|
||||
ECA739012BE61D9C00A4542E /* MemolaModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = ECA738FF2BE61D9C00A4542E /* MemolaModel.xcdatamodeld */; };
|
||||
ECA739052BE61E3100A4542E /* Memo.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA739042BE61E3100A4542E /* Memo.swift */; };
|
||||
ECA739082BE623F300A4542E /* PenToolView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA739072BE623F300A4542E /* PenToolView.swift */; };
|
||||
ECEC01A82BEE11BA006DA24C /* QuadShape.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECEC01A72BEE11BA006DA24C /* QuadShape.swift */; };
|
||||
ECFA15202BEF21EF00455818 /* MemoObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECFA151F2BEF21EF00455818 /* MemoObject.swift */; };
|
||||
ECFA15222BEF21F500455818 /* CanvasObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECFA15212BEF21F500455818 /* CanvasObject.swift */; };
|
||||
ECFA15242BEF223300455818 /* GraphicContextObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECFA15232BEF223300455818 /* GraphicContextObject.swift */; };
|
||||
ECFA15262BEF224900455818 /* StrokeObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECFA15252BEF224900455818 /* StrokeObject.swift */; };
|
||||
ECFA15282BEF225000455818 /* QuadObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECFA15272BEF225000455818 /* QuadObject.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
@@ -128,9 +132,13 @@
|
||||
ECA738F52BE612B700A4542E /* MTLDevice++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MTLDevice++.swift"; sourceTree = "<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>"; };
|
||||
ECFA151F2BEF21EF00455818 /* MemoObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoObject.swift; sourceTree = "<group>"; };
|
||||
ECFA15212BEF21F500455818 /* CanvasObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CanvasObject.swift; sourceTree = "<group>"; };
|
||||
ECFA15232BEF223300455818 /* GraphicContextObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphicContextObject.swift; sourceTree = "<group>"; };
|
||||
ECFA15252BEF224900455818 /* StrokeObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StrokeObject.swift; sourceTree = "<group>"; };
|
||||
ECFA15272BEF225000455818 /* QuadObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuadObject.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -226,7 +234,6 @@
|
||||
ECA7387B2BE5EF3500A4542E /* Memo */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ECA739042BE61E3100A4542E /* Memo.swift */,
|
||||
ECA7387C2BE5EF4B00A4542E /* MemoView.swift */,
|
||||
ECA739072BE623F300A4542E /* PenToolView.swift */,
|
||||
);
|
||||
@@ -453,6 +460,7 @@
|
||||
ECA738FA2BE61B1700A4542E /* Persistence */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ECFA151E2BEF21BE00455818 /* Objects */,
|
||||
ECA739022BE61DE700A4542E /* Core */,
|
||||
);
|
||||
path = Persistence;
|
||||
@@ -491,6 +499,18 @@
|
||||
path = Core;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
ECFA151E2BEF21BE00455818 /* Objects */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ECFA151F2BEF21EF00455818 /* MemoObject.swift */,
|
||||
ECFA15212BEF21F500455818 /* CanvasObject.swift */,
|
||||
ECFA15232BEF223300455818 /* GraphicContextObject.swift */,
|
||||
ECFA15252BEF224900455818 /* StrokeObject.swift */,
|
||||
ECFA15272BEF225000455818 /* QuadObject.swift */,
|
||||
);
|
||||
path = Objects;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
@@ -569,14 +589,17 @@
|
||||
ECA7389C2BE601AF00A4542E /* GridVertex.swift in Sources */,
|
||||
ECA738A82BE6025900A4542E /* GraphicUniforms.swift in Sources */,
|
||||
ECA738E62BE611FD00A4542E /* CGRect++.swift in Sources */,
|
||||
ECFA15262BEF224900455818 /* StrokeObject.swift in Sources */,
|
||||
ECA738E82BE6120F00A4542E /* Color++.swift in Sources */,
|
||||
ECA738FC2BE61C5200A4542E /* Persistence.swift in Sources */,
|
||||
ECA7387A2BE5EF0400A4542E /* MemosView.swift in Sources */,
|
||||
ECA738BA2BE60DEF00A4542E /* HistoryAction.swift in Sources */,
|
||||
ECFA15222BEF21F500455818 /* CanvasObject.swift in Sources */,
|
||||
ECA738AA2BE6026D00A4542E /* Uniforms.swift in Sources */,
|
||||
ECA7387D2BE5EF4B00A4542E /* MemoView.swift in Sources */,
|
||||
ECA738DA2BE60FF100A4542E /* CacheRenderPass.swift in Sources */,
|
||||
ECA738CD2BE60F2F00A4542E /* GridContext.swift in Sources */,
|
||||
ECFA15202BEF21EF00455818 /* MemoObject.swift in Sources */,
|
||||
ECA738C62BE60E9D00A4542E /* EraserPenStyle.swift in Sources */,
|
||||
ECA738EA2BE6122E00A4542E /* CGPoint++.swift in Sources */,
|
||||
ECA738A62BE6023F00A4542E /* GridUniforms.swift in Sources */,
|
||||
@@ -594,11 +617,11 @@
|
||||
ECA738952BE6012D00A4542E /* ViewPort.metal in Sources */,
|
||||
ECA739012BE61D9C00A4542E /* MemolaModel.xcdatamodeld in Sources */,
|
||||
ECA738F02BE6127700A4542E /* CGSize++.swift in Sources */,
|
||||
ECFA15242BEF223300455818 /* GraphicContextObject.swift in Sources */,
|
||||
ECA738EC2BE6124E00A4542E /* CGAffineTransform++.swift in Sources */,
|
||||
ECA738E22BE610D000A4542E /* GraphicRenderPass.swift in Sources */,
|
||||
ECA738DC2BE6108D00A4542E /* StrokeRenderPass.swift in Sources */,
|
||||
ECA738F42BE612A000A4542E /* Array++.swift in Sources */,
|
||||
ECA739052BE61E3100A4542E /* Memo.swift in Sources */,
|
||||
EC4538892BEBCAE000A86FEC /* Quad.swift in Sources */,
|
||||
ECA7388F2BE600DA00A4542E /* Grid.metal in Sources */,
|
||||
ECA738C92BE60EF700A4542E /* GraphicContext.swift in Sources */,
|
||||
@@ -607,6 +630,7 @@
|
||||
ECA738B32BE60D9E00A4542E /* CanvasView.swift in Sources */,
|
||||
ECA738C42BE60E8800A4542E /* MarkerPenStyle.swift in Sources */,
|
||||
ECA738BF2BE60E3400A4542E /* Pen.swift in Sources */,
|
||||
ECFA15282BEF225000455818 /* QuadObject.swift in Sources */,
|
||||
ECA738932BE6011100A4542E /* Stroke.metal in Sources */,
|
||||
ECA738B62BE60DCD00A4542E /* History.swift in Sources */,
|
||||
ECA738D22BE60F7B00A4542E /* Stroke.swift in Sources */,
|
||||
|
||||
@@ -10,11 +10,9 @@ import MetalKit
|
||||
import CoreData
|
||||
import Foundation
|
||||
|
||||
@objc(GraphicContext)
|
||||
final class GraphicContext: NSManagedObject {
|
||||
@NSManaged var id: UUID
|
||||
@NSManaged var canvas: Canvas?
|
||||
@NSManaged var strokes: NSMutableOrderedSet
|
||||
final class GraphicContext: @unchecked Sendable {
|
||||
var strokes: [Stroke] = []
|
||||
var object: GraphicContextObject?
|
||||
|
||||
var currentStroke: Stroke?
|
||||
var previousStroke: Stroke?
|
||||
@@ -24,8 +22,7 @@ final class GraphicContext: NSManagedObject {
|
||||
var vertexCount: Int = 4
|
||||
var vertexBuffer: MTLBuffer?
|
||||
|
||||
override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) {
|
||||
super.init(entity: entity, insertInto: context)
|
||||
init() {
|
||||
setViewPortVertices()
|
||||
}
|
||||
|
||||
@@ -40,21 +37,37 @@ final class GraphicContext: NSManagedObject {
|
||||
}
|
||||
|
||||
func undoGraphic() {
|
||||
guard let stroke = strokes.lastObject as? Stroke else { return }
|
||||
strokes.remove(stroke)
|
||||
stroke.graphicContext = nil
|
||||
guard !strokes.isEmpty else { return }
|
||||
let stroke = strokes.removeLast()
|
||||
Persistence.backgroundContext.perform {
|
||||
stroke.object?.graphicContext = nil
|
||||
Persistence.saveIfNeededInBackground()
|
||||
}
|
||||
previousStroke = nil
|
||||
Persistence.saveIfNeeded()
|
||||
}
|
||||
|
||||
func redoGraphic(for event: HistoryEvent) {
|
||||
switch event {
|
||||
case .stroke(let stroke):
|
||||
strokes.add(stroke)
|
||||
stroke.graphicContext = self
|
||||
strokes.append(stroke)
|
||||
Persistence.backgroundContext.perform { [weak self] in
|
||||
stroke.object?.graphicContext = self?.object
|
||||
Persistence.saveIfNeededInBackground()
|
||||
}
|
||||
previousStroke = nil
|
||||
}
|
||||
Persistence.saveIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
extension GraphicContext {
|
||||
func load() {
|
||||
guard let object else { return }
|
||||
self.strokes = object.strokes.compactMap { stroke -> Stroke? in
|
||||
guard let stroke = stroke as? StrokeObject else { return nil }
|
||||
let _stroke = Stroke(object: stroke)
|
||||
_stroke.loadVertices()
|
||||
return _stroke
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,15 +89,24 @@ extension GraphicContext: Drawable {
|
||||
|
||||
extension GraphicContext {
|
||||
func beginStroke(at point: CGPoint, pen: Pen) -> Stroke {
|
||||
let stroke = Stroke(context: Persistence.context)
|
||||
stroke.id = UUID()
|
||||
stroke.color = pen.color
|
||||
stroke.style = pen.strokeStyle.rawValue
|
||||
stroke.thickness = pen.thickness
|
||||
stroke.createdAt = .now
|
||||
stroke.quads = []
|
||||
stroke.graphicContext = self
|
||||
strokes.add(stroke)
|
||||
let stroke = Stroke(
|
||||
color: pen.color,
|
||||
style: pen.strokeStyle.rawValue,
|
||||
createdAt: .now,
|
||||
thickness: pen.thickness
|
||||
)
|
||||
Persistence.backgroundContext.perform { [graphicContext = object, _stroke = stroke] in
|
||||
let stroke = StrokeObject(context: Persistence.backgroundContext)
|
||||
stroke.color = _stroke.color
|
||||
stroke.style = _stroke.style
|
||||
stroke.thickness = _stroke.thickness
|
||||
stroke.createdAt = _stroke.createdAt
|
||||
stroke.quads = []
|
||||
stroke.graphicContext = graphicContext
|
||||
graphicContext?.strokes.add(stroke)
|
||||
_stroke.object = stroke
|
||||
}
|
||||
strokes.append(stroke)
|
||||
currentStroke = stroke
|
||||
currentPoint = point
|
||||
currentStroke?.begin(at: point)
|
||||
@@ -101,19 +123,31 @@ extension GraphicContext {
|
||||
func endStroke(at point: CGPoint) {
|
||||
guard currentPoint != nil, let currentStroke else { return }
|
||||
currentStroke.finish(at: point)
|
||||
Persistence.saveIfNeeded()
|
||||
let saveIndex = currentStroke.batchIndex
|
||||
let quads = Array(currentStroke.quads[saveIndex..<currentStroke.quads.count])
|
||||
Persistence.backgroundContext.perform { [currentStroke, quads] in
|
||||
currentStroke.saveQuads(for: quads)
|
||||
Persistence.saveIfNeededInBackground()
|
||||
if let stroke = currentStroke.object {
|
||||
currentStroke.quads.removeAll()
|
||||
Persistence.backgroundContext.refresh(stroke, mergeChanges: false)
|
||||
}
|
||||
}
|
||||
previousStroke = currentStroke
|
||||
self.currentStroke = nil
|
||||
self.currentPoint = nil
|
||||
}
|
||||
|
||||
func cancelStroke() {
|
||||
if let stroke = strokes.lastObject as? Stroke {
|
||||
Persistence.performe { context in
|
||||
strokes.remove(stroke)
|
||||
context.delete(stroke)
|
||||
if !strokes.isEmpty {
|
||||
let stroke = strokes.removeLast()
|
||||
Persistence.backgroundContext.perform { [graphicContext = object, _stroke = stroke] in
|
||||
if let stroke = _stroke.object {
|
||||
graphicContext?.strokes.remove(stroke)
|
||||
Persistence.backgroundContext.delete(stroke)
|
||||
}
|
||||
Persistence.saveIfNeededInBackground()
|
||||
}
|
||||
Persistence.saveIfNeeded()
|
||||
}
|
||||
currentStroke = nil
|
||||
currentPoint = nil
|
||||
|
||||
@@ -10,16 +10,14 @@ import CoreData
|
||||
import MetalKit
|
||||
import Foundation
|
||||
|
||||
@objc(Canvas)
|
||||
final class Canvas: NSManagedObject, Identifiable {
|
||||
@NSManaged var id: UUID
|
||||
@NSManaged var width: CGFloat
|
||||
@NSManaged var height: CGFloat
|
||||
@NSManaged var memo: Memo?
|
||||
@NSManaged var graphicContext: GraphicContext
|
||||
final class Canvas: ObservableObject, Identifiable, @unchecked Sendable {
|
||||
let size: CGSize
|
||||
let canvasID: NSManagedObjectID
|
||||
|
||||
let gridContext = GridContext()
|
||||
var graphicContext = GraphicContext()
|
||||
let viewPortContext = ViewPortContext()
|
||||
|
||||
let maximumZoomScale: CGFloat = 28
|
||||
let minimumZoomScale: CGFloat = 3.1
|
||||
|
||||
@@ -28,9 +26,13 @@ final class Canvas: NSManagedObject, Identifiable {
|
||||
var zoomScale: CGFloat = .zero
|
||||
var uniformsBuffer: MTLBuffer?
|
||||
|
||||
init(size: CGSize, canvasID: NSManagedObjectID) {
|
||||
self.size = size
|
||||
self.canvasID = canvasID
|
||||
}
|
||||
|
||||
@Published var state: State = .initial
|
||||
|
||||
var size: CGSize { CGSize(width: width, height: height) }
|
||||
var hasValidStroke: Bool {
|
||||
if let currentStroke = graphicContext.currentStroke {
|
||||
return Date.now.timeIntervalSince(currentStroke.createdAt) * 1000 > 80
|
||||
@@ -42,16 +44,21 @@ final class Canvas: NSManagedObject, Identifiable {
|
||||
// MARK: - Actions
|
||||
extension Canvas {
|
||||
func load() {
|
||||
state = .loading
|
||||
let start = Date().formatted(.dateTime.minute().second().secondFraction(.fractional(5)))
|
||||
for stroke in graphicContext.strokes {
|
||||
if let stroke = stroke as? Stroke {
|
||||
stroke.loadVertices()
|
||||
Persistence.backgroundContext.perform { [weak self, canvasID] in
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.state = .loading
|
||||
}
|
||||
guard let canvas = Persistence.backgroundContext.object(with: canvasID) as? CanvasObject else {
|
||||
return
|
||||
}
|
||||
let graphicContext = canvas.graphicContext
|
||||
self?.graphicContext.object = graphicContext
|
||||
self?.graphicContext.load()
|
||||
Persistence.backgroundContext.refresh(canvas, mergeChanges: false)
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.state = .loaded
|
||||
}
|
||||
}
|
||||
let end = Date().formatted(.dateTime.minute().second().secondFraction(.fractional(5)))
|
||||
NSLog("[Memola] - Loaded from \(start) to \(end)")
|
||||
state = .loaded
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,7 +111,7 @@ extension Canvas {
|
||||
}
|
||||
|
||||
func getNewlyAddedStroke() -> Stroke? {
|
||||
graphicContext.strokes.lastObject as? Stroke
|
||||
graphicContext.strokes.last
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,15 +8,28 @@
|
||||
import CoreData
|
||||
import Foundation
|
||||
|
||||
@objc(Quad)
|
||||
class Quad: NSManagedObject {
|
||||
@NSManaged var id: UUID
|
||||
@NSManaged var originX: CGFloat
|
||||
@NSManaged var originY: CGFloat
|
||||
@NSManaged var size: CGFloat
|
||||
@NSManaged var rotation: CGFloat
|
||||
@NSManaged var shape: Int16
|
||||
@NSManaged var stroke: Stroke?
|
||||
struct Quad {
|
||||
var originX: CGFloat
|
||||
var originY: CGFloat
|
||||
var size: CGFloat
|
||||
var rotation: CGFloat
|
||||
var shape: Int16
|
||||
|
||||
init(object: QuadObject) {
|
||||
self.originX = object.originX
|
||||
self.originY = object.originY
|
||||
self.size = object.size
|
||||
self.rotation = object.rotation
|
||||
self.shape = object.shape
|
||||
}
|
||||
|
||||
init(origin: CGPoint, size: CGFloat, rotation: CGFloat, shape: Int16) {
|
||||
self.originX = origin.x
|
||||
self.originY = origin.y
|
||||
self.size = size
|
||||
self.rotation = rotation
|
||||
self.shape = shape
|
||||
}
|
||||
|
||||
var origin: CGPoint {
|
||||
get { CGPoint(x: originX, y: originY) }
|
||||
|
||||
@@ -132,42 +132,15 @@ struct SolidPointStrokeGenerator: StrokeGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
#warning("TODO: remove later")
|
||||
private func addLine(from start: CGPoint, to end: CGPoint, on stroke: Stroke) {
|
||||
let distance = end.distance(to: start)
|
||||
let segments = max(distance / stroke.penStyle.anyPenStyle.stepRate, 2)
|
||||
for i in 0..<Int(segments) {
|
||||
let i = CGFloat(i)
|
||||
let x = start.x + (end.x - start.x) * (i / segments)
|
||||
let y = start.y + (end.y - start.y) * (i / segments)
|
||||
let point = CGPoint(x: x, y: y)
|
||||
addPoint(point, on: stroke)
|
||||
}
|
||||
}
|
||||
|
||||
private func discardVertices(upto index: Int, quadIndex: Int, on stroke: Stroke) {
|
||||
if index < 0 {
|
||||
stroke.vertices.removeAll()
|
||||
discardQuads(from: quadIndex + 1, on: stroke)
|
||||
} else {
|
||||
let count = stroke.vertices.endIndex
|
||||
let dropCount = count - (max(0, index) + 1)
|
||||
stroke.vertices.removeLast(dropCount)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
stroke.removeQuads(from: quadIndex + 1)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,15 +9,36 @@ import MetalKit
|
||||
import CoreData
|
||||
import Foundation
|
||||
|
||||
@objc(Stroke)
|
||||
final class Stroke: NSManagedObject {
|
||||
@NSManaged var id: UUID
|
||||
@NSManaged var color: [CGFloat]
|
||||
@NSManaged var style: Int16
|
||||
@NSManaged var createdAt: Date
|
||||
@NSManaged var thickness: CGFloat
|
||||
@NSManaged var quads: NSMutableOrderedSet
|
||||
@NSManaged var graphicContext: GraphicContext?
|
||||
final class Stroke: @unchecked Sendable {
|
||||
var object: StrokeObject?
|
||||
var color: [CGFloat]
|
||||
var style: Int16
|
||||
var createdAt: Date
|
||||
var thickness: CGFloat
|
||||
var quads: [Quad]
|
||||
|
||||
init(object: StrokeObject) {
|
||||
self.object = object
|
||||
self.color = object.color
|
||||
self.style = object.style
|
||||
self.createdAt = object.createdAt
|
||||
self.thickness = object.thickness
|
||||
self.quads = []
|
||||
}
|
||||
|
||||
init(
|
||||
color: [CGFloat],
|
||||
style: Int16,
|
||||
createdAt: Date,
|
||||
thickness: CGFloat,
|
||||
quads: [Quad] = []
|
||||
) {
|
||||
self.color = color
|
||||
self.style = style
|
||||
self.createdAt = createdAt
|
||||
self.thickness = thickness
|
||||
self.quads = quads
|
||||
}
|
||||
|
||||
var angle: CGFloat = 0
|
||||
|
||||
@@ -25,6 +46,7 @@ final class Stroke: NSManagedObject {
|
||||
Style(rawValue: style) ?? .marker
|
||||
}
|
||||
|
||||
var batchIndex: Int = 0
|
||||
var quadIndex: Int = -1
|
||||
var vertexIndex: Int = -1
|
||||
var keyPoints: [CGPoint] = []
|
||||
@@ -58,22 +80,47 @@ final class Stroke: NSManagedObject {
|
||||
}
|
||||
|
||||
func loadVertices() {
|
||||
vertices = quads
|
||||
.flatMap { ($0 as? Quad)?.generateVertices(color) ?? [] }
|
||||
guard let object else { return }
|
||||
for quad in object.quads {
|
||||
guard let quad = quad as? QuadObject else { continue }
|
||||
vertices.append(contentsOf: Quad(object: quad).generateVertices(object.color))
|
||||
}
|
||||
vertexCount = vertices.endIndex
|
||||
}
|
||||
|
||||
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
|
||||
let quad = Quad(
|
||||
origin: point,
|
||||
size: thickness,
|
||||
rotation: rotation,
|
||||
shape: shape.rawValue
|
||||
)
|
||||
quads.append(quad)
|
||||
return quad
|
||||
}
|
||||
|
||||
func removeQuads(from index: Int) {
|
||||
let dropCount = quads.endIndex - max(1, index)
|
||||
quads.removeLast(dropCount)
|
||||
let quads = Array(quads[batchIndex..<index])
|
||||
batchIndex = index
|
||||
Persistence.backgroundContext.perform { [weak self, quads] in
|
||||
self?.saveQuads(for: quads)
|
||||
}
|
||||
}
|
||||
|
||||
func saveQuads(for quads: [Quad]) {
|
||||
for _quad in quads {
|
||||
let quad = QuadObject(context: Persistence.backgroundContext)
|
||||
quad.originX = _quad.originX
|
||||
quad.originY = _quad.originY
|
||||
quad.size = _quad.size
|
||||
quad.rotation = _quad.rotation
|
||||
quad.shape = _quad.shape
|
||||
quad.stroke = object
|
||||
object?.quads.add(quad)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Stroke: Drawable {
|
||||
|
||||
@@ -51,9 +51,11 @@ class History: ObservableObject {
|
||||
redoCache = redoStack
|
||||
for event in redoStack {
|
||||
switch event {
|
||||
case .stroke(let stroke):
|
||||
Persistence.performe { context in
|
||||
context.delete(stroke)
|
||||
case .stroke(let _stroke):
|
||||
Persistence.backgroundContext.perform {
|
||||
if let stroke = _stroke.object {
|
||||
Persistence.backgroundContext.delete(stroke)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -36,17 +36,16 @@ 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.array {
|
||||
guard let stroke = stroke as? Stroke else { continue }
|
||||
for stroke in graphicContext.strokes {
|
||||
if graphicContext.previousStroke === stroke || graphicContext.currentStroke === stroke {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -62,9 +62,6 @@ class CanvasViewController: UIViewController {
|
||||
override func viewDidDisappear(_ animated: Bool) {
|
||||
super.viewDidDisappear(animated)
|
||||
history.resetRedo()
|
||||
Persistence.performe { context in
|
||||
context.refresh(canvas, mergeChanges: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
//
|
||||
// Memo.swift
|
||||
// Memola
|
||||
//
|
||||
// Created by Dscyre Scotti on 5/4/24.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import Foundation
|
||||
|
||||
@objc(Memo)
|
||||
final class Memo: NSManagedObject {
|
||||
@NSManaged var id: UUID
|
||||
@NSManaged var title: String
|
||||
@NSManaged var data: Data
|
||||
@NSManaged var createdAt: Date
|
||||
@NSManaged var updatedAt: Date
|
||||
@NSManaged var canvas: Canvas
|
||||
}
|
||||
|
||||
extension Memo: Identifiable { }
|
||||
@@ -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()
|
||||
|
||||
@@ -10,9 +10,9 @@ import SwiftUI
|
||||
struct MemosView: View {
|
||||
@Environment(\.managedObjectContext) var managedObjectContext
|
||||
|
||||
@FetchRequest(sortDescriptors: []) var memos: FetchedResults<Memo>
|
||||
@FetchRequest(sortDescriptors: []) var memoObjects: FetchedResults<MemoObject>
|
||||
|
||||
@State var memo: Memo?
|
||||
@State var memo: MemoObject?
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
@@ -30,15 +30,14 @@ struct MemosView: View {
|
||||
}
|
||||
}
|
||||
.fullScreenCover(item: $memo) { memo in
|
||||
MemoView()
|
||||
.environmentObject(memo.canvas)
|
||||
MemoView(memo: memo)
|
||||
}
|
||||
}
|
||||
|
||||
var memoGrid: some View {
|
||||
ScrollView {
|
||||
LazyVGrid(columns: .init(repeating: GridItem(.flexible()), count: 3)) {
|
||||
ForEach(memos) { memo in
|
||||
ForEach(memoObjects) { memo in
|
||||
memoCard(memo)
|
||||
}
|
||||
}
|
||||
@@ -46,47 +45,44 @@ struct MemosView: View {
|
||||
}
|
||||
}
|
||||
|
||||
func memoCard(_ memo: Memo) -> some View {
|
||||
func memoCard(_ memoObject: MemoObject) -> some View {
|
||||
VStack(alignment: .leading) {
|
||||
Rectangle()
|
||||
.frame(height: 150)
|
||||
Text(memo.title)
|
||||
Text(memoObject.title)
|
||||
}
|
||||
.onTapGesture {
|
||||
openMemo(for: memo)
|
||||
openMemo(for: memoObject)
|
||||
}
|
||||
}
|
||||
|
||||
func createMemo(title: String) {
|
||||
do {
|
||||
let memo = Memo(context: managedObjectContext)
|
||||
memo.id = UUID()
|
||||
memo.title = title
|
||||
memo.createdAt = .now
|
||||
memo.updatedAt = .now
|
||||
let memoObject = MemoObject(context: managedObjectContext)
|
||||
memoObject.title = title
|
||||
memoObject.createdAt = .now
|
||||
memoObject.updatedAt = .now
|
||||
|
||||
let canvas = Canvas(context: managedObjectContext)
|
||||
canvas.id = UUID()
|
||||
canvas.width = 4_000
|
||||
canvas.height = 4_000
|
||||
let canvasObject = CanvasObject(context: managedObjectContext)
|
||||
canvasObject.width = 4_000
|
||||
canvasObject.height = 4_000
|
||||
|
||||
let graphicContext = GraphicContext(context: managedObjectContext)
|
||||
graphicContext.id = UUID()
|
||||
graphicContext.strokes = []
|
||||
let graphicContextObject = GraphicContextObject(context: managedObjectContext)
|
||||
graphicContextObject.strokes = []
|
||||
|
||||
memo.canvas = canvas
|
||||
canvas.memo = memo
|
||||
canvas.graphicContext = graphicContext
|
||||
graphicContext.canvas = canvas
|
||||
memoObject.canvas = canvasObject
|
||||
canvasObject.memo = memoObject
|
||||
canvasObject.graphicContext = graphicContextObject
|
||||
graphicContextObject.canvas = canvasObject
|
||||
|
||||
try managedObjectContext.save()
|
||||
openMemo(for: memo)
|
||||
openMemo(for: memoObject)
|
||||
} catch {
|
||||
NSLog("[Memola] - \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
func openMemo(for memo: Memo) {
|
||||
func openMemo(for memo: MemoObject) {
|
||||
self.memo = memo
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,9 @@ class Persistence {
|
||||
|
||||
static var backgroundContext: NSManagedObjectContext = {
|
||||
let context = shared.persistentContainer.newBackgroundContext()
|
||||
context.automaticallyMergesChangesFromParent = true
|
||||
context.undoManager = nil
|
||||
|
||||
// context.automaticallyMergesC hangesFromParent = true
|
||||
return context
|
||||
}()
|
||||
|
||||
@@ -87,4 +89,24 @@ class Persistence {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func saveIfNeededInBackground() {
|
||||
if backgroundContext.hasChanges {
|
||||
do {
|
||||
try backgroundContext.save()
|
||||
} catch {
|
||||
NSLog("[Memola] - \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Persistence {
|
||||
static func background(_ task: @escaping (NSManagedObjectContext) async throws -> Void, errorHandler: ((Error) async -> Void)? = nil) async {
|
||||
do {
|
||||
try await task(backgroundContext)
|
||||
} catch {
|
||||
await errorHandler?(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
22
Memola/Persistence/Objects/CanvasObject.swift
Normal file
22
Memola/Persistence/Objects/CanvasObject.swift
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
15
Memola/Persistence/Objects/GraphicContextObject.swift
Normal file
15
Memola/Persistence/Objects/GraphicContextObject.swift
Normal file
@@ -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
|
||||
}
|
||||
18
Memola/Persistence/Objects/MemoObject.swift
Normal file
18
Memola/Persistence/Objects/MemoObject.swift
Normal file
@@ -0,0 +1,18 @@
|
||||
//
|
||||
// MemoObject.swift
|
||||
// Memola
|
||||
//
|
||||
// Created by Dscyre Scotti on 5/11/24.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import Foundation
|
||||
|
||||
@objc(MemoObject)
|
||||
final class MemoObject: NSManagedObject, Identifiable {
|
||||
@NSManaged var title: String
|
||||
@NSManaged var data: Data
|
||||
@NSManaged var createdAt: Date
|
||||
@NSManaged var updatedAt: Date
|
||||
@NSManaged var canvas: CanvasObject
|
||||
}
|
||||
19
Memola/Persistence/Objects/QuadObject.swift
Normal file
19
Memola/Persistence/Objects/QuadObject.swift
Normal file
@@ -0,0 +1,19 @@
|
||||
//
|
||||
// QuadObject.swift
|
||||
// Memola
|
||||
//
|
||||
// Created by Dscyre Scotti on 5/11/24.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import Foundation
|
||||
|
||||
@objc(QuadObject)
|
||||
final class QuadObject: NSManagedObject {
|
||||
@NSManaged var originX: CGFloat
|
||||
@NSManaged var originY: CGFloat
|
||||
@NSManaged var size: CGFloat
|
||||
@NSManaged var rotation: CGFloat
|
||||
@NSManaged var shape: Int16
|
||||
@NSManaged var stroke: StrokeObject?
|
||||
}
|
||||
19
Memola/Persistence/Objects/StrokeObject.swift
Normal file
19
Memola/Persistence/Objects/StrokeObject.swift
Normal file
@@ -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?
|
||||
}
|
||||
@@ -1,40 +1,35 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22757" systemVersion="23B74" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<entity name="Canvas" representedClassName="Canvas" syncable="YES">
|
||||
<entity name="CanvasObject" representedClassName="CanvasObject" syncable="YES">
|
||||
<attribute name="height" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES" customClassName="CGFloat"/>
|
||||
<attribute name="id" attributeType="UUID" usesScalarValueType="NO"/>
|
||||
<attribute name="width" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES" customClassName="CGFloat"/>
|
||||
<relationship name="graphicContext" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="GraphicContext" inverseName="canvas" inverseEntity="GraphicContext"/>
|
||||
<relationship name="memo" maxCount="1" deletionRule="Deny" destinationEntity="Memo" inverseName="canvas" inverseEntity="Memo"/>
|
||||
<relationship name="graphicContext" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="GraphicContextObject" inverseName="canvas" inverseEntity="GraphicContextObject"/>
|
||||
<relationship name="memo" maxCount="1" deletionRule="Deny" destinationEntity="MemoObject" inverseName="canvas" inverseEntity="MemoObject"/>
|
||||
</entity>
|
||||
<entity name="GraphicContext" representedClassName="GraphicContext" syncable="YES">
|
||||
<attribute name="id" attributeType="UUID" usesScalarValueType="NO"/>
|
||||
<relationship name="canvas" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Canvas" inverseName="graphicContext" inverseEntity="Canvas"/>
|
||||
<relationship name="strokes" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="Stroke" inverseName="graphicContext" inverseEntity="Stroke"/>
|
||||
<entity name="GraphicContextObject" representedClassName="GraphicContextObject" syncable="YES">
|
||||
<relationship name="canvas" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="CanvasObject" inverseName="graphicContext" inverseEntity="CanvasObject"/>
|
||||
<relationship name="strokes" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="StrokeObject" inverseName="graphicContext" inverseEntity="StrokeObject"/>
|
||||
</entity>
|
||||
<entity name="Memo" representedClassName="Memo" syncable="YES">
|
||||
<entity name="MemoObject" representedClassName="MemoObject" syncable="YES">
|
||||
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="id" attributeType="UUID" usesScalarValueType="NO"/>
|
||||
<attribute name="title" attributeType="String"/>
|
||||
<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="CanvasObject" inverseName="memo" inverseEntity="CanvasObject"/>
|
||||
</entity>
|
||||
<entity name="Quad" representedClassName="Quad" syncable="YES">
|
||||
<attribute name="id" attributeType="UUID" usesScalarValueType="NO"/>
|
||||
<entity name="QuadObject" representedClassName="QuadObject" syncable="YES">
|
||||
<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"/>
|
||||
<relationship name="stroke" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StrokeObject" inverseName="quads" inverseEntity="StrokeObject"/>
|
||||
</entity>
|
||||
<entity name="Stroke" representedClassName="Stroke" syncable="YES">
|
||||
<entity name="StrokeObject" representedClassName="StrokeObject" 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="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"/>
|
||||
<relationship name="graphicContext" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="GraphicContextObject" inverseName="strokes" inverseEntity="GraphicContextObject"/>
|
||||
<relationship name="quads" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="QuadObject" inverseName="stroke" inverseEntity="QuadObject"/>
|
||||
</entity>
|
||||
</model>
|
||||
Reference in New Issue
Block a user