mirror of
https://github.com/dscyrescotti/Memola.git
synced 2026-07-08 22:05:09 +02:00
Merge pull request #42 from dscyrescotti/feature/eraser-optimization
Optimize eraser stroke generation
This commit is contained in:
@@ -28,6 +28,7 @@
|
|||||||
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 */; };
|
||||||
|
EC9AB09F2C1401A40076AF58 /* EraserObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC9AB09E2C1401A40076AF58 /* EraserObject.swift */; };
|
||||||
ECA7387A2BE5EF0400A4542E /* MemosView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738792BE5EF0400A4542E /* MemosView.swift */; };
|
ECA7387A2BE5EF0400A4542E /* MemosView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738792BE5EF0400A4542E /* MemosView.swift */; };
|
||||||
ECA7387D2BE5EF4B00A4542E /* MemoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA7387C2BE5EF4B00A4542E /* MemoView.swift */; };
|
ECA7387D2BE5EF4B00A4542E /* MemoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA7387C2BE5EF4B00A4542E /* MemoView.swift */; };
|
||||||
ECA738832BE5FEFE00A4542E /* RenderPass.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738822BE5FEFE00A4542E /* RenderPass.swift */; };
|
ECA738832BE5FEFE00A4542E /* RenderPass.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738822BE5FEFE00A4542E /* RenderPass.swift */; };
|
||||||
@@ -117,6 +118,7 @@
|
|||||||
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>"; };
|
||||||
EC7F6BF22BE5E6E400A34A7B /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
|
EC7F6BF22BE5E6E400A34A7B /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
|
||||||
|
EC9AB09E2C1401A40076AF58 /* EraserObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EraserObject.swift; sourceTree = "<group>"; };
|
||||||
ECA738792BE5EF0400A4542E /* MemosView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemosView.swift; sourceTree = "<group>"; };
|
ECA738792BE5EF0400A4542E /* MemosView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemosView.swift; sourceTree = "<group>"; };
|
||||||
ECA7387C2BE5EF4B00A4542E /* MemoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoView.swift; sourceTree = "<group>"; };
|
ECA7387C2BE5EF4B00A4542E /* MemoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoView.swift; sourceTree = "<group>"; };
|
||||||
ECA738822BE5FEFE00A4542E /* RenderPass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RenderPass.swift; sourceTree = "<group>"; };
|
ECA738822BE5FEFE00A4542E /* RenderPass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RenderPass.swift; sourceTree = "<group>"; };
|
||||||
@@ -673,6 +675,7 @@
|
|||||||
ECFA15272BEF225000455818 /* QuadObject.swift */,
|
ECFA15272BEF225000455818 /* QuadObject.swift */,
|
||||||
EC0D14202BF79C73009BFE5F /* ToolObject.swift */,
|
EC0D14202BF79C73009BFE5F /* ToolObject.swift */,
|
||||||
EC0D14252BF7A8C9009BFE5F /* PenObject.swift */,
|
EC0D14252BF7A8C9009BFE5F /* PenObject.swift */,
|
||||||
|
EC9AB09E2C1401A40076AF58 /* EraserObject.swift */,
|
||||||
);
|
);
|
||||||
path = Objects;
|
path = Objects;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -820,6 +823,7 @@
|
|||||||
ECA738F62BE612B700A4542E /* MTLDevice++.swift in Sources */,
|
ECA738F62BE612B700A4542E /* MTLDevice++.swift in Sources */,
|
||||||
EC0D14212BF79C73009BFE5F /* ToolObject.swift in Sources */,
|
EC0D14212BF79C73009BFE5F /* ToolObject.swift in Sources */,
|
||||||
EC50500D2BF6674400B4D86E /* OnDragViewModifier.swift in Sources */,
|
EC50500D2BF6674400B4D86E /* OnDragViewModifier.swift in Sources */,
|
||||||
|
EC9AB09F2C1401A40076AF58 /* EraserObject.swift in Sources */,
|
||||||
ECA7389E2BE601CB00A4542E /* QuadVertex.swift in Sources */,
|
ECA7389E2BE601CB00A4542E /* QuadVertex.swift in Sources */,
|
||||||
ECA738B32BE60D9E00A4542E /* CanvasView.swift in Sources */,
|
ECA738B32BE60D9E00A4542E /* CanvasView.swift in Sources */,
|
||||||
ECA738C42BE60E8800A4542E /* MarkerPenStyle.swift in Sources */,
|
ECA738C42BE60E8800A4542E /* MarkerPenStyle.swift in Sources */,
|
||||||
|
|||||||
@@ -50,6 +50,12 @@
|
|||||||
ReferencedContainer = "container:Memola.xcodeproj">
|
ReferencedContainer = "container:Memola.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</BuildableProductRunnable>
|
</BuildableProductRunnable>
|
||||||
|
<CommandLineArguments>
|
||||||
|
<CommandLineArgument
|
||||||
|
argument = "-com.apple.CoreData.SQLDebug 1"
|
||||||
|
isEnabled = "NO">
|
||||||
|
</CommandLineArgument>
|
||||||
|
</CommandLineArguments>
|
||||||
</LaunchAction>
|
</LaunchAction>
|
||||||
<ProfileAction
|
<ProfileAction
|
||||||
buildConfiguration = "Release"
|
buildConfiguration = "Release"
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import Foundation
|
|||||||
|
|
||||||
final class GraphicContext: @unchecked Sendable {
|
final class GraphicContext: @unchecked Sendable {
|
||||||
var tree: RTree = RTree<AnyStroke>(maxEntries: 8)
|
var tree: RTree = RTree<AnyStroke>(maxEntries: 8)
|
||||||
|
var eraserStrokes: Set<EraserStroke> = []
|
||||||
var object: GraphicContextObject?
|
var object: GraphicContextObject?
|
||||||
|
|
||||||
var currentStroke: (any Stroke)?
|
var currentStroke: (any Stroke)?
|
||||||
@@ -23,6 +24,10 @@ final class GraphicContext: @unchecked Sendable {
|
|||||||
var vertexCount: Int = 4
|
var vertexCount: Int = 4
|
||||||
var vertexBuffer: MTLBuffer?
|
var vertexBuffer: MTLBuffer?
|
||||||
|
|
||||||
|
var erasers: [URL: EraserStroke] = [:]
|
||||||
|
|
||||||
|
let barrierQueue = DispatchQueue(label: "com.memola.app.graphic-context", attributes: .concurrent)
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
setViewPortVertices()
|
setViewPortVertices()
|
||||||
}
|
}
|
||||||
@@ -40,26 +45,64 @@ final class GraphicContext: @unchecked Sendable {
|
|||||||
func undoGraphic(for event: HistoryEvent) {
|
func undoGraphic(for event: HistoryEvent) {
|
||||||
switch event {
|
switch event {
|
||||||
case .stroke(let stroke):
|
case .stroke(let stroke):
|
||||||
guard let _stroke = stroke.stroke(as: PenStroke.self) else { return }
|
switch stroke.style {
|
||||||
let deletedStroke = tree.remove(_stroke.anyStroke, in: _stroke.strokeBox)
|
case .marker:
|
||||||
withPersistence(\.backgroundContext) { [stroke = deletedStroke] context in
|
guard let penStroke = stroke.stroke(as: PenStroke.self) else { return }
|
||||||
stroke?.stroke(as: PenStroke.self)?.object?.graphicContext = nil
|
tree.remove(penStroke.anyStroke, in: penStroke.strokeBox)
|
||||||
try context.saveIfNeeded()
|
withPersistence(\.backgroundContext) { [weak penStroke] context in
|
||||||
|
penStroke?.object?.graphicContext = nil
|
||||||
|
try context.saveIfNeeded()
|
||||||
|
context.refreshAllObjects()
|
||||||
|
}
|
||||||
|
case .eraser:
|
||||||
|
guard let eraserStroke = stroke.stroke(as: EraserStroke.self) else { return }
|
||||||
|
eraserStrokes.remove(eraserStroke)
|
||||||
|
withPersistence(\.backgroundContext) { [weak eraserStroke] context in
|
||||||
|
guard let eraserStroke else { return }
|
||||||
|
for penStroke in eraserStroke.penStrokes.allObjects {
|
||||||
|
penStroke.eraserStrokes.remove(eraserStroke)
|
||||||
|
if let object = eraserStroke.object {
|
||||||
|
penStroke.object?.erasers.remove(object)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try context.saveIfNeeded()
|
||||||
|
context.refreshAllObjects()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
previousStroke = nil
|
previousStroke = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func redoGraphic(for event: HistoryEvent) {
|
func redoGraphic(for event: HistoryEvent) {
|
||||||
switch event {
|
switch event {
|
||||||
case .stroke(let stroke):
|
case .stroke(let stroke):
|
||||||
if let stroke = stroke.stroke(as: PenStroke.self) {
|
switch stroke.style {
|
||||||
tree.insert(stroke.anyStroke, in: stroke.strokeBox)
|
case .marker:
|
||||||
}
|
guard let penStroke = stroke.stroke(as: PenStroke.self) else {
|
||||||
withPersistence(\.backgroundContext) { [weak self, stroke] context in
|
break
|
||||||
stroke.stroke(as: PenStroke.self)?.object?.graphicContext = self?.object
|
}
|
||||||
try context.saveIfNeeded()
|
tree.insert(penStroke.anyStroke, in: penStroke.strokeBox)
|
||||||
|
withPersistence(\.backgroundContext) { [weak self, weak penStroke] context in
|
||||||
|
penStroke?.object?.graphicContext = self?.object
|
||||||
|
try context.saveIfNeeded()
|
||||||
|
context.refreshAllObjects()
|
||||||
|
}
|
||||||
|
case .eraser:
|
||||||
|
guard let eraserStroke = stroke.stroke(as: EraserStroke.self) else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
eraserStrokes.insert(eraserStroke)
|
||||||
|
withPersistence(\.backgroundContext) { [weak eraserStroke] context in
|
||||||
|
guard let eraserStroke else { return }
|
||||||
|
for penStroke in eraserStroke.penStrokes.allObjects {
|
||||||
|
penStroke.eraserStrokes.insert(eraserStroke)
|
||||||
|
if let object = eraserStroke.object {
|
||||||
|
penStroke.object?.erasers.add(object)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try context.saveIfNeeded()
|
||||||
|
context.refreshAllObjects()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
previousStroke = nil
|
previousStroke = nil
|
||||||
}
|
}
|
||||||
@@ -72,34 +115,34 @@ extension GraphicContext {
|
|||||||
let queue = OperationQueue()
|
let queue = OperationQueue()
|
||||||
queue.qualityOfService = .userInteractive
|
queue.qualityOfService = .userInteractive
|
||||||
object.strokes.forEach { stroke in
|
object.strokes.forEach { stroke in
|
||||||
guard let stroke = stroke as? StrokeObject else { return }
|
guard let stroke = stroke as? StrokeObject, stroke.style == 0 else { return }
|
||||||
let _stroke = PenStroke(object: stroke)
|
let _stroke = PenStroke(object: stroke)
|
||||||
tree.insert(_stroke.anyStroke, in: _stroke.strokeBox)
|
tree.insert(_stroke.anyStroke, in: _stroke.strokeBox)
|
||||||
if _stroke.isVisible(in: bounds) {
|
if _stroke.isVisible(in: bounds) {
|
||||||
let id = stroke.objectID
|
let id = stroke.objectID
|
||||||
queue.addOperation {
|
queue.addOperation { [weak self] in
|
||||||
withPersistenceSync(\.newBackgroundContext) { [_stroke] context in
|
guard let self else { return }
|
||||||
|
withPersistenceSync(\.newBackgroundContext) { [weak _stroke] context in
|
||||||
guard let stroke = try? context.existingObject(with: id) as? StrokeObject else { return }
|
guard let stroke = try? context.existingObject(with: id) as? StrokeObject else { return }
|
||||||
_stroke.loadQuads(from: stroke)
|
_stroke?.loadQuads(from: stroke, with: self)
|
||||||
}
|
context.refreshAllObjects()
|
||||||
withPersistence(\.backgroundContext) { [stroke] context in
|
|
||||||
context.refresh(stroke, mergeChanges: false)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
withPersistence(\.backgroundContext) { [stroke] context in
|
withPersistence(\.backgroundContext) { [weak self, weak _stroke] context in
|
||||||
_stroke.loadQuads()
|
guard let self else { return }
|
||||||
context.refresh(stroke, mergeChanges: false)
|
_stroke?.loadQuads(with: self)
|
||||||
|
context.refreshAllObjects()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
queue.waitUntilAllOperationsAreFinished()
|
queue.waitUntilAllOperationsAreFinished()
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadQuads(_ bounds: CGRect) {
|
func loadQuads(_ bounds: CGRect, on context: NSManagedObjectContext) {
|
||||||
for _stroke in self.tree.search(box: bounds.box) {
|
for _stroke in self.tree.search(box: bounds.box) {
|
||||||
guard let stroke = _stroke.stroke(as: PenStroke.self), stroke.isEmpty else { continue }
|
guard let stroke = _stroke.stroke(as: PenStroke.self), stroke.isEmpty else { continue }
|
||||||
stroke.loadQuads()
|
stroke.loadQuads(with: self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -122,24 +165,55 @@ extension GraphicContext: Drawable {
|
|||||||
|
|
||||||
extension GraphicContext {
|
extension GraphicContext {
|
||||||
func beginStroke(at point: CGPoint, pen: Pen) -> any Stroke {
|
func beginStroke(at point: CGPoint, pen: Pen) -> any Stroke {
|
||||||
let stroke = PenStroke(
|
let stroke: any Stroke
|
||||||
bounds: [point.x - pen.thickness, point.y - pen.thickness, point.x + pen.thickness, point.y + pen.thickness],
|
switch pen.strokeStyle {
|
||||||
color: pen.rgba,
|
case .marker:
|
||||||
style: pen.strokeStyle,
|
let penStroke = PenStroke(
|
||||||
createdAt: .now,
|
bounds: [point.x - pen.thickness, point.y - pen.thickness, point.x + pen.thickness, point.y + pen.thickness],
|
||||||
thickness: pen.thickness
|
color: pen.rgba,
|
||||||
)
|
style: pen.strokeStyle,
|
||||||
withPersistence(\.backgroundContext) { [graphicContext = object, _stroke = stroke] context in
|
createdAt: .now,
|
||||||
let stroke = StrokeObject(\.backgroundContext)
|
thickness: pen.thickness
|
||||||
stroke.bounds = _stroke.bounds
|
)
|
||||||
stroke.color = _stroke.color
|
withPersistence(\.backgroundContext) { [weak graphicContext = object, weak _stroke = penStroke] context in
|
||||||
stroke.style = _stroke.style.rawValue
|
guard let _stroke else { return }
|
||||||
stroke.thickness = _stroke.thickness
|
let stroke = StrokeObject(\.backgroundContext)
|
||||||
stroke.createdAt = _stroke.createdAt
|
stroke.bounds = _stroke.bounds
|
||||||
stroke.quads = []
|
stroke.color = _stroke.color
|
||||||
stroke.graphicContext = graphicContext
|
stroke.style = _stroke.style.rawValue
|
||||||
graphicContext?.strokes.add(stroke)
|
stroke.thickness = _stroke.thickness
|
||||||
_stroke.object = stroke
|
stroke.createdAt = _stroke.createdAt
|
||||||
|
stroke.quads = []
|
||||||
|
stroke.erasers = .init()
|
||||||
|
stroke.graphicContext = graphicContext
|
||||||
|
graphicContext?.strokes.add(stroke)
|
||||||
|
_stroke.object = stroke
|
||||||
|
try context.saveIfNeeded()
|
||||||
|
}
|
||||||
|
stroke = penStroke
|
||||||
|
case .eraser:
|
||||||
|
let eraserStroke = EraserStroke(
|
||||||
|
bounds: [point.x - pen.thickness, point.y - pen.thickness, point.x + pen.thickness, point.y + pen.thickness],
|
||||||
|
color: pen.rgba,
|
||||||
|
style: pen.strokeStyle,
|
||||||
|
createdAt: .now,
|
||||||
|
thickness: pen.thickness
|
||||||
|
)
|
||||||
|
eraserStroke.graphicContext = self
|
||||||
|
withPersistence(\.backgroundContext) { [weak _stroke = eraserStroke] context in
|
||||||
|
guard let _stroke else { return }
|
||||||
|
let stroke = EraserObject(\.backgroundContext)
|
||||||
|
stroke.bounds = _stroke.bounds
|
||||||
|
stroke.color = _stroke.color
|
||||||
|
stroke.style = _stroke.style.rawValue
|
||||||
|
stroke.thickness = _stroke.thickness
|
||||||
|
stroke.createdAt = _stroke.createdAt
|
||||||
|
stroke.quads = []
|
||||||
|
stroke.strokes = .init()
|
||||||
|
_stroke.object = stroke
|
||||||
|
try context.saveIfNeeded()
|
||||||
|
}
|
||||||
|
stroke = eraserStroke
|
||||||
}
|
}
|
||||||
currentStroke = stroke
|
currentStroke = stroke
|
||||||
currentPoint = point
|
currentPoint = point
|
||||||
@@ -157,15 +231,25 @@ extension GraphicContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func endStroke(at point: CGPoint) {
|
func endStroke(at point: CGPoint) {
|
||||||
guard currentPoint != nil, let currentStroke = currentStroke?.stroke(as: PenStroke.self) else { return }
|
guard currentPoint != nil, let currentStroke = currentStroke else { return }
|
||||||
currentStroke.finish(at: point)
|
currentStroke.finish(at: point)
|
||||||
tree.insert(currentStroke.anyStroke, in: currentStroke.strokeBox)
|
if let penStroke = currentStroke.stroke(as: PenStroke.self) {
|
||||||
withPersistence(\.backgroundContext) { [currentStroke] context in
|
penStroke.saveQuads()
|
||||||
guard let stroke = currentStroke.stroke(as: PenStroke.self) else { return }
|
tree.insert(currentStroke.anyStroke, in: currentStroke.strokeBox)
|
||||||
stroke.object?.bounds = stroke.bounds
|
withPersistence(\.backgroundContext) { [weak penStroke] context in
|
||||||
try context.saveIfNeeded()
|
guard let penStroke else { return }
|
||||||
if let object = stroke.object {
|
penStroke.object?.bounds = penStroke.bounds
|
||||||
context.refresh(object, mergeChanges: false)
|
try context.saveIfNeeded()
|
||||||
|
context.refreshAllObjects()
|
||||||
|
}
|
||||||
|
} else if let eraserStroke = currentStroke.stroke(as: EraserStroke.self) {
|
||||||
|
eraserStroke.saveQuads()
|
||||||
|
eraserStrokes.insert(eraserStroke)
|
||||||
|
withPersistence(\.backgroundContext) { [weak eraserStroke] context in
|
||||||
|
guard let eraserStroke else { return }
|
||||||
|
eraserStroke.object?.bounds = eraserStroke.bounds
|
||||||
|
try context.saveIfNeeded()
|
||||||
|
context.refreshAllObjects()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
previousStroke = currentStroke
|
previousStroke = currentStroke
|
||||||
@@ -174,14 +258,27 @@ extension GraphicContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func cancelStroke() {
|
func cancelStroke() {
|
||||||
if !tree.isEmpty, let stroke = currentStroke?.stroke(as: PenStroke.self) {
|
if let stroke = currentStroke {
|
||||||
let _stroke = tree.remove(stroke.anyStroke, in: stroke.strokeBox)
|
switch stroke.style {
|
||||||
withPersistence(\.backgroundContext) { [graphicContext = object, _stroke] context in
|
case .marker:
|
||||||
if let stroke = _stroke?.stroke(as: PenStroke.self)?.object {
|
guard let _stroke = stroke.stroke(as: PenStroke.self) else { break }
|
||||||
graphicContext?.strokes.remove(stroke)
|
withPersistence(\.backgroundContext) { [weak graphicContext = object, weak _stroke] context in
|
||||||
context.delete(stroke)
|
guard let _stroke else { return }
|
||||||
|
if let stroke = _stroke.object {
|
||||||
|
graphicContext?.strokes.remove(stroke)
|
||||||
|
context.delete(stroke)
|
||||||
|
}
|
||||||
|
try context.saveIfNeeded()
|
||||||
|
}
|
||||||
|
case .eraser:
|
||||||
|
guard let eraserStroke = stroke.stroke(as: EraserStroke.self) else { break }
|
||||||
|
eraserStrokes.remove(eraserStroke)
|
||||||
|
withPersistence(\.backgroundContext) { [weak eraserStroke] context in
|
||||||
|
if let stroke = eraserStroke?.object {
|
||||||
|
context.delete(stroke)
|
||||||
|
}
|
||||||
|
try context.saveIfNeeded()
|
||||||
}
|
}
|
||||||
try context.saveIfNeeded()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
currentStroke = nil
|
currentStroke = nil
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ extension Canvas {
|
|||||||
let graphicContext = canvas.graphicContext
|
let graphicContext = canvas.graphicContext
|
||||||
self?.graphicContext.object = graphicContext
|
self?.graphicContext.object = graphicContext
|
||||||
self?.graphicContext.loadStrokes(bounds)
|
self?.graphicContext.loadStrokes(bounds)
|
||||||
context.refresh(canvas, mergeChanges: false)
|
context.refreshAllObjects()
|
||||||
DispatchQueue.main.async { [weak self] in
|
DispatchQueue.main.async { [weak self] in
|
||||||
self?.state = .loaded
|
self?.state = .loaded
|
||||||
}
|
}
|
||||||
@@ -68,7 +68,8 @@ extension Canvas {
|
|||||||
|
|
||||||
func loadStrokes(_ bounds: CGRect) {
|
func loadStrokes(_ bounds: CGRect) {
|
||||||
withPersistence(\.backgroundContext) { [weak self, bounds] context in
|
withPersistence(\.backgroundContext) { [weak self, bounds] context in
|
||||||
self?.graphicContext.loadQuads(bounds)
|
self?.graphicContext.loadQuads(bounds, on: context)
|
||||||
|
context.refreshAllObjects()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,4 +53,14 @@ extension Quad {
|
|||||||
func getColor() -> [CGFloat] {
|
func getColor() -> [CGFloat] {
|
||||||
[color.x.cgFloat, color.y.cgFloat, color.z.cgFloat, color.w.cgFloat]
|
[color.x.cgFloat, color.y.cgFloat, color.z.cgFloat, color.w.cgFloat]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var quadBounds: CGRect {
|
||||||
|
let halfSize = size.cgFloat / 2
|
||||||
|
return CGRect(x: originX.cgFloat - halfSize, y: originY.cgFloat - halfSize, width: size.cgFloat, height: size.cgFloat)
|
||||||
|
}
|
||||||
|
|
||||||
|
var quadBox: Box {
|
||||||
|
let halfSize = size / 2
|
||||||
|
return Box(minX: Double(originX - halfSize), minY: Double(originY - halfSize), maxX: Double(originX + halfSize), maxY: Double(originY + halfSize))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
// Created by Dscyre Scotti on 5/24/24.
|
// Created by Dscyre Scotti on 5/24/24.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import CoreData
|
||||||
import MetalKit
|
import MetalKit
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
@@ -25,6 +26,16 @@ final class EraserStroke: Stroke, @unchecked Sendable {
|
|||||||
var indexBuffer: (any MTLBuffer)?
|
var indexBuffer: (any MTLBuffer)?
|
||||||
var vertexBuffer: (any MTLBuffer)?
|
var vertexBuffer: (any MTLBuffer)?
|
||||||
|
|
||||||
|
let batchSize: Int = 50
|
||||||
|
var batchIndex: Int = 0
|
||||||
|
|
||||||
|
var object: EraserObject?
|
||||||
|
|
||||||
|
weak var graphicContext: GraphicContext?
|
||||||
|
|
||||||
|
var finishesSaving: Bool = false
|
||||||
|
var penStrokes: NSHashTable<PenStroke> = .weakObjects()
|
||||||
|
|
||||||
init(
|
init(
|
||||||
bounds: [CGFloat],
|
bounds: [CGFloat],
|
||||||
color: [CGFloat],
|
color: [CGFloat],
|
||||||
@@ -41,4 +52,77 @@ final class EraserStroke: Stroke, @unchecked Sendable {
|
|||||||
self.quads = quads
|
self.quads = quads
|
||||||
self.penStyle = style.penStyle
|
self.penStyle = style.penStyle
|
||||||
}
|
}
|
||||||
|
|
||||||
|
convenience init(object: EraserObject) {
|
||||||
|
let style = StrokeStyle(rawValue: object.style) ?? .marker
|
||||||
|
self.init(
|
||||||
|
bounds: object.bounds,
|
||||||
|
color: object.color,
|
||||||
|
style: style,
|
||||||
|
createdAt: object.createdAt ?? .now,
|
||||||
|
thickness: object.thickness
|
||||||
|
)
|
||||||
|
self.object = object
|
||||||
|
}
|
||||||
|
|
||||||
|
func addQuad(at point: CGPoint, rotation: CGFloat, shape: QuadShape) {
|
||||||
|
let quad = Quad(
|
||||||
|
origin: point,
|
||||||
|
size: thickness,
|
||||||
|
rotation: rotation,
|
||||||
|
shape: shape.rawValue,
|
||||||
|
color: color
|
||||||
|
)
|
||||||
|
quads.append(quad)
|
||||||
|
bounds = [
|
||||||
|
min(quad.originX.cgFloat, bounds[0]),
|
||||||
|
min(quad.originY.cgFloat, bounds[1]),
|
||||||
|
max(quad.originX.cgFloat, bounds[2]),
|
||||||
|
max(quad.originY.cgFloat, bounds[3])
|
||||||
|
]
|
||||||
|
if quads.endIndex >= batchIndex + batchSize {
|
||||||
|
saveQuads(to: batchIndex + batchSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadQuads(from object: EraserObject) {
|
||||||
|
quads = object.quads.compactMap { quad in
|
||||||
|
guard let quad = quad as? QuadObject else { return nil }
|
||||||
|
return Quad(object: quad)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveQuads(to endIndex: Int? = nil) {
|
||||||
|
let isEnded: Bool = endIndex == nil
|
||||||
|
guard let graphicContext else { return }
|
||||||
|
let endIndex = endIndex ?? quads.endIndex
|
||||||
|
let batch = quads[batchIndex..<endIndex]
|
||||||
|
batchIndex = endIndex
|
||||||
|
withPersistence(\.backgroundContext) { [weak self, weak eraser = object, quads = batch] context in
|
||||||
|
guard let self, let eraser else { return }
|
||||||
|
for _quad in quads {
|
||||||
|
let quad = QuadObject(\.backgroundContext)
|
||||||
|
quad.originX = _quad.originX.cgFloat
|
||||||
|
quad.originY = _quad.originY.cgFloat
|
||||||
|
quad.size = _quad.size.cgFloat
|
||||||
|
quad.rotation = _quad.rotation.cgFloat
|
||||||
|
quad.shape = _quad.shape
|
||||||
|
quad.color = _quad.getColor()
|
||||||
|
quad.eraser = eraser
|
||||||
|
for stroke in graphicContext.tree.search(box: _quad.quadBox) {
|
||||||
|
if let _penStroke = stroke.stroke(as: PenStroke.self), !_penStroke.eraserStrokes.contains(self) {
|
||||||
|
_penStroke.eraserStrokes.insert(self)
|
||||||
|
penStrokes.add(_penStroke)
|
||||||
|
if let penStroke = _penStroke.object {
|
||||||
|
penStroke.erasers.add(eraser)
|
||||||
|
eraser.strokes.add(penStroke)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if isEnded {
|
||||||
|
finishesSaving = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
// Created by Dscyre Scotti on 5/4/24.
|
// Created by Dscyre Scotti on 5/4/24.
|
||||||
//
|
//
|
||||||
|
|
||||||
import MetalKit
|
|
||||||
import CoreData
|
import CoreData
|
||||||
|
import MetalKit
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
final class PenStroke: Stroke, @unchecked Sendable {
|
final class PenStroke: Stroke, @unchecked Sendable {
|
||||||
@@ -25,9 +25,23 @@ final class PenStroke: Stroke, @unchecked Sendable {
|
|||||||
var texture: (any MTLTexture)?
|
var texture: (any MTLTexture)?
|
||||||
var indexBuffer: (any MTLBuffer)?
|
var indexBuffer: (any MTLBuffer)?
|
||||||
var vertexBuffer: (any MTLBuffer)?
|
var vertexBuffer: (any MTLBuffer)?
|
||||||
|
var erasedIndexBuffer: (any MTLBuffer)?
|
||||||
|
var erasedVertexBuffer: (any MTLBuffer)?
|
||||||
|
|
||||||
var object: StrokeObject?
|
var object: StrokeObject?
|
||||||
|
|
||||||
|
let batchSize: Int = 50
|
||||||
|
var batchIndex: Int = 0
|
||||||
|
var erasedQuadCount: Int = 0
|
||||||
|
|
||||||
|
var eraserStrokes: Set<EraserStroke> = []
|
||||||
|
|
||||||
|
var isEmptyErasedQuads: Bool {
|
||||||
|
eraserStrokes.isEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
weak var graphicContext: GraphicContext?
|
||||||
|
|
||||||
init(
|
init(
|
||||||
bounds: [CGFloat],
|
bounds: [CGFloat],
|
||||||
color: [CGFloat],
|
color: [CGFloat],
|
||||||
@@ -47,26 +61,53 @@ final class PenStroke: Stroke, @unchecked Sendable {
|
|||||||
|
|
||||||
convenience init(object: StrokeObject) {
|
convenience init(object: StrokeObject) {
|
||||||
let style = StrokeStyle(rawValue: object.style) ?? .marker
|
let style = StrokeStyle(rawValue: object.style) ?? .marker
|
||||||
|
#warning("TODO: revisit here and check if there is any crash")
|
||||||
self.init(
|
self.init(
|
||||||
bounds: object.bounds,
|
bounds: object.bounds,
|
||||||
color: object.color,
|
color: object.color,
|
||||||
style: style,
|
style: style,
|
||||||
createdAt: object.createdAt,
|
createdAt: object.createdAt ?? .now, // sometimes crash here
|
||||||
thickness: object.thickness
|
thickness: object.thickness
|
||||||
)
|
)
|
||||||
self.object = object
|
self.object = object
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadQuads() {
|
func loadQuads(with graphicContext: GraphicContext) {
|
||||||
guard let object else { return }
|
guard let object else { return }
|
||||||
loadQuads(from: object)
|
loadQuads(from: object, with: graphicContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadQuads(from object: StrokeObject) {
|
func loadQuads(from object: StrokeObject, with graphicContext: GraphicContext) {
|
||||||
quads = object.quads.compactMap { quad in
|
quads = object.quads.compactMap { quad in
|
||||||
guard let quad = quad as? QuadObject else { return nil }
|
guard let quad = quad as? QuadObject else { return nil }
|
||||||
return Quad(object: quad)
|
return Quad(object: quad)
|
||||||
}
|
}
|
||||||
|
let erasers = fetchErasers(of: object)
|
||||||
|
eraserStrokes = Set(erasers.compactMap { [graphicContext] eraser -> EraserStroke? in
|
||||||
|
let url = eraser.objectID.uriRepresentation()
|
||||||
|
return graphicContext.barrierQueue.sync(flags: .barrier) {
|
||||||
|
if graphicContext.erasers[url] == nil {
|
||||||
|
let _stroke = EraserStroke(object: eraser)
|
||||||
|
_stroke.loadQuads(from: eraser)
|
||||||
|
graphicContext.erasers[url] = _stroke
|
||||||
|
return _stroke
|
||||||
|
}
|
||||||
|
return graphicContext.erasers[url]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchErasers(of stroke: StrokeObject) -> [EraserObject] {
|
||||||
|
let fetchRequest: NSFetchRequest<EraserObject> = .init(entityName: "EraserObject")
|
||||||
|
fetchRequest.predicate = NSPredicate(format: "ANY strokes == %@", stroke)
|
||||||
|
|
||||||
|
do {
|
||||||
|
let erasers = try Persistence.shared.backgroundContext.fetch(fetchRequest)
|
||||||
|
return erasers
|
||||||
|
} catch {
|
||||||
|
NSLog("[Memola] - \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
func addQuad(at point: CGPoint, rotation: CGFloat, shape: QuadShape) {
|
func addQuad(at point: CGPoint, rotation: CGFloat, shape: QuadShape) {
|
||||||
@@ -84,16 +125,49 @@ final class PenStroke: Stroke, @unchecked Sendable {
|
|||||||
max(quad.originX.cgFloat, bounds[2]),
|
max(quad.originX.cgFloat, bounds[2]),
|
||||||
max(quad.originY.cgFloat, bounds[3])
|
max(quad.originY.cgFloat, bounds[3])
|
||||||
]
|
]
|
||||||
withPersistence(\.backgroundContext) { [object, _quad = quad] context in
|
if quads.endIndex >= batchIndex + batchSize {
|
||||||
let quad = QuadObject(\.backgroundContext)
|
saveQuads(to: batchIndex + batchSize)
|
||||||
quad.originX = _quad.originX.cgFloat
|
|
||||||
quad.originY = _quad.originY.cgFloat
|
|
||||||
quad.size = _quad.size.cgFloat
|
|
||||||
quad.rotation = _quad.rotation.cgFloat
|
|
||||||
quad.shape = _quad.shape
|
|
||||||
quad.color = _quad.getColor()
|
|
||||||
quad.stroke = object
|
|
||||||
object?.quads.add(quad)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func saveQuads(to endIndex: Int? = nil) {
|
||||||
|
let endIndex = endIndex ?? quads.endIndex
|
||||||
|
let batch = quads[batchIndex..<endIndex]
|
||||||
|
batchIndex = endIndex
|
||||||
|
withPersistence(\.backgroundContext) { [weak object, quads = batch] context in
|
||||||
|
for _quad in quads {
|
||||||
|
let quad = QuadObject(\.backgroundContext)
|
||||||
|
quad.originX = _quad.originX.cgFloat
|
||||||
|
quad.originY = _quad.originY.cgFloat
|
||||||
|
quad.size = _quad.size.cgFloat
|
||||||
|
quad.rotation = _quad.rotation.cgFloat
|
||||||
|
quad.shape = _quad.shape
|
||||||
|
quad.color = _quad.getColor()
|
||||||
|
quad.stroke = object
|
||||||
|
object?.quads.add(quad)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAllErasedQuads() -> [Quad] {
|
||||||
|
eraserStrokes.flatMap { $0.quads }
|
||||||
|
}
|
||||||
|
|
||||||
|
func erase(device: MTLDevice, renderEncoder: MTLRenderCommandEncoder) {
|
||||||
|
guard !isEmptyErasedQuads, let erasedIndexBuffer else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
prepare(device: device)
|
||||||
|
renderEncoder.setFragmentTexture(texture, index: 0)
|
||||||
|
renderEncoder.setVertexBuffer(erasedVertexBuffer, offset: 0, index: 0)
|
||||||
|
renderEncoder.drawIndexedPrimitives(
|
||||||
|
type: .triangle,
|
||||||
|
indexCount: erasedQuadCount * 6,
|
||||||
|
indexType: .uint32,
|
||||||
|
indexBuffer: erasedIndexBuffer,
|
||||||
|
indexBufferOffset: 0
|
||||||
|
)
|
||||||
|
self.erasedIndexBuffer = nil
|
||||||
|
self.erasedVertexBuffer = nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,11 +52,23 @@ class History: ObservableObject {
|
|||||||
for event in redoStack {
|
for event in redoStack {
|
||||||
switch event {
|
switch event {
|
||||||
case .stroke(let _stroke):
|
case .stroke(let _stroke):
|
||||||
withPersistence(\.backgroundContext) { context in
|
switch _stroke.style {
|
||||||
if let stroke = _stroke.stroke(as: PenStroke.self)?.object {
|
case .marker:
|
||||||
context.delete(stroke)
|
guard let penStroke = _stroke.stroke(as: PenStroke.self) else { return }
|
||||||
|
withPersistence(\.backgroundContext) { context in
|
||||||
|
if let stroke = penStroke.object {
|
||||||
|
context.delete(stroke)
|
||||||
|
}
|
||||||
|
try context.saveIfNeeded()
|
||||||
|
}
|
||||||
|
case .eraser:
|
||||||
|
guard let eraserStroke = _stroke.stroke(as: EraserStroke.self) else { return }
|
||||||
|
withPersistence(\.backgroundContext) { context in
|
||||||
|
if let stroke = eraserStroke.object {
|
||||||
|
context.delete(stroke)
|
||||||
|
}
|
||||||
|
try context.saveIfNeeded()
|
||||||
}
|
}
|
||||||
try context.saveIfNeeded()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ class RTree<T> where T: Equatable & Comparable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Search
|
// MARK: - Search
|
||||||
func search(box: Box) -> [T] {
|
func search(box: Box, isInOrder: Bool = true) -> [T] {
|
||||||
guard box.intersects(with: root.box) else { return [] }
|
guard box.intersects(with: root.box) else { return [] }
|
||||||
var result: [T] = []
|
var result: [T] = []
|
||||||
var queue: [Node<T>] = [root]
|
var queue: [Node<T>] = [root]
|
||||||
@@ -76,10 +76,18 @@ class RTree<T> where T: Equatable & Comparable {
|
|||||||
if box.intersects(with: childNode.box) {
|
if box.intersects(with: childNode.box) {
|
||||||
if node.isLeaf {
|
if node.isLeaf {
|
||||||
if let value = childNode.value {
|
if let value = childNode.value {
|
||||||
result = _merge(result, [value])
|
if isInOrder {
|
||||||
|
result = _merge(result, [value])
|
||||||
|
} else {
|
||||||
|
result.append(value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if box.contains(with: childNode.box) {
|
} else if box.contains(with: childNode.box) {
|
||||||
result = _merge(result, _traverse(from: childNode))
|
if isInOrder {
|
||||||
|
result = _merge(result, _traverse(from: childNode))
|
||||||
|
} else {
|
||||||
|
result.append(contentsOf: _traverse(from: childNode))
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
queue.append(childNode)
|
queue.append(childNode)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,21 +41,33 @@ class EraserRenderPass: RenderPass {
|
|||||||
renderEncoder.setRenderPipelineState(eraserPipelineState)
|
renderEncoder.setRenderPipelineState(eraserPipelineState)
|
||||||
|
|
||||||
canvas.setUniformsBuffer(device: renderer.device, renderEncoder: renderEncoder)
|
canvas.setUniformsBuffer(device: renderer.device, renderEncoder: renderEncoder)
|
||||||
stroke?.draw(device: renderer.device, renderEncoder: renderEncoder)
|
if let stroke = stroke as? PenStroke {
|
||||||
|
stroke.erase(device: renderer.device, renderEncoder: renderEncoder)
|
||||||
|
} else {
|
||||||
|
stroke?.draw(device: renderer.device, renderEncoder: renderEncoder)
|
||||||
|
}
|
||||||
|
|
||||||
renderEncoder.endEncoding()
|
renderEncoder.endEncoding()
|
||||||
commandBuffer.commit()
|
commandBuffer.commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func generateVertexBuffer(on canvas: Canvas, with renderer: Renderer) {
|
private func generateVertexBuffer(on canvas: Canvas, with renderer: Renderer) {
|
||||||
guard let stroke, !stroke.isEmpty, let quadPipelineState else { return }
|
guard let stroke else { return }
|
||||||
|
let quadCount: Int
|
||||||
|
var quads: [Quad]
|
||||||
|
if let stroke = stroke as? PenStroke {
|
||||||
|
quads = stroke.getAllErasedQuads()
|
||||||
|
quadCount = quads.endIndex
|
||||||
|
} else {
|
||||||
|
quadCount = stroke.quads.endIndex
|
||||||
|
quads = stroke.quads
|
||||||
|
}
|
||||||
|
guard !quads.isEmpty, let quadPipelineState else { return }
|
||||||
guard let quadCommandBuffer = renderer.commandQueue.makeCommandBuffer() else { return }
|
guard let quadCommandBuffer = renderer.commandQueue.makeCommandBuffer() else { return }
|
||||||
guard let computeEncoder = quadCommandBuffer.makeComputeCommandEncoder() else { return }
|
guard let computeEncoder = quadCommandBuffer.makeComputeCommandEncoder() else { return }
|
||||||
|
|
||||||
computeEncoder.label = "Quad Render Pass"
|
computeEncoder.label = "Quad Render Pass"
|
||||||
|
|
||||||
let quadCount = stroke.quads.endIndex
|
|
||||||
var quads = stroke.quads
|
|
||||||
let quadBuffer = renderer.device.makeBuffer(bytes: &quads, length: MemoryLayout<Quad>.stride * quadCount, options: [])
|
let quadBuffer = renderer.device.makeBuffer(bytes: &quads, length: MemoryLayout<Quad>.stride * quadCount, options: [])
|
||||||
let indexBuffer = renderer.device.makeBuffer(length: MemoryLayout<UInt>.stride * quadCount * 6, options: [])
|
let indexBuffer = renderer.device.makeBuffer(length: MemoryLayout<UInt>.stride * quadCount * 6, options: [])
|
||||||
let vertexBuffer = renderer.device.makeBuffer(length: MemoryLayout<QuadVertex>.stride * quadCount * 4, options: [])
|
let vertexBuffer = renderer.device.makeBuffer(length: MemoryLayout<QuadVertex>.stride * quadCount * 4, options: [])
|
||||||
@@ -65,8 +77,14 @@ class EraserRenderPass: RenderPass {
|
|||||||
computeEncoder.setBuffer(indexBuffer, offset: 0, index: 1)
|
computeEncoder.setBuffer(indexBuffer, offset: 0, index: 1)
|
||||||
computeEncoder.setBuffer(vertexBuffer, offset: 0, index: 2)
|
computeEncoder.setBuffer(vertexBuffer, offset: 0, index: 2)
|
||||||
|
|
||||||
stroke.indexBuffer = indexBuffer
|
if let stroke = stroke as? PenStroke {
|
||||||
stroke.vertexBuffer = vertexBuffer
|
stroke.erasedIndexBuffer = indexBuffer
|
||||||
|
stroke.erasedVertexBuffer = vertexBuffer
|
||||||
|
stroke.erasedQuadCount = quadCount
|
||||||
|
} else {
|
||||||
|
stroke.indexBuffer = indexBuffer
|
||||||
|
stroke.vertexBuffer = vertexBuffer
|
||||||
|
}
|
||||||
|
|
||||||
let threadsPerGroup = MTLSize(width: 1, height: 1, depth: 1)
|
let threadsPerGroup = MTLSize(width: 1, height: 1, depth: 1)
|
||||||
let numThreadgroups = MTLSize(width: quadCount + 1, height: 1, depth: 1)
|
let numThreadgroups = MTLSize(width: quadCount + 1, height: 1, depth: 1)
|
||||||
|
|||||||
@@ -64,6 +64,13 @@ class GraphicRenderPass: RenderPass {
|
|||||||
strokeRenderPass.graphicDescriptor = descriptor
|
strokeRenderPass.graphicDescriptor = descriptor
|
||||||
strokeRenderPass.graphicPipelineState = graphicPipelineState
|
strokeRenderPass.graphicPipelineState = graphicPipelineState
|
||||||
strokeRenderPass.draw(on: canvas, with: renderer)
|
strokeRenderPass.draw(on: canvas, with: renderer)
|
||||||
|
|
||||||
|
if let stroke = stroke as? PenStroke, !stroke.isEmptyErasedQuads {
|
||||||
|
descriptor.colorAttachments[0].loadAction = .load
|
||||||
|
eraserRenderPass.stroke = stroke
|
||||||
|
eraserRenderPass.descriptor = descriptor
|
||||||
|
eraserRenderPass.draw(on: canvas, with: renderer)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
renderer.redrawsGraphicRender = false
|
renderer.redrawsGraphicRender = false
|
||||||
@@ -83,8 +90,23 @@ class GraphicRenderPass: RenderPass {
|
|||||||
strokeRenderPass.graphicDescriptor = descriptor
|
strokeRenderPass.graphicDescriptor = descriptor
|
||||||
strokeRenderPass.graphicPipelineState = graphicPipelineState
|
strokeRenderPass.graphicPipelineState = graphicPipelineState
|
||||||
strokeRenderPass.draw(on: canvas, with: renderer)
|
strokeRenderPass.draw(on: canvas, with: renderer)
|
||||||
|
|
||||||
|
if let stroke = stroke as? PenStroke, !stroke.isEmptyErasedQuads {
|
||||||
|
descriptor.colorAttachments[0].loadAction = .load
|
||||||
|
eraserRenderPass.stroke = stroke
|
||||||
|
eraserRenderPass.descriptor = descriptor
|
||||||
|
eraserRenderPass.draw(on: canvas, with: renderer)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
graphicContext.previousStroke = nil
|
graphicContext.previousStroke = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let eraserStrokes = graphicContext.eraserStrokes
|
||||||
|
for eraserStroke in eraserStrokes {
|
||||||
|
if eraserStroke.finishesSaving {
|
||||||
|
graphicContext.eraserStrokes.remove(eraserStroke)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,9 +62,6 @@ class CanvasViewController: UIViewController {
|
|||||||
override func viewDidDisappear(_ animated: Bool) {
|
override func viewDidDisappear(_ animated: Bool) {
|
||||||
super.viewDidDisappear(animated)
|
super.viewDidDisappear(animated)
|
||||||
history.resetRedo()
|
history.resetRedo()
|
||||||
withPersistence(\.backgroundContext) { context in
|
|
||||||
context.refreshAllObjects()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,9 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct CanvasView: UIViewControllerRepresentable {
|
struct CanvasView: UIViewControllerRepresentable {
|
||||||
@EnvironmentObject var tool: Tool
|
@ObservedObject var tool: Tool
|
||||||
@EnvironmentObject var canvas: Canvas
|
@ObservedObject var canvas: Canvas
|
||||||
@EnvironmentObject var history: History
|
@ObservedObject var history: History
|
||||||
|
|
||||||
func makeUIViewController(context: Context) -> CanvasViewController {
|
func makeUIViewController(context: Context) -> CanvasViewController {
|
||||||
CanvasViewController(tool: tool, canvas: canvas, history: history)
|
CanvasViewController(tool: tool, canvas: canvas, history: history)
|
||||||
|
|||||||
@@ -27,17 +27,17 @@ struct MemoView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
CanvasView()
|
CanvasView(tool: tool, canvas: canvas, history: history)
|
||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
.overlay(alignment: .trailing) {
|
.overlay(alignment: .trailing) {
|
||||||
PenDock()
|
PenDock(tool: tool, canvas: canvas)
|
||||||
}
|
}
|
||||||
.overlay(alignment: .bottomLeading) {
|
.overlay(alignment: .bottomLeading) {
|
||||||
zoomControl
|
zoomControl
|
||||||
}
|
}
|
||||||
.disabled(textFieldState)
|
.disabled(textFieldState)
|
||||||
.overlay(alignment: .top) {
|
.overlay(alignment: .top) {
|
||||||
Toolbar(memo: memo, size: size)
|
Toolbar(size: size, memo: memo, canvas: canvas, history: history)
|
||||||
}
|
}
|
||||||
.disabled(canvas.state == .loading || canvas.state == .closing)
|
.disabled(canvas.state == .loading || canvas.state == .closing)
|
||||||
.overlay {
|
.overlay {
|
||||||
@@ -50,9 +50,6 @@ struct MemoView: View {
|
|||||||
EmptyView()
|
EmptyView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.environmentObject(tool)
|
|
||||||
.environmentObject(canvas)
|
|
||||||
.environmentObject(history)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
|
|||||||
@@ -8,8 +8,8 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct PenDock: View {
|
struct PenDock: View {
|
||||||
@EnvironmentObject var tool: Tool
|
@ObservedObject var tool: Tool
|
||||||
@EnvironmentObject var canvas: Canvas
|
@ObservedObject var canvas: Canvas
|
||||||
|
|
||||||
let width: CGFloat = 90
|
let width: CGFloat = 90
|
||||||
let height: CGFloat = 30
|
let height: CGFloat = 30
|
||||||
@@ -194,7 +194,6 @@ struct PenDock: View {
|
|||||||
.stroke(Color.gray, lineWidth: 0.4)
|
.stroke(Color.gray, lineWidth: 0.4)
|
||||||
}
|
}
|
||||||
.padding(0.2)
|
.padding(0.2)
|
||||||
.drawingGroup()
|
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
.hoverEffect(.lift)
|
.hoverEffect(.lift)
|
||||||
@@ -250,7 +249,9 @@ struct PenDock: View {
|
|||||||
var newPenButton: some View {
|
var newPenButton: some View {
|
||||||
Button {
|
Button {
|
||||||
let pen = PenObject.createObject(\.viewContext, penStyle: .marker)
|
let pen = PenObject.createObject(\.viewContext, penStyle: .marker)
|
||||||
if let color = (tool.selectedPen ?? tool.pens.last)?.rgba {
|
var selectedPen = tool.selectedPen
|
||||||
|
selectedPen = (selectedPen?.strokeStyle == .marker ? (selectedPen ?? tool.pens.last) : tool.pens.last)
|
||||||
|
if let color = selectedPen?.rgba {
|
||||||
pen.color = color
|
pen.color = color
|
||||||
}
|
}
|
||||||
pen.isSelected = true
|
pen.isSelected = true
|
||||||
@@ -300,7 +301,6 @@ struct PenDock: View {
|
|||||||
.resizable()
|
.resizable()
|
||||||
.renderingMode(.template)
|
.renderingMode(.template)
|
||||||
}
|
}
|
||||||
.drawingGroup()
|
|
||||||
.foregroundStyle(.black.opacity(0.2))
|
.foregroundStyle(.black.opacity(0.2))
|
||||||
.blur(radius: 3)
|
.blur(radius: 3)
|
||||||
if let tip = pen.style.icon.tip {
|
if let tip = pen.style.icon.tip {
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ import Foundation
|
|||||||
|
|
||||||
struct Toolbar: View {
|
struct Toolbar: View {
|
||||||
@Environment(\.dismiss) var dismiss
|
@Environment(\.dismiss) var dismiss
|
||||||
|
|
||||||
@EnvironmentObject var history: History
|
@ObservedObject var canvas: Canvas
|
||||||
@EnvironmentObject var canvas: Canvas
|
@ObservedObject var history: History
|
||||||
|
|
||||||
@State var memo: MemoObject
|
@State var memo: MemoObject
|
||||||
@State var title: String
|
@State var title: String
|
||||||
@@ -20,9 +20,11 @@ struct Toolbar: View {
|
|||||||
|
|
||||||
let size: CGFloat
|
let size: CGFloat
|
||||||
|
|
||||||
init(memo: MemoObject, size: CGFloat) {
|
init(size: CGFloat, memo: MemoObject, canvas: Canvas, history: History) {
|
||||||
self.memo = memo
|
|
||||||
self.size = size
|
self.size = size
|
||||||
|
self.memo = memo
|
||||||
|
self.canvas = canvas
|
||||||
|
self.history = history
|
||||||
self.title = memo.title
|
self.title = memo.title
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,6 +74,9 @@ struct Toolbar: View {
|
|||||||
} else {
|
} else {
|
||||||
title = memo.title
|
title = memo.title
|
||||||
}
|
}
|
||||||
|
withPersistence(\.viewContext) { context in
|
||||||
|
try context.saveIfNeeded()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.transition(.move(edge: .top).combined(with: .blurReplace))
|
.transition(.move(edge: .top).combined(with: .blurReplace))
|
||||||
@@ -128,15 +133,16 @@ struct Toolbar: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func closeMemo() {
|
func closeMemo() {
|
||||||
DispatchQueue.global(qos: .userInitiated).async {
|
withAnimation {
|
||||||
|
canvas.state = .closing
|
||||||
|
}
|
||||||
|
withPersistence(\.backgroundContext) { context in
|
||||||
|
try? context.saveIfNeeded()
|
||||||
|
context.refreshAllObjects()
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
canvas.state = .closing
|
withAnimation {
|
||||||
}
|
canvas.state = .closed
|
||||||
withPersistenceSync(\.viewContext) { context in
|
}
|
||||||
try context.saveIfNeeded()
|
|
||||||
}
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
canvas.state = .closed
|
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ struct MemosView: View {
|
|||||||
MemoView(memo: memo)
|
MemoView(memo: memo)
|
||||||
.onDisappear {
|
.onDisappear {
|
||||||
withPersistence(\.viewContext) { context in
|
withPersistence(\.viewContext) { context in
|
||||||
|
try context.saveIfNeeded()
|
||||||
context.refreshAllObjects()
|
context.refreshAllObjects()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
//
|
||||||
|
// EraserObject.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 6/8/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import CoreData
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
@objc(EraserObject)
|
||||||
|
final class EraserObject: NSManagedObject {
|
||||||
|
@NSManaged var bounds: [CGFloat]
|
||||||
|
@NSManaged var color: [CGFloat]
|
||||||
|
@NSManaged var style: Int16
|
||||||
|
@NSManaged var createdAt: Date?
|
||||||
|
@NSManaged var thickness: CGFloat
|
||||||
|
@NSManaged var quads: NSMutableOrderedSet
|
||||||
|
@NSManaged var strokes: NSMutableSet
|
||||||
|
}
|
||||||
@@ -17,4 +17,5 @@ final class QuadObject: NSManagedObject {
|
|||||||
@NSManaged var shape: Int16
|
@NSManaged var shape: Int16
|
||||||
@NSManaged var color: [CGFloat]
|
@NSManaged var color: [CGFloat]
|
||||||
@NSManaged var stroke: StrokeObject?
|
@NSManaged var stroke: StrokeObject?
|
||||||
|
@NSManaged var eraser: EraserObject?
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,8 +13,9 @@ final class StrokeObject: NSManagedObject {
|
|||||||
@NSManaged var bounds: [CGFloat]
|
@NSManaged var bounds: [CGFloat]
|
||||||
@NSManaged var color: [CGFloat]
|
@NSManaged var color: [CGFloat]
|
||||||
@NSManaged var style: Int16
|
@NSManaged var style: Int16
|
||||||
@NSManaged var createdAt: Date
|
@NSManaged var createdAt: Date?
|
||||||
@NSManaged var thickness: CGFloat
|
@NSManaged var thickness: CGFloat
|
||||||
@NSManaged var quads: NSMutableOrderedSet
|
@NSManaged var quads: NSMutableOrderedSet
|
||||||
|
@NSManaged var erasers: NSMutableSet
|
||||||
@NSManaged var graphicContext: GraphicContextObject?
|
@NSManaged var graphicContext: GraphicContextObject?
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,15 @@
|
|||||||
<relationship name="graphicContext" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="GraphicContextObject" inverseName="canvas" inverseEntity="GraphicContextObject"/>
|
<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"/>
|
<relationship name="memo" maxCount="1" deletionRule="Deny" destinationEntity="MemoObject" inverseName="canvas" inverseEntity="MemoObject"/>
|
||||||
</entity>
|
</entity>
|
||||||
|
<entity name="EraserObject" representedClassName="EraserObject" syncable="YES">
|
||||||
|
<attribute name="bounds" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromDataTransformer" customClassName="[CGFloat]"/>
|
||||||
|
<attribute name="color" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromDataTransformer" customClassName="[CGFloat]"/>
|
||||||
|
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="style" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="thickness" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||||
|
<relationship name="quads" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="QuadObject" inverseName="eraser" inverseEntity="QuadObject"/>
|
||||||
|
<relationship name="strokes" toMany="YES" deletionRule="Nullify" destinationEntity="StrokeObject" inverseName="erasers" inverseEntity="StrokeObject"/>
|
||||||
|
</entity>
|
||||||
<entity name="GraphicContextObject" representedClassName="GraphicContextObject" syncable="YES">
|
<entity name="GraphicContextObject" representedClassName="GraphicContextObject" syncable="YES">
|
||||||
<relationship name="canvas" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="CanvasObject" inverseName="graphicContext" inverseEntity="CanvasObject"/>
|
<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"/>
|
<relationship name="strokes" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="StrokeObject" inverseName="graphicContext" inverseEntity="StrokeObject"/>
|
||||||
@@ -32,6 +41,7 @@
|
|||||||
<attribute name="rotation" 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="shape" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
|
||||||
<attribute name="size" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
|
<attribute name="size" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||||
|
<relationship name="eraser" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="EraserObject" inverseName="quads" inverseEntity="EraserObject"/>
|
||||||
<relationship name="stroke" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StrokeObject" inverseName="quads" inverseEntity="StrokeObject"/>
|
<relationship name="stroke" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StrokeObject" inverseName="quads" inverseEntity="StrokeObject"/>
|
||||||
</entity>
|
</entity>
|
||||||
<entity name="StrokeObject" representedClassName="StrokeObject" syncable="YES">
|
<entity name="StrokeObject" representedClassName="StrokeObject" syncable="YES">
|
||||||
@@ -40,6 +50,7 @@
|
|||||||
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
<attribute name="style" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
|
<attribute name="style" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
|
||||||
<attribute name="thickness" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
|
<attribute name="thickness" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||||
|
<relationship name="erasers" toMany="YES" deletionRule="Nullify" destinationEntity="EraserObject" inverseName="strokes" inverseEntity="EraserObject"/>
|
||||||
<relationship name="graphicContext" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="GraphicContextObject" inverseName="strokes" inverseEntity="GraphicContextObject"/>
|
<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"/>
|
<relationship name="quads" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="QuadObject" inverseName="stroke" inverseEntity="QuadObject"/>
|
||||||
</entity>
|
</entity>
|
||||||
|
|||||||
Reference in New Issue
Block a user