diff --git a/Memola/Canvas/Contexts/GraphicContext.swift b/Memola/Canvas/Contexts/GraphicContext.swift index e004576..10fca2c 100644 --- a/Memola/Canvas/Contexts/GraphicContext.swift +++ b/Memola/Canvas/Contexts/GraphicContext.swift @@ -11,7 +11,7 @@ import CoreData import Foundation final class GraphicContext: @unchecked Sendable { - var strokes: [any Stroke] = [] + var tree: RTree = RTree(maxEntries: 8) var object: GraphicContextObject? var currentStroke: (any Stroke)? @@ -37,20 +37,26 @@ final class GraphicContext: @unchecked Sendable { ] } - func undoGraphic() { - guard !strokes.isEmpty else { return } - let stroke = strokes.removeLast() - withPersistence(\.backgroundContext) { [stroke] context in - stroke.stroke(as: PenStroke.self)?.object?.graphicContext = nil - try context.saveIfNeeded() + func undoGraphic(for event: HistoryEvent) { + switch event { + case .stroke(let stroke): + guard let _stroke = stroke.stroke(as: PenStroke.self) else { return } + let deletedStroke = tree.remove(_stroke, in: _stroke.strokeBox) + withPersistence(\.backgroundContext) { [stroke = deletedStroke] context in + stroke?.stroke(as: PenStroke.self)?.object?.graphicContext = nil + try context.saveIfNeeded() + } + previousStroke = nil } - previousStroke = nil + } func redoGraphic(for event: HistoryEvent) { switch event { case .stroke(let stroke): - strokes.append(stroke) + if let stroke = stroke.stroke(as: PenStroke.self) { + tree.insert(stroke, in: stroke.strokeBox) + } withPersistence(\.backgroundContext) { [weak self, stroke] context in stroke.stroke(as: PenStroke.self)?.object?.graphicContext = self?.object try context.saveIfNeeded() @@ -65,9 +71,10 @@ extension GraphicContext { guard let object else { return } let queue = OperationQueue() queue.qualityOfService = .userInteractive - self.strokes = object.strokes.compactMap { stroke -> PenStroke? in - guard let stroke = stroke as? StrokeObject else { return nil } + object.strokes.forEach { stroke in + guard let stroke = stroke as? StrokeObject else { return } let _stroke = PenStroke(object: stroke) + tree.insert(_stroke, in: _stroke.strokeBox) if _stroke.isVisible(in: bounds) { let id = stroke.objectID queue.addOperation { @@ -85,14 +92,13 @@ extension GraphicContext { context.refresh(stroke, mergeChanges: false) } } - return _stroke } queue.waitUntilAllOperationsAreFinished() } func loadQuads(_ bounds: CGRect) { - for stroke in self.strokes { - guard stroke.isVisible(in: bounds), stroke.isEmpty else { continue } + for stroke in self.tree.search(box: bounds.box) { + guard stroke.isEmpty else { continue } stroke.stroke(as: PenStroke.self)?.loadQuads() } } @@ -135,7 +141,6 @@ extension GraphicContext { graphicContext?.strokes.add(stroke) _stroke.object = stroke } - strokes.append(stroke) currentStroke = stroke currentPoint = point currentStroke?.begin(at: point) @@ -152,8 +157,9 @@ extension GraphicContext { } func endStroke(at point: CGPoint) { - guard currentPoint != nil, let currentStroke else { return } + guard currentPoint != nil, let currentStroke = currentStroke?.stroke(as: PenStroke.self) else { return } currentStroke.finish(at: point) + tree.insert(currentStroke, in: currentStroke.strokeBox) withPersistence(\.backgroundContext) { [currentStroke] context in guard let stroke = currentStroke.stroke(as: PenStroke.self) else { return } stroke.object?.bounds = stroke.bounds @@ -168,10 +174,10 @@ extension GraphicContext { } func cancelStroke() { - if !strokes.isEmpty { - let stroke = strokes.removeLast() - withPersistence(\.backgroundContext) { [graphicContext = object, _stroke = stroke] context in - if let stroke = _stroke.stroke(as: PenStroke.self)?.object { + if !tree.isEmpty, let stroke = currentStroke?.stroke(as: PenStroke.self) { + let _stroke = tree.remove(stroke, in: stroke.strokeBox) + withPersistence(\.backgroundContext) { [graphicContext = object, _stroke] context in + if let stroke = _stroke?.stroke(as: PenStroke.self)?.object { graphicContext?.strokes.remove(stroke) context.delete(stroke) } diff --git a/Memola/Canvas/Core/Canvas.swift b/Memola/Canvas/Core/Canvas.swift index 477d3d7..f4ee75b 100644 --- a/Memola/Canvas/Core/Canvas.swift +++ b/Memola/Canvas/Core/Canvas.swift @@ -123,10 +123,6 @@ extension Canvas { func setGraphicRenderType(_ renderType: GraphicContext.RenderType) { graphicContext.renderType = renderType } - - func getNewlyAddedStroke() -> (any Stroke)? { - graphicContext.strokes.last - } } // MARK: - Rendering diff --git a/Memola/Canvas/Geometries/Stroke/Core/Stroke.swift b/Memola/Canvas/Geometries/Stroke/Core/Stroke.swift index 6829343..f1d959e 100644 --- a/Memola/Canvas/Geometries/Stroke/Core/Stroke.swift +++ b/Memola/Canvas/Geometries/Stroke/Core/Stroke.swift @@ -8,7 +8,7 @@ import MetalKit import Foundation -protocol Stroke: AnyObject, Drawable, Hashable, Equatable { +protocol Stroke: AnyObject, Drawable, Hashable, Equatable, Comparable { var id: UUID { get set } var bounds: [CGFloat] { get set } var color: [CGFloat] { get set } @@ -43,6 +43,10 @@ extension Stroke { return CGRect(x: x, y: y, width: width, height: height) } + var strokeBox: Box { + Box(minX: bounds[0], minY: bounds[1], maxX: bounds[2], maxY: bounds[3]) + } + func isVisible(in bounds: CGRect) -> Bool { bounds.contains(strokeBounds) || bounds.intersects(strokeBounds) } @@ -107,6 +111,10 @@ extension Stroke { func hash(into hasher: inout Hasher) { hasher.combine(id) } + + static func < (lhs: Self, rhs: Self) -> Bool { + lhs.createdAt < rhs.createdAt + } } extension Stroke { diff --git a/Memola/Canvas/History/History.swift b/Memola/Canvas/History/History.swift index 501e0af..825f357 100644 --- a/Memola/Canvas/History/History.swift +++ b/Memola/Canvas/History/History.swift @@ -23,12 +23,12 @@ class History: ObservableObject { redoStack.isEmpty } - func undo() -> Bool { + func undo() -> HistoryEvent? { guard let event = undoStack.popLast() else { - return false + return nil } addRedo(event) - return true + return event } func redo() -> HistoryEvent? { diff --git a/Memola/Canvas/RTree/RTree.swift b/Memola/Canvas/RTree/RTree.swift index 82c0351..be75479 100644 --- a/Memola/Canvas/RTree/RTree.swift +++ b/Memola/Canvas/RTree/RTree.swift @@ -18,6 +18,10 @@ class RTree where T: Equatable & Comparable { self.root = Node.createNode() } + var isEmpty: Bool { + root.children.isEmpty + } + // MARK: - Retrival func traverse() -> [T] { _traverse(from: root) @@ -40,7 +44,7 @@ class RTree where T: Equatable & Comparable { return result } - func _merge(_ left: [T], _ right: [T]) -> [T] { + private func _merge(_ left: [T], _ right: [T]) -> [T] { var mergedArray: [T] = [] var leftIndex = 0 var rightIndex = 0 diff --git a/Memola/Canvas/RenderPasses/GraphicRenderPass.swift b/Memola/Canvas/RenderPasses/GraphicRenderPass.swift index 8f68bb6..6e34684 100644 --- a/Memola/Canvas/RenderPasses/GraphicRenderPass.swift +++ b/Memola/Canvas/RenderPasses/GraphicRenderPass.swift @@ -45,7 +45,9 @@ class GraphicRenderPass: RenderPass { let graphicContext = canvas.graphicContext if renderer.redrawsGraphicRender { canvas.setGraphicRenderType(.finished) - for stroke in graphicContext.strokes { + let strokes = graphicContext.tree.search(box: canvas.bounds.box) + print(strokes.count) + for stroke in strokes { if graphicContext.previousStroke === stroke || graphicContext.currentStroke === stroke { continue } diff --git a/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift b/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift index 818c762..5e3bac4 100644 --- a/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift +++ b/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift @@ -141,8 +141,8 @@ extension CanvasViewController { func updateDocumentBounds() { var bounds = scrollView.bounds.muliply(by: drawingView.ratio / scrollView.zoomScale) - let xDelta = bounds.minX * 0.05 - let yDelta = bounds.minY * 0.05 + let xDelta = bounds.minX * 0.0 + let yDelta = bounds.minY * 0.0 bounds.origin.x -= xDelta bounds.origin.y -= yDelta bounds.size.width += xDelta * 2 @@ -323,9 +323,9 @@ extension CanvasViewController { extension CanvasViewController { func historyUndid() { - guard history.undo() else { return } + guard let event = history.undo() else { return } drawingView.disableUserInteraction() - canvas.graphicContext.undoGraphic() + canvas.graphicContext.undoGraphic(for: event) renderer.redrawsGraphicRender = true renderer.resize(on: renderView, to: renderView.drawableSize) renderView.draw() diff --git a/Memola/Extensions/CGRect++.swift b/Memola/Extensions/CGRect++.swift index ca95178..9f92c7b 100644 --- a/Memola/Extensions/CGRect++.swift +++ b/Memola/Extensions/CGRect++.swift @@ -20,4 +20,8 @@ extension CGRect { func muliply(by factor: CGFloat) -> CGRect { CGRect(origin: origin.muliply(by: factor), size: size.multiply(by: factor)) } + + var box: Box { + Box(minX: minX, minY: minY, maxX: maxX, maxY: maxY) + } }