feat: add pen tool persistence

This commit is contained in:
dscyrescotti
2024-05-17 23:30:32 +07:00
parent 3204328e5e
commit 4701eac3ba
13 changed files with 212 additions and 75 deletions

View File

@@ -7,6 +7,9 @@
objects = {
/* Begin PBXBuildFile section */
EC0D14212BF79C73009BFE5F /* ToolObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC0D14202BF79C73009BFE5F /* ToolObject.swift */; };
EC0D14242BF79C98009BFE5F /* MemolaModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = EC0D14222BF79C98009BFE5F /* MemolaModel.xcdatamodeld */; };
EC0D14262BF7A8C9009BFE5F /* PenObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC0D14252BF7A8C9009BFE5F /* PenObject.swift */; };
EC3565522BEFC65F00A4E0BF /* NSManagedObjectContext++.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC3565512BEFC65F00A4E0BF /* NSManagedObjectContext++.swift */; };
EC3565542BEFC6AD00A4E0BF /* View++.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC3565532BEFC6AD00A4E0BF /* View++.swift */; };
EC3565562BEFC7B300A4E0BF /* NSManagedObject++.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC3565552BEFC7B300A4E0BF /* NSManagedObject++.swift */; };
@@ -70,7 +73,6 @@
ECA738F42BE612A000A4542E /* Array++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738F32BE612A000A4542E /* Array++.swift */; };
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 */; };
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 */; };
@@ -81,6 +83,9 @@
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
EC0D14202BF79C73009BFE5F /* ToolObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolObject.swift; sourceTree = "<group>"; };
EC0D14232BF79C98009BFE5F /* MemolaModel.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MemolaModel.xcdatamodel; sourceTree = "<group>"; };
EC0D14252BF7A8C9009BFE5F /* PenObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PenObject.swift; sourceTree = "<group>"; };
EC3565512BEFC65F00A4E0BF /* NSManagedObjectContext++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSManagedObjectContext++.swift"; sourceTree = "<group>"; };
EC3565532BEFC6AD00A4E0BF /* View++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View++.swift"; sourceTree = "<group>"; };
EC3565552BEFC7B300A4E0BF /* NSManagedObject++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSManagedObject++.swift"; sourceTree = "<group>"; };
@@ -146,7 +151,6 @@
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>"; };
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>"; };
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>"; };
@@ -556,7 +560,7 @@
ECA738FE2BE61D5700A4542E /* Models */ = {
isa = PBXGroup;
children = (
ECA738FF2BE61D9C00A4542E /* MemolaModel.xcdatamodeld */,
EC0D14222BF79C98009BFE5F /* MemolaModel.xcdatamodeld */,
);
path = Models;
sourceTree = "<group>";
@@ -586,6 +590,8 @@
ECFA15232BEF223300455818 /* GraphicContextObject.swift */,
ECFA15252BEF224900455818 /* StrokeObject.swift */,
ECFA15272BEF225000455818 /* QuadObject.swift */,
EC0D14202BF79C73009BFE5F /* ToolObject.swift */,
EC0D14252BF7A8C9009BFE5F /* PenObject.swift */,
);
path = Objects;
sourceTree = "<group>";
@@ -696,8 +702,9 @@
ECA738EE2BE6125D00A4542E /* simd_float4x4++.swift in Sources */,
ECA7388C2BE6009600A4542E /* Textures.swift in Sources */,
ECA738B82BE60DDC00A4542E /* HistoryEvent.swift in Sources */,
EC0D14262BF7A8C9009BFE5F /* PenObject.swift in Sources */,
ECA738952BE6012D00A4542E /* ViewPort.metal in Sources */,
ECA739012BE61D9C00A4542E /* MemolaModel.xcdatamodeld in Sources */,
EC0D14242BF79C98009BFE5F /* MemolaModel.xcdatamodeld in Sources */,
ECA738F02BE6127700A4542E /* CGSize++.swift in Sources */,
ECFA15242BEF223300455818 /* GraphicContextObject.swift in Sources */,
EC3565562BEFC7B300A4E0BF /* NSManagedObject++.swift in Sources */,
@@ -710,6 +717,7 @@
ECA7388F2BE600DA00A4542E /* Grid.metal in Sources */,
ECA738C92BE60EF700A4542E /* GraphicContext.swift in Sources */,
ECA738F62BE612B700A4542E /* MTLDevice++.swift in Sources */,
EC0D14212BF79C73009BFE5F /* ToolObject.swift in Sources */,
EC50500D2BF6674400B4D86E /* DraggableViewModifier.swift in Sources */,
ECA7389E2BE601CB00A4542E /* QuadVertex.swift in Sources */,
ECA738B32BE60D9E00A4542E /* CanvasView.swift in Sources */,
@@ -944,13 +952,14 @@
/* End XCConfigurationList section */
/* Begin XCVersionGroup section */
ECA738FF2BE61D9C00A4542E /* MemolaModel.xcdatamodeld */ = {
EC0D14222BF79C98009BFE5F /* MemolaModel.xcdatamodeld */ = {
isa = XCVersionGroup;
children = (
ECA739002BE61D9C00A4542E /* MemolaModel.xcdatamodel */,
EC0D14232BF79C98009BFE5F /* MemolaModel.xcdatamodel */,
);
currentVersion = ECA739002BE61D9C00A4542E /* MemolaModel.xcdatamodel */;
path = MemolaModel.xcdatamodeld;
currentVersion = EC0D14232BF79C98009BFE5F /* MemolaModel.xcdatamodel */;
name = MemolaModel.xcdatamodeld;
path = /Users/dscyrescotti/Documents/Projects/Memola/Memola/Resources/Models/MemolaModel.xcdatamodeld;
sourceTree = "<group>";
versionGroupType = wrapper.xcdatamodel;
};

View File

@@ -6,27 +6,54 @@
//
import SwiftUI
import CoreData
import Foundation
class Tool: NSObject, ObservableObject {
@Published var pens: [Pen]
public class Tool: NSObject, ObservableObject {
let object: ToolObject
@Published var pens: [Pen] = []
@Published var selectedPen: Pen?
@Published var draggedPen: Pen?
override init() {
pens = [
Pen(for: .eraser),
Pen(for: .marker)
]
super.init()
selectedPen = pens[1]
init(object: ToolObject) {
self.object = object
}
func changePen(_ pen: Pen) {
selectedPen = pen
func load() {
DispatchQueue.main.async { [weak self] in
guard let self else { return }
pens = object.pens.sortedArray(using: [NSSortDescriptor(key: "orderIndex", ascending: true)]).compactMap {
guard let pen = $0 as? PenObject else { return nil }
return Pen(object: pen)
}
if let selectedPen = pens.first(where: { $0.isSelected }) {
selectPen(selectedPen)
}
}
}
func selectPen(_ pen: Pen) {
if let selectedPen {
unselectPen(selectedPen)
}
withAnimation {
selectedPen = pen
}
selectedPen?.isSelected = true
}
func unselectPen(_ pen: Pen) {
pen.isSelected = false
}
func addPen(_ pen: Pen) {
pens.append(pen)
withAnimation {
pens.append(pen)
}
selectPen(pen)
if let _pen = pen.object {
object.pens.add(_pen)
}
}
}

View File

@@ -10,32 +10,41 @@ import Foundation
import UniformTypeIdentifiers
class Pen: NSObject, ObservableObject, Identifiable {
let id: String
@Published var style: any PenStyle
@Published var color: [CGFloat]
@Published var thickness: CGFloat
var object: PenObject?
init(style: any PenStyle, color: [CGFloat], thickness: CGFloat) {
self.id = UUID().uuidString
self.style = style
self.color = color
self.thickness = thickness
let id: String
@Published var style: any PenStyle {
didSet {
object?.style = strokeStyle.rawValue
}
}
@Published var color: [CGFloat] {
didSet {
object?.color = color
}
}
@Published var thickness: CGFloat {
didSet {
object?.thickness = thickness
}
}
@Published var isSelected: Bool {
didSet {
object?.isSelected = isSelected
}
}
init(object: PenObject) {
self.object = object
self.id = object.objectID.uriRepresentation().absoluteString
self.style = (Stroke.Style(rawValue: object.style) ?? .marker).anyPenStyle
self.color = object.color
self.thickness = object.thickness
self.isSelected = object.isSelected
super.init()
}
var strokeStyle: Stroke.Style {
switch style {
case is MarkerPenStyle:
return .marker
case is EraserPenStyle:
return .eraser
default:
return .marker
}
}
}
extension Pen {
convenience init(for style: any PenStyle) {
self.init(style: style, color: style.color, thickness: style.thinkness.min)
style.strokeStyle
}
}

View File

@@ -22,4 +22,15 @@ extension PenStyle {
func loadTexture(on device: MTLDevice) -> MTLTexture? {
Textures.createPenTexture(with: textureName, on: device)
}
var strokeStyle: Stroke.Style {
switch self {
case is MarkerPenStyle:
return .marker
case is EraserPenStyle:
return .eraser
default:
return .marker
}
}
}

View File

@@ -183,6 +183,7 @@ extension CanvasViewController {
extension CanvasViewController {
func loadMemo() {
tool.load()
canvas.load()
}

View File

@@ -10,9 +10,8 @@ import CoreData
struct MemoView: View {
@Environment(\.dismiss) var dismiss
@Environment(\.managedObjectContext) var managedObjectContext
@StateObject var tool = Tool()
@StateObject var tool: Tool
@StateObject var canvas: Canvas
@StateObject var history = History()
@@ -20,6 +19,7 @@ struct MemoView: View {
init(memo: MemoObject) {
self.memo = memo
self._tool = StateObject(wrappedValue: Tool(object: memo.tool))
self._canvas = StateObject(wrappedValue: Canvas(size: memo.canvas.size, canvasID: memo.canvas.objectID))
}
@@ -97,13 +97,8 @@ struct MemoView: View {
}
func closeMemo() {
history.resetRedo()
if managedObjectContext.hasChanges {
do {
try managedObjectContext.save()
} catch {
NSLog("[Memola] - \(error.localizedDescription)")
}
withPersistenceSync(\.viewContext) { context in
try context.saveIfNeeded()
}
dismiss()
}

View File

@@ -27,6 +27,11 @@ struct PenDropDelegate: DropDelegate {
tool.pens.move(fromOffsets: IndexSet(integer: fromIndex), toOffset: toIndex > fromIndex ? toIndex + 1 : toIndex)
tool.objectWillChange.send()
}
withPersistence(\.viewContext) { context in
for (index, pen) in tool.pens.enumerated() {
pen.object?.orderIndex = Int16(index)
}
}
}
}
}

View File

@@ -82,11 +82,11 @@ struct PenToolView: View {
.onTapGesture {
if tool.selectedPen === pen {
withAnimation {
tool.selectedPen = nil
tool.unselectPen(pen)
}
} else {
withAnimation {
tool.changePen(pen)
tool.selectPen(pen)
}
}
}
@@ -95,9 +95,13 @@ struct PenToolView: View {
var newPenButton: some View {
Button(action: {
let pen = Pen(for: .marker)
let pen = PenObject.createObject(\.viewContext, penStyle: .marker)
pen.color = [Color.red, Color.blue, Color.green, Color.black, Color.orange].randomElement()!.components
tool.addPen(pen)
pen.isSelected = true
pen.tool = tool.object
pen.orderIndex = Int16(tool.pens.count)
let _pen = Pen(object: pen)
tool.addPen(_pen)
}) {
Image(systemName: "plus")
.font(.title3)

View File

@@ -62,28 +62,45 @@ struct MemosView: View {
}
func createMemo(title: String) {
do {
let memoObject = MemoObject(context: managedObjectContext)
memoObject.title = title
memoObject.createdAt = .now
memoObject.updatedAt = .now
let memoObject = MemoObject(\.viewContext)
memoObject.title = title
memoObject.createdAt = .now
memoObject.updatedAt = .now
let canvasObject = CanvasObject(context: managedObjectContext)
canvasObject.width = 8_000
canvasObject.height = 8_000
let canvasObject = CanvasObject(context: managedObjectContext)
canvasObject.width = 8_000
canvasObject.height = 8_000
let graphicContextObject = GraphicContextObject(context: managedObjectContext)
graphicContextObject.strokes = []
let toolObject = ToolObject(\.viewContext)
toolObject.pens = []
memoObject.canvas = canvasObject
canvasObject.memo = memoObject
canvasObject.graphicContext = graphicContextObject
graphicContextObject.canvas = canvasObject
let eraserPenObject = PenObject.createObject(\.viewContext, penStyle: .eraser)
eraserPenObject.orderIndex = 0
let markerPenObject = PenObject.createObject(\.viewContext, penStyle: .marker)
markerPenObject.orderIndex = 1
try managedObjectContext.save()
openMemo(for: memoObject)
} catch {
NSLog("[Memola] - \(error.localizedDescription)")
let graphicContextObject = GraphicContextObject(\.viewContext)
graphicContextObject.strokes = []
memoObject.canvas = canvasObject
memoObject.tool = toolObject
canvasObject.memo = memoObject
canvasObject.graphicContext = graphicContextObject
toolObject.memo = memoObject
toolObject.pens = [eraserPenObject, markerPenObject]
eraserPenObject.tool = toolObject
markerPenObject.tool = toolObject
graphicContextObject.canvas = canvasObject
withPersistenceSync(\.viewContext) { context in
try context.save()
DispatchQueue.main.async {
openMemo(for: memoObject)
}
}
}

View File

@@ -10,9 +10,10 @@ import Foundation
@objc(MemoObject)
final class MemoObject: NSManagedObject, Identifiable {
@NSManaged var title: String
@NSManaged var data: Data
@NSManaged var title: String
@NSManaged var createdAt: Date
@NSManaged var updatedAt: Date
@NSManaged var tool: ToolObject
@NSManaged var canvas: CanvasObject
}

View File

@@ -0,0 +1,30 @@
//
// PenObject.swift
// Memola
//
// Created by Dscyre Scotti on 5/17/24.
//
import CoreData
import Foundation
@objc(PenObject)
class PenObject: NSManagedObject {
@NSManaged var color: [CGFloat]
@NSManaged var style: Int16
@NSManaged var thickness: CGFloat
@NSManaged var isSelected: Bool
@NSManaged var orderIndex: Int16
@NSManaged var tool: ToolObject?
}
extension PenObject {
static func createObject(_ keyPath: KeyPath<Persistence, NSManagedObjectContext>, penStyle: any PenStyle) -> PenObject {
let object = PenObject(context: Persistence.shared[keyPath: keyPath])
object.color = penStyle.color
object.style = penStyle.strokeStyle.rawValue
object.isSelected = false
object.thickness = penStyle.thinkness.min
return object
}
}

View File

@@ -0,0 +1,15 @@
//
// ToolObject.swift
// Memola
//
// Created by Dscyre Scotti on 5/17/24.
//
import CoreData
import Foundation
@objc(ToolObject)
class ToolObject: NSManagedObject {
@NSManaged var pens: NSMutableSet
@NSManaged var memo: MemoObject?
}

View File

@@ -15,6 +15,15 @@
<attribute name="title" attributeType="String"/>
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
<relationship name="canvas" maxCount="1" deletionRule="Cascade" destinationEntity="CanvasObject" inverseName="memo" inverseEntity="CanvasObject"/>
<relationship name="tool" maxCount="1" deletionRule="Cascade" destinationEntity="ToolObject" inverseName="memo" inverseEntity="ToolObject"/>
</entity>
<entity name="PenObject" representedClassName="PenObject" syncable="YES">
<attribute name="color" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromDataTransformer" customClassName="[CGFloat]"/>
<attribute name="isSelected" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="orderIndex" 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"/>
<relationship name="tool" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ToolObject" inverseName="pens" inverseEntity="ToolObject"/>
</entity>
<entity name="QuadObject" representedClassName="QuadObject" syncable="YES">
<attribute name="color" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromDataTransformer" customClassName="[CGFloat]"/>
@@ -34,4 +43,8 @@
<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>
<entity name="ToolObject" representedClassName="ToolObject" syncable="YES">
<relationship name="memo" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MemoObject" inverseName="tool" inverseEntity="MemoObject"/>
<relationship name="pens" toMany="YES" deletionRule="Cascade" destinationEntity="PenObject" inverseName="tool" inverseEntity="PenObject"/>
</entity>
</model>