feat: reduce memory footprint

This commit is contained in:
dscyrescotti
2024-05-08 22:51:20 +07:00
parent 8fb98f4f76
commit 1f9c176eb0
17 changed files with 370 additions and 253 deletions

View File

@@ -7,6 +7,8 @@
objects = { objects = {
/* Begin PBXBuildFile section */ /* 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 */; }; 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 */; };
@@ -61,7 +63,7 @@
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 /* Quad.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738F72BE612EB00A4542E /* Quad.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 */; };
@@ -69,6 +71,8 @@
/* 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>"; };
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>"; };
@@ -124,7 +128,7 @@
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 /* Quad.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Quad.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>"; };
@@ -158,6 +162,14 @@
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 = (
@@ -224,6 +236,7 @@
ECA7387B2BE5EF3500A4542E /* Memo */ = { ECA7387B2BE5EF3500A4542E /* Memo */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
ECA739042BE61E3100A4542E /* Memo.swift */,
ECA7387C2BE5EF4B00A4542E /* MemoView.swift */, ECA7387C2BE5EF4B00A4542E /* MemoView.swift */,
ECA739072BE623F300A4542E /* PenToolView.swift */, ECA739072BE623F300A4542E /* PenToolView.swift */,
); );
@@ -291,7 +304,8 @@
ECA738982BE6015700A4542E /* Primitives */ = { ECA738982BE6015700A4542E /* Primitives */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
ECA738F72BE612EB00A4542E /* Quad.swift */, ECA738F72BE612EB00A4542E /* StrokeQuad.swift */,
EC4538882BEBCAE000A86FEC /* Quad.swift */,
); );
path = Primitives; path = Primitives;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -449,7 +463,7 @@
ECA738FA2BE61B1700A4542E /* Persistence */ = { ECA738FA2BE61B1700A4542E /* Persistence */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
ECA739032BE61E2600A4542E /* Entities */, EC771E5C2BEB37FC0053CC68 /* Transformers */,
ECA739022BE61DE700A4542E /* Core */, ECA739022BE61DE700A4542E /* Core */,
); );
path = Persistence; path = Persistence;
@@ -479,14 +493,6 @@
path = Core; path = Core;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
ECA739032BE61E2600A4542E /* Entities */ = {
isa = PBXGroup;
children = (
ECA739042BE61E3100A4542E /* Memo.swift */,
);
path = Entities;
sourceTree = "<group>";
};
ECA739062BE61F7500A4542E /* Core */ = { ECA739062BE61F7500A4542E /* Core */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -566,6 +572,7 @@
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 */,
@@ -603,6 +610,7 @@
ECA738DC2BE6108D00A4542E /* StrokeRenderPass.swift in Sources */, ECA738DC2BE6108D00A4542E /* StrokeRenderPass.swift in Sources */,
ECA738F42BE612A000A4542E /* Array++.swift in Sources */, ECA738F42BE612A000A4542E /* Array++.swift in Sources */,
ECA739052BE61E3100A4542E /* Memo.swift in Sources */, ECA739052BE61E3100A4542E /* Memo.swift in Sources */,
EC4538892BEBCAE000A86FEC /* Quad.swift in Sources */,
ECA7388F2BE600DA00A4542E /* Grid.metal in Sources */, ECA7388F2BE600DA00A4542E /* Grid.metal in Sources */,
ECA738C92BE60EF700A4542E /* GraphicContext.swift in Sources */, ECA738C92BE60EF700A4542E /* GraphicContext.swift in Sources */,
ECA738F62BE612B700A4542E /* MTLDevice++.swift in Sources */, ECA738F62BE612B700A4542E /* MTLDevice++.swift in Sources */,
@@ -620,7 +628,7 @@
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 /* Quad.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

@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1530"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "EC7F6BE72BE5E6E300A34A7B"
BuildableName = "Memola.app"
BlueprintName = "Memola"
ReferencedContainer = "container:Memola.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "EC7F6BE72BE5E6E300A34A7B"
BuildableName = "Memola.app"
BlueprintName = "Memola"
ReferencedContainer = "container:Memola.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "EC7F6BE72BE5E6E300A34A7B"
BuildableName = "Memola.app"
BlueprintName = "Memola"
ReferencedContainer = "container:Memola.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -7,45 +7,28 @@
import Combine import Combine
import MetalKit import MetalKit
import CoreData
import Foundation import Foundation
protocol GraphicContextDelegate: AnyObject { @objc(GraphicContext)
var didUpdate: PassthroughSubject<Void, Never> { get set } class GraphicContext: NSManagedObject {
} @NSManaged var id: UUID
@NSManaged var canvas: Canvas
@NSManaged var strokes: NSMutableOrderedSet
class GraphicContext: Codable {
var strokes: [Stroke] = []
var currentStroke: Stroke? var currentStroke: Stroke?
var previousStroke: Stroke? var previousStroke: Stroke?
var currentPoint: CGPoint? var currentPoint: CGPoint?
var renderType: RenderType = .finished var renderType: RenderType = .finished
var vertices: [ViewPortVertex] = [] var vertices: [ViewPortVertex] = []
var vertexCount: Int = 4 var vertexCount: Int = 4
var vertexBuffer: MTLBuffer? var vertexBuffer: MTLBuffer?
weak var delegate: GraphicContextDelegate? override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) {
super.init(entity: entity, insertInto: context)
init() {
setViewPortVertices() setViewPortVertices()
} }
enum CodingKeys: CodingKey {
case strokes
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.strokes = try container.decode([Stroke].self, forKey: .strokes)
setViewPortVertices()
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.strokes, forKey: .strokes)
}
func setViewPortVertices() { func setViewPortVertices() {
vertexBuffer = nil vertexBuffer = nil
vertices = [ vertices = [
@@ -57,19 +40,17 @@ class GraphicContext: Codable {
} }
func undoGraphic() { func undoGraphic() {
guard !strokes.isEmpty else { return } guard let stroke = strokes.lastObject as? Stroke else { return }
strokes.removeLast() strokes.remove(stroke)
previousStroke = nil previousStroke = nil
delegate?.didUpdate.send()
} }
func redoGraphic(for event: HistoryEvent) { func redoGraphic(for event: HistoryEvent) {
switch event { switch event {
case .stroke(let stroke): case .stroke(let stroke):
strokes.append(stroke) strokes.add(stroke)
previousStroke = nil previousStroke = nil
} }
delegate?.didUpdate.send()
} }
} }
@@ -91,12 +72,15 @@ 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( let stroke = Stroke(context: Persistence.shared.viewContext)
color: pen.color, stroke.id = UUID()
style: pen.style, stroke.color = pen.color
thickness: pen.thickness stroke.style = pen.strokeStyle.rawValue
) stroke.thickness = pen.thickness
strokes.append(stroke) stroke.createdAt = .now
stroke.strokeQuads = []
stroke.graphicContext = self
strokes.add(stroke)
currentStroke = stroke currentStroke = stroke
currentPoint = point currentPoint = point
currentStroke?.begin(at: point) currentStroke?.begin(at: point)
@@ -105,23 +89,28 @@ extension GraphicContext {
func appendStroke(with point: CGPoint) { func appendStroke(with point: CGPoint) {
guard let currentStroke else { return } guard let currentStroke else { return }
guard let currentPoint, point.distance(to: currentPoint) > currentStroke.thickness * currentStroke.style.stepRate else { return } guard let currentPoint, point.distance(to: currentPoint) > currentStroke.thickness * currentStroke.penStyle.anyPenStyle.stepRate else { return }
currentStroke.append(to: point) currentStroke.append(to: point)
self.currentPoint = point self.currentPoint = point
} }
func endStroke(at point: CGPoint) { func endStroke(at point: CGPoint) {
guard currentPoint != nil else { return } guard currentPoint != nil, let currentStroke else { return }
currentStroke?.finish(at: point) currentStroke.finish(at: point)
currentStroke.saveQuads()
do {
try Persistence.shared.viewContext.save()
} catch {
NSLog("[Memola] - \(error.localizedDescription)")
}
previousStroke = currentStroke previousStroke = currentStroke
currentStroke = nil self.currentStroke = nil
self.currentPoint = nil self.currentPoint = nil
delegate?.didUpdate.send()
} }
func cancelStroke() { func cancelStroke() {
if !strokes.isEmpty { if let stroke = strokes.lastObject {
strokes.removeLast() strokes.remove(stroke)
} }
currentStroke = nil currentStroke = nil
currentPoint = nil currentPoint = nil

View File

@@ -10,92 +10,43 @@ import CoreData
import MetalKit import MetalKit
import Foundation import Foundation
final class Canvas: NSObject, ObservableObject, Identifiable, Codable, GraphicContextDelegate { @objc(Canvas)
let size: CGSize class Canvas: NSManagedObject, Identifiable {
@NSManaged var id: UUID
@NSManaged var width: CGFloat
@NSManaged var height: CGFloat
@NSManaged var memo: Memo
@NSManaged var graphicContext: GraphicContext
let gridContext = GridContext()
let viewPortContext = ViewPortContext()
let maximumZoomScale: CGFloat = 25 let maximumZoomScale: CGFloat = 25
let minimumZoomScale: CGFloat = 3.1 let minimumZoomScale: CGFloat = 3.1
var transform: simd_float4x4 = .init() var transform: simd_float4x4 = .init()
var uniformsBuffer: MTLBuffer?
let gridContext = GridContext()
var graphicContext = GraphicContext()
let viewPortContext = ViewPortContext()
var clipBounds: CGRect = .zero var clipBounds: CGRect = .zero
var zoomScale: CGFloat = .zero var zoomScale: CGFloat = .zero
var uniformsBuffer: MTLBuffer?
weak var memo: Memo?
var graphicLoader: (() throws -> GraphicContext)?
@Published var state: State = .initial @Published var state: State = .initial
lazy var didUpdate = PassthroughSubject<Void, Never>()
var size: CGSize { CGSize(width: width, height: height) }
var hasValidStroke: Bool { var hasValidStroke: Bool {
if let currentStroke = graphicContext.currentStroke { if let currentStroke = graphicContext.currentStroke {
return Date.now.timeIntervalSince(currentStroke.createdAt) * 1000 > 80 return Date.now.timeIntervalSince(currentStroke.createdAt) * 1000 > 80
} }
return false return false
} }
init(size: CGSize = .init(width: 4_000, height: 4_000)) {
self.size = size
}
enum CodingKeys: CodingKey {
case size
case graphicContext
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.size = try container.decode(CGSize.self, forKey: .size)
self.graphicLoader = { try container.decode(GraphicContext.self, forKey: .graphicContext) }
}
} }
// MARK: - Actions // MARK: - Actions
extension Canvas { extension Canvas {
func load() { func load() {
guard let graphicLoader else { return } state = .loading
Task(priority: .high) { [unowned self, graphicLoader] in graphicContext.strokes.forEach {
await MainActor.run { ($0 as? Stroke)?.loadVertices()
self.state = .loading
}
do {
let graphicContext = try graphicLoader()
graphicContext.delegate = self
await MainActor.run {
self.graphicContext = graphicContext
self.state = .loaded
}
} catch {
NSLog("[Memola] - \(error.localizedDescription)")
await MainActor.run {
self.state = .failed
}
}
} }
} state = .loaded
func save(on managedObjectContext: NSManagedObjectContext) async {
guard let memo else { return }
do {
memo.data = try JSONEncoder().encode(self)
memo.updatedAt = Date()
try managedObjectContext.save()
} catch {
NSLog("[Memola] - \(error.localizedDescription)")
}
}
func listen(on managedObjectContext: NSManagedObjectContext) {
// Task(priority: .utility) { [unowned self] in
// for await _ in didUpdate.throttle(for: 500, scheduler: DispatchQueue.global(qos: .utility), latest: false).values {
// await save(on: managedObjectContext)
// }
// }
} }
} }
@@ -148,7 +99,7 @@ extension Canvas {
} }
func getNewlyAddedStroke() -> Stroke? { func getNewlyAddedStroke() -> Stroke? {
graphicContext.strokes.last graphicContext.strokes.lastObject as? Stroke
} }
} }

View File

@@ -2,31 +2,27 @@
// Quad.swift // Quad.swift
// Memola // Memola
// //
// Created by Dscyre Scotti on 5/4/24. // Created by Dscyre Scotti on 5/8/24.
// //
import MetalKit
import Foundation import Foundation
class Quad { struct Quad: Codable {
var origin: CGPoint var origin: CGPoint
var color: [CGFloat] var color: [CGFloat]
var size: CGFloat var size: CGFloat
var rotation: CGFloat var rotation: CGFloat
var vertices: [QuadVertex] = [] var shape: QuadShape
var vertexBuffer: MTLBuffer? init(origin: CGPoint, size: CGFloat, color: [CGFloat], rotation: CGFloat, shape: QuadShape = .rounded) {
var vertexCount: Int = 0
init(origin: CGPoint, size: CGFloat, color: [CGFloat], rotation: CGFloat, shape: Shape = .rounded) {
self.origin = origin self.origin = origin
self.size = size self.size = size
self.color = color self.color = color
self.rotation = rotation self.rotation = rotation
generateVertices(shape) self.shape = shape
} }
func generateVertices(_ shape: Shape) { func generateVertices(_ shape: QuadShape) -> [QuadVertex] {
switch shape { switch shape {
case .rounded: case .rounded:
generateRoundedQuad() generateRoundedQuad()
@@ -39,9 +35,9 @@ class Quad {
} }
} }
func generateRoundedQuad() { func generateRoundedQuad() -> [QuadVertex] {
let halfSize = size * 0.5 let halfSize = size * 0.5
vertices = [ 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),
QuadVertex(x: origin.x + halfSize, y: origin.y - halfSize, textCoord: CGPoint(x: 1, y: 0), color: color, origin: origin, rotation: rotation), QuadVertex(x: origin.x + halfSize, y: origin.y - halfSize, textCoord: CGPoint(x: 1, y: 0), color: color, origin: origin, rotation: rotation),
QuadVertex(x: origin.x - halfSize, y: origin.y + halfSize, textCoord: CGPoint(x: 0, y: 1), color: color, origin: origin, rotation: rotation), QuadVertex(x: origin.x - halfSize, y: origin.y + halfSize, textCoord: CGPoint(x: 0, y: 1), color: color, origin: origin, rotation: rotation),
@@ -49,13 +45,12 @@ class Quad {
QuadVertex(x: origin.x - halfSize, y: origin.y + halfSize, textCoord: CGPoint(x: 0, y: 1), color: color, origin: origin, rotation: rotation), QuadVertex(x: origin.x - halfSize, y: origin.y + halfSize, textCoord: CGPoint(x: 0, y: 1), color: color, origin: origin, rotation: rotation),
QuadVertex(x: origin.x + halfSize, y: origin.y + halfSize, textCoord: CGPoint(x: 1, y: 1), color: color, origin: origin, rotation: rotation) QuadVertex(x: origin.x + halfSize, y: origin.y + halfSize, textCoord: CGPoint(x: 1, y: 1), color: color, origin: origin, rotation: rotation)
] ]
vertexCount = vertices.count
} }
func generateSquaredQuad() { func generateSquaredQuad() -> [QuadVertex] {
let vHalfSize = size * 0.5 let vHalfSize = size * 0.5
let hHalfSize = size * 0.15 let hHalfSize = size * 0.15
vertices = [ 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: 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: 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: 0, y: 1), color: color, origin: origin, rotation: rotation),
@@ -63,13 +58,12 @@ class Quad {
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: 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) QuadVertex(x: origin.x + hHalfSize, y: origin.y + vHalfSize, textCoord: CGPoint(x: 1, y: 1), color: color, origin: origin, rotation: rotation)
] ]
vertexCount = vertices.count
} }
func generateCalligraphicQuad(vFactor: CGFloat, hFactor: CGFloat) { func generateCalligraphicQuad(vFactor: CGFloat, hFactor: CGFloat) -> [QuadVertex] {
let vHalfSize = size * vFactor * 0.5 let vHalfSize = size * vFactor * 0.5
let hHalfSize = size * hFactor * 0.5 let hHalfSize = size * hFactor * 0.5
vertices = [ 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: 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: 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: 0, y: 1), color: color, origin: origin, rotation: rotation),
@@ -77,14 +71,13 @@ class Quad {
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: 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) QuadVertex(x: origin.x + hHalfSize, y: origin.y + vHalfSize, textCoord: CGPoint(x: 1, y: 1), color: color, origin: origin, rotation: rotation)
] ]
vertexCount = vertices.count
} }
func generateTrapezoidQuad(topFactor: CGFloat, bottomFactor: CGFloat, heightFactor: CGFloat) { func generateTrapezoidQuad(topFactor: CGFloat, bottomFactor: CGFloat, heightFactor: CGFloat) -> [QuadVertex] {
let vHalfSize = size * heightFactor * 0.5 let vHalfSize = size * heightFactor * 0.5
let hTopHalfSize = size * topFactor * 0.5 let hTopHalfSize = size * topFactor * 0.5
let hBottomHalfSize = size * bottomFactor * 0.5 let hBottomHalfSize = size * bottomFactor * 0.5
vertices = [ 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 - 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 + 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 - hTopHalfSize, y: origin.y + vHalfSize, textCoord: CGPoint(x: 0, y: 1), color: color, origin: origin, rotation: rotation),
@@ -92,15 +85,12 @@ class Quad {
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 - 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) QuadVertex(x: origin.x + hBottomHalfSize, y: origin.y + vHalfSize, textCoord: CGPoint(x: 1, y: 1), color: color, origin: origin, rotation: rotation)
] ]
vertexCount = vertices.count
} }
} }
extension Quad { enum QuadShape: Codable {
enum Shape { case rounded
case rounded case squared
case squared case calligraphic(CGFloat, CGFloat)
case calligraphic(CGFloat, CGFloat) case trapezoid(topFactor: CGFloat, bottomFactor: CGFloat, heightFactor: CGFloat)
case trapezoid(topFactor: CGFloat, bottomFactor: CGFloat, heightFactor: CGFloat)
}
} }

View File

@@ -0,0 +1,18 @@
//
// StrokeQuad.swift
// Memola
//
// Created by Dscyre Scotti on 5/4/24.
//
import MetalKit
import Foundation
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:
discardPoints(upto: stroke.vertexIndex, on: stroke) discardVertices(upto: stroke.vertexIndex, 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) {
discardPoints(upto: stroke.vertexIndex, on: stroke) discardVertices(upto: stroke.vertexIndex, on: stroke)
adjustPreviousKeyPoint(on: stroke) adjustPreviousKeyPoint(on: stroke)
switch stroke.keyPoints.count { switch stroke.keyPoints.count {
case 4: case 4:
@@ -106,7 +106,8 @@ struct SolidPointStrokeGenerator: StrokeGenerator {
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 = Quad(origin: point, size: stroke.thickness, color: stroke.color, rotation: rotation)
stroke.vertices.append(contentsOf: quad.vertices) stroke._quads.append(quad)
stroke.vertices.append(contentsOf: quad.generateVertices(quad.shape))
stroke.vertexCount = stroke.vertices.endIndex stroke.vertexCount = stroke.vertices.endIndex
} }
@@ -115,9 +116,9 @@ struct SolidPointStrokeGenerator: StrokeGenerator {
let factor: CGFloat let factor: CGFloat
switch configuration.granularity { switch configuration.granularity {
case .automatic: case .automatic:
factor = min(6, 1 / (stroke.thickness * 10 / 500)) factor = min(3.5, 1 / (stroke.thickness * 10 / 300))
case .fixed: case .fixed:
factor = 1 / (stroke.thickness * stroke.style.stepRate) factor = 1 / (stroke.thickness * stroke.penStyle.anyPenStyle.stepRate)
case .none: case .none:
factor = 1 / (stroke.thickness * 10 / 500) factor = 1 / (stroke.thickness * 10 / 500)
} }
@@ -134,7 +135,7 @@ struct SolidPointStrokeGenerator: StrokeGenerator {
#warning("TODO: remove later") #warning("TODO: remove later")
private func addLine(from start: CGPoint, to end: CGPoint, on stroke: Stroke) { private func addLine(from start: CGPoint, to end: CGPoint, on stroke: Stroke) {
let distance = end.distance(to: start) let distance = end.distance(to: start)
let segments = max(distance / stroke.style.stepRate, 2) let segments = max(distance / stroke.penStyle.anyPenStyle.stepRate, 2)
for i in 0..<Int(segments) { for i in 0..<Int(segments) {
let i = CGFloat(i) let i = CGFloat(i)
let x = start.x + (end.x - start.x) * (i / segments) let x = start.x + (end.x - start.x) * (i / segments)
@@ -144,13 +145,15 @@ struct SolidPointStrokeGenerator: StrokeGenerator {
} }
} }
private func discardPoints(upto index: Int, on stroke: Stroke) { private func discardVertices(upto index: Int, on stroke: Stroke) {
if index < 0 { if index < 0 {
stroke.vertices.removeAll() stroke.vertices.removeAll()
stroke._quads.removeAll()
} 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)
} }
} }
} }

View File

@@ -6,24 +6,35 @@
// //
import MetalKit import MetalKit
import CoreData
import Foundation import Foundation
class Stroke: Codable { @objc(Stroke)
var color: [CGFloat] class Stroke: NSManagedObject {
var style: any PenStyle @NSManaged var id: UUID
var thickness: CGFloat @NSManaged var color: [CGFloat]
@NSManaged var style: Int16
@NSManaged var createdAt: Date
@NSManaged var thickness: CGFloat
@NSManaged var strokeQuads: Array<StrokeQuad>
@NSManaged var graphicContext: GraphicContext?
var angle: CGFloat = 0 var angle: CGFloat = 0
var penStyle: Style {
Style(rawValue: style) ?? .marker
}
var quadIndex: Int = -1
var vertexIndex: Int = -1 var vertexIndex: Int = -1
var keyPoints: [CGPoint] = [] var keyPoints: [CGPoint] = []
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
let createdAt: Date = Date()
var texture: MTLTexture? var texture: MTLTexture?
var isEmpty: Bool { var isEmpty: Bool {
@@ -31,73 +42,38 @@ class Stroke: Codable {
} }
var isEraserPenStyle: Bool { var isEraserPenStyle: Bool {
style is EraserPenStyle penStyle == .eraser
}
init(color: [CGFloat], style: any PenStyle, thickness: CGFloat) {
self.color = color
self.style = style
self.thickness = thickness
}
enum CodingKeys: CodingKey {
case color
case style
case thickness
case vertices
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
color = try container.decode([CGFloat].self, forKey: .color)
let style: String = try container.decode(String.self, forKey: .style)
thickness = try container.decode(CGFloat.self, forKey: .thickness)
vertices = try container.decode([QuadVertex].self, forKey: .vertices)
vertexCount = vertices.count
switch style {
case "marker":
self.style = .marker
case "eraser":
self.style = .eraser
default:
throw DecodingError.valueNotFound(PenStyle.self, .init(codingPath: [CodingKeys.style], debugDescription: "There is no pen style called `\(style)`."))
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(color, forKey: .color)
try container.encode(thickness, forKey: .thickness)
try container.encode(vertices, forKey: .vertices)
let styleName: String
switch style {
case is MarkerPenStyle:
styleName = "marker"
case is EraserPenStyle:
styleName = "eraser"
default:
fatalError()
}
try container.encode(styleName, forKey: .style)
} }
func begin(at point: CGPoint) { func begin(at point: CGPoint) {
style.generator.begin(at: point, on: self) penStyle.anyPenStyle.generator.begin(at: point, on: self)
} }
func append(to point: CGPoint) { func append(to point: CGPoint) {
style.generator.append(to: point, on: self) penStyle.anyPenStyle.generator.append(to: point, on: self)
} }
func finish(at point: CGPoint) { func finish(at point: CGPoint) {
style.generator.finish(at: point, on: self) penStyle.anyPenStyle.generator.finish(at: point, on: self)
keyPoints.removeAll()
}
func loadVertices() {
vertices = strokeQuads
.flatMap { $0.quad.generateVertices($0.quad.shape) }
vertexCount = vertices.endIndex
}
func saveQuads() {
strokeQuads = _quads.map(StrokeQuad.init)
_quads.removeAll()
} }
} }
extension Stroke: Drawable { extension Stroke: Drawable {
func prepare(device: MTLDevice) { func prepare(device: MTLDevice) {
if texture == nil { if texture == nil {
texture = style.loadTexture(on: device) texture = penStyle.anyPenStyle.loadTexture(on: device)
} }
vertexBuffer = device.makeBuffer(bytes: &vertices, length: MemoryLayout<QuadVertex>.stride * vertexCount, options: .cpuCacheModeWriteCombined) vertexBuffer = device.makeBuffer(bytes: &vertices, length: MemoryLayout<QuadVertex>.stride * vertexCount, options: .cpuCacheModeWriteCombined)
} }
@@ -110,3 +86,19 @@ extension Stroke: Drawable {
renderEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: vertexCount) renderEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: vertexCount)
} }
} }
extension Stroke {
enum Style: Int16 {
case marker
case eraser
var anyPenStyle: any PenStyle {
switch self {
case .marker:
return MarkerPenStyle.marker
case .eraser:
return EraserPenStyle.eraser
}
}
}
}

View File

@@ -45,7 +45,8 @@ class GraphicRenderPass: RenderPass {
if renderer.redrawsGraphicRender { if renderer.redrawsGraphicRender {
canvas.setGraphicRenderType(.finished) canvas.setGraphicRenderType(.finished)
for stroke in graphicContext.strokes { for stroke in graphicContext.strokes.array {
guard let stroke = stroke as? Stroke else { continue }
if graphicContext.previousStroke === stroke || graphicContext.currentStroke === stroke { if graphicContext.previousStroke === stroke || graphicContext.currentStroke === stroke {
continue continue
} }

View File

@@ -9,7 +9,7 @@ import SwiftUI
import Foundation import Foundation
class Pen: NSObject, ObservableObject, Identifiable { class Pen: NSObject, ObservableObject, Identifiable {
@Published var style: PenStyle @Published var style: any PenStyle
@Published var color: [CGFloat] @Published var color: [CGFloat]
@Published var thickness: CGFloat @Published var thickness: CGFloat
@@ -18,6 +18,17 @@ class Pen: NSObject, ObservableObject, Identifiable {
self.color = color self.color = color
self.thickness = thickness self.thickness = thickness
} }
var strokeStyle: Stroke.Style {
switch style {
case is MarkerPenStyle:
return .marker
case is EraserPenStyle:
return .eraser
default:
return .marker
}
}
} }
extension Pen { extension Pen {

View File

@@ -8,8 +8,8 @@
import SwiftUI import SwiftUI
struct CanvasView: UIViewControllerRepresentable { struct CanvasView: UIViewControllerRepresentable {
let canvas: Canvas
@EnvironmentObject var tool: Tool @EnvironmentObject var tool: Tool
@EnvironmentObject var canvas: Canvas
@EnvironmentObject var history: History @EnvironmentObject var history: History
func makeUIViewController(context: Context) -> CanvasViewController { func makeUIViewController(context: Context) -> CanvasViewController {

View File

@@ -15,6 +15,7 @@ class Memo: NSManagedObject {
@NSManaged var data: Data @NSManaged var data: Data
@NSManaged var createdAt: Date @NSManaged var createdAt: Date
@NSManaged var updatedAt: Date @NSManaged var updatedAt: Date
@NSManaged var canvas: Canvas
} }
extension Memo: Identifiable { } extension Memo: Identifiable { }

View File

@@ -14,10 +14,10 @@ struct MemoView: View {
@StateObject var tool = Tool() @StateObject var tool = Tool()
@StateObject var history = History() @StateObject var history = History()
@EnvironmentObject var canvas: Canvas let canvas: Canvas
var body: some View { var body: some View {
CanvasView() CanvasView(canvas: canvas)
.ignoresSafeArea() .ignoresSafeArea()
.overlay(alignment: .bottomTrailing) { .overlay(alignment: .bottomTrailing) {
PenToolView() PenToolView()
@@ -53,9 +53,6 @@ struct MemoView: View {
.environmentObject(tool) .environmentObject(tool)
.environmentObject(canvas) .environmentObject(canvas)
.environmentObject(history) .environmentObject(history)
.task {
canvas.listen(on: managedObjectContext)
}
} }
var historyTool: some View { var historyTool: some View {
@@ -91,15 +88,13 @@ struct MemoView: View {
} }
func closeMemo() { func closeMemo() {
Task(priority: .high) { if managedObjectContext.hasChanges {
await MainActor.run { do {
canvas.state = .closing try managedObjectContext.save()
} } catch {
await canvas.save(on: managedObjectContext) NSLog("[Memola] - \(error.localizedDescription)")
await MainActor.run {
canvas.state = .closed
dismiss()
} }
} }
dismiss()
} }
} }

View File

@@ -12,7 +12,7 @@ struct MemosView: View {
@FetchRequest(sortDescriptors: []) var memos: FetchedResults<Memo> @FetchRequest(sortDescriptors: []) var memos: FetchedResults<Memo>
@State var canvas: Canvas? @State var memo: Memo?
var body: some View { var body: some View {
NavigationStack { NavigationStack {
@@ -29,9 +29,8 @@ struct MemosView: View {
} }
} }
} }
.fullScreenCover(item: $canvas) { canvas in .fullScreenCover(item: $memo) { memo in
MemoView() MemoView(canvas: memo.canvas)
.environmentObject(canvas)
} }
} }
@@ -59,29 +58,34 @@ struct MemosView: View {
func createMemo(title: String) { func createMemo(title: String) {
do { do {
let data = try JSONEncoder().encode(Canvas())
let memo = Memo(context: managedObjectContext) let memo = Memo(context: managedObjectContext)
memo.id = UUID() memo.id = UUID()
memo.title = title memo.title = title
memo.data = data
memo.createdAt = .now memo.createdAt = .now
memo.updatedAt = .now memo.updatedAt = .now
let canvas = Canvas(context: managedObjectContext)
canvas.id = UUID()
canvas.width = 4_000
canvas.height = 4_000
let graphicContext = GraphicContext(context: managedObjectContext)
graphicContext.id = UUID()
graphicContext.strokes = []
memo.canvas = canvas
canvas.memo = memo
canvas.graphicContext = graphicContext
graphicContext.canvas = canvas
try managedObjectContext.save() try managedObjectContext.save()
openMemo(for: memo) openMemo(for: memo)
} catch { } catch {
NSLog("[SketchNote] - \(error.localizedDescription)") NSLog("[Memola] - \(error.localizedDescription)")
} }
} }
func openMemo(for memo: Memo) { func openMemo(for memo: Memo) {
do { self.memo = memo
let data = memo.data
let canvas = try JSONDecoder().decode(Canvas.self, from: data)
canvas.memo = memo
self.canvas = canvas
} catch {
NSLog("[SketchNote] - \(error.localizedDescription)")
}
} }
} }

View File

@@ -13,12 +13,14 @@ class Persistence {
static let shared: Persistence = Persistence() static let shared: Persistence = Persistence()
private init() { } private init() {
QuadValueTransformer.register()
var viewContext: NSManagedObjectContext {
persistentContainer.viewContext
} }
lazy var viewContext: NSManagedObjectContext = {
persistentContainer.viewContext
}()
lazy var persistentContainer: NSPersistentContainer = { lazy var persistentContainer: NSPersistentContainer = {
let persistentStore = NSPersistentStoreDescription() let persistentStore = NSPersistentStoreDescription()
persistentStore.shouldMigrateStoreAutomatically = true persistentStore.shouldMigrateStoreAutomatically = true

View File

@@ -0,0 +1,53 @@
//
// 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)
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([StrokeQuad].self, from: data)
return quads
} 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

@@ -1,10 +1,31 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22222" systemVersion="23B74" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithSwiftData="YES" userDefinedModelVersionIdentifier=""> <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">
<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" maxCount="1" deletionRule="Cascade" destinationEntity="GraphicContext" inverseName="canvas" inverseEntity="GraphicContext"/>
<relationship name="memo" maxCount="1" deletionRule="Deny" destinationEntity="Memo" inverseName="canvas" inverseEntity="Memo"/>
</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>
<entity name="Memo" representedClassName="Memo" syncable="YES"> <entity name="Memo" representedClassName="Memo" syncable="YES">
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/> <attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="data" attributeType="Binary" allowsExternalBinaryDataStorage="YES"/>
<attribute name="id" attributeType="UUID" usesScalarValueType="NO"/> <attribute name="id" attributeType="UUID" usesScalarValueType="NO"/>
<attribute name="title" attributeType="String"/> <attribute name="title" attributeType="String"/>
<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"/>
</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" optional="YES" 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"/>
</entity> </entity>
</model> </model>