feat: replace array with rtree

This commit is contained in:
dscyrescotti
2024-06-05 22:58:15 +07:00
parent e4dc567819
commit 90dfe97f9e
8 changed files with 54 additions and 34 deletions

View File

@@ -11,7 +11,7 @@ import CoreData
import Foundation import Foundation
final class GraphicContext: @unchecked Sendable { final class GraphicContext: @unchecked Sendable {
var strokes: [any Stroke] = [] var tree: RTree = RTree<PenStroke>(maxEntries: 8)
var object: GraphicContextObject? var object: GraphicContextObject?
var currentStroke: (any Stroke)? var currentStroke: (any Stroke)?
@@ -37,20 +37,26 @@ final class GraphicContext: @unchecked Sendable {
] ]
} }
func undoGraphic() { func undoGraphic(for event: HistoryEvent) {
guard !strokes.isEmpty else { return } switch event {
let stroke = strokes.removeLast() case .stroke(let stroke):
withPersistence(\.backgroundContext) { [stroke] context in guard let _stroke = stroke.stroke(as: PenStroke.self) else { return }
stroke.stroke(as: PenStroke.self)?.object?.graphicContext = nil let deletedStroke = tree.remove(_stroke, in: _stroke.strokeBox)
try context.saveIfNeeded() 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) { func redoGraphic(for event: HistoryEvent) {
switch event { switch event {
case .stroke(let stroke): 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 withPersistence(\.backgroundContext) { [weak self, stroke] context in
stroke.stroke(as: PenStroke.self)?.object?.graphicContext = self?.object stroke.stroke(as: PenStroke.self)?.object?.graphicContext = self?.object
try context.saveIfNeeded() try context.saveIfNeeded()
@@ -65,9 +71,10 @@ extension GraphicContext {
guard let object else { return } guard let object else { return }
let queue = OperationQueue() let queue = OperationQueue()
queue.qualityOfService = .userInteractive queue.qualityOfService = .userInteractive
self.strokes = object.strokes.compactMap { stroke -> PenStroke? in object.strokes.forEach { stroke in
guard let stroke = stroke as? StrokeObject else { return nil } guard let stroke = stroke as? StrokeObject else { return }
let _stroke = PenStroke(object: stroke) let _stroke = PenStroke(object: stroke)
tree.insert(_stroke, 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 {
@@ -85,14 +92,13 @@ extension GraphicContext {
context.refresh(stroke, mergeChanges: false) context.refresh(stroke, mergeChanges: false)
} }
} }
return _stroke
} }
queue.waitUntilAllOperationsAreFinished() queue.waitUntilAllOperationsAreFinished()
} }
func loadQuads(_ bounds: CGRect) { func loadQuads(_ bounds: CGRect) {
for stroke in self.strokes { for stroke in self.tree.search(box: bounds.box) {
guard stroke.isVisible(in: bounds), stroke.isEmpty else { continue } guard stroke.isEmpty else { continue }
stroke.stroke(as: PenStroke.self)?.loadQuads() stroke.stroke(as: PenStroke.self)?.loadQuads()
} }
} }
@@ -135,7 +141,6 @@ extension GraphicContext {
graphicContext?.strokes.add(stroke) graphicContext?.strokes.add(stroke)
_stroke.object = stroke _stroke.object = stroke
} }
strokes.append(stroke)
currentStroke = stroke currentStroke = stroke
currentPoint = point currentPoint = point
currentStroke?.begin(at: point) currentStroke?.begin(at: point)
@@ -152,8 +157,9 @@ extension GraphicContext {
} }
func endStroke(at point: CGPoint) { 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) currentStroke.finish(at: point)
tree.insert(currentStroke, in: currentStroke.strokeBox)
withPersistence(\.backgroundContext) { [currentStroke] context in withPersistence(\.backgroundContext) { [currentStroke] context in
guard let stroke = currentStroke.stroke(as: PenStroke.self) else { return } guard let stroke = currentStroke.stroke(as: PenStroke.self) else { return }
stroke.object?.bounds = stroke.bounds stroke.object?.bounds = stroke.bounds
@@ -168,10 +174,10 @@ extension GraphicContext {
} }
func cancelStroke() { func cancelStroke() {
if !strokes.isEmpty { if !tree.isEmpty, let stroke = currentStroke?.stroke(as: PenStroke.self) {
let stroke = strokes.removeLast() let _stroke = tree.remove(stroke, in: stroke.strokeBox)
withPersistence(\.backgroundContext) { [graphicContext = object, _stroke = stroke] context in withPersistence(\.backgroundContext) { [graphicContext = object, _stroke] context in
if let stroke = _stroke.stroke(as: PenStroke.self)?.object { if let stroke = _stroke?.stroke(as: PenStroke.self)?.object {
graphicContext?.strokes.remove(stroke) graphicContext?.strokes.remove(stroke)
context.delete(stroke) context.delete(stroke)
} }

View File

@@ -123,10 +123,6 @@ extension Canvas {
func setGraphicRenderType(_ renderType: GraphicContext.RenderType) { func setGraphicRenderType(_ renderType: GraphicContext.RenderType) {
graphicContext.renderType = renderType graphicContext.renderType = renderType
} }
func getNewlyAddedStroke() -> (any Stroke)? {
graphicContext.strokes.last
}
} }
// MARK: - Rendering // MARK: - Rendering

View File

@@ -8,7 +8,7 @@
import MetalKit import MetalKit
import Foundation import Foundation
protocol Stroke: AnyObject, Drawable, Hashable, Equatable { protocol Stroke: AnyObject, Drawable, Hashable, Equatable, Comparable {
var id: UUID { get set } var id: UUID { get set }
var bounds: [CGFloat] { get set } var bounds: [CGFloat] { get set }
var color: [CGFloat] { get set } var color: [CGFloat] { get set }
@@ -43,6 +43,10 @@ extension Stroke {
return CGRect(x: x, y: y, width: width, height: height) 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 { func isVisible(in bounds: CGRect) -> Bool {
bounds.contains(strokeBounds) || bounds.intersects(strokeBounds) bounds.contains(strokeBounds) || bounds.intersects(strokeBounds)
} }
@@ -107,6 +111,10 @@ extension Stroke {
func hash(into hasher: inout Hasher) { func hash(into hasher: inout Hasher) {
hasher.combine(id) hasher.combine(id)
} }
static func < (lhs: Self, rhs: Self) -> Bool {
lhs.createdAt < rhs.createdAt
}
} }
extension Stroke { extension Stroke {

View File

@@ -23,12 +23,12 @@ class History: ObservableObject {
redoStack.isEmpty redoStack.isEmpty
} }
func undo() -> Bool { func undo() -> HistoryEvent? {
guard let event = undoStack.popLast() else { guard let event = undoStack.popLast() else {
return false return nil
} }
addRedo(event) addRedo(event)
return true return event
} }
func redo() -> HistoryEvent? { func redo() -> HistoryEvent? {

View File

@@ -18,6 +18,10 @@ class RTree<T> where T: Equatable & Comparable {
self.root = Node<T>.createNode() self.root = Node<T>.createNode()
} }
var isEmpty: Bool {
root.children.isEmpty
}
// MARK: - Retrival // MARK: - Retrival
func traverse() -> [T] { func traverse() -> [T] {
_traverse(from: root) _traverse(from: root)
@@ -40,7 +44,7 @@ class RTree<T> where T: Equatable & Comparable {
return result return result
} }
func _merge(_ left: [T], _ right: [T]) -> [T] { private func _merge(_ left: [T], _ right: [T]) -> [T] {
var mergedArray: [T] = [] var mergedArray: [T] = []
var leftIndex = 0 var leftIndex = 0
var rightIndex = 0 var rightIndex = 0

View File

@@ -45,7 +45,9 @@ class GraphicRenderPass: RenderPass {
let graphicContext = canvas.graphicContext let graphicContext = canvas.graphicContext
if renderer.redrawsGraphicRender { if renderer.redrawsGraphicRender {
canvas.setGraphicRenderType(.finished) 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 { if graphicContext.previousStroke === stroke || graphicContext.currentStroke === stroke {
continue continue
} }

View File

@@ -141,8 +141,8 @@ extension CanvasViewController {
func updateDocumentBounds() { func updateDocumentBounds() {
var bounds = scrollView.bounds.muliply(by: drawingView.ratio / scrollView.zoomScale) var bounds = scrollView.bounds.muliply(by: drawingView.ratio / scrollView.zoomScale)
let xDelta = bounds.minX * 0.05 let xDelta = bounds.minX * 0.0
let yDelta = bounds.minY * 0.05 let yDelta = bounds.minY * 0.0
bounds.origin.x -= xDelta bounds.origin.x -= xDelta
bounds.origin.y -= yDelta bounds.origin.y -= yDelta
bounds.size.width += xDelta * 2 bounds.size.width += xDelta * 2
@@ -323,9 +323,9 @@ extension CanvasViewController {
extension CanvasViewController { extension CanvasViewController {
func historyUndid() { func historyUndid() {
guard history.undo() else { return } guard let event = history.undo() else { return }
drawingView.disableUserInteraction() drawingView.disableUserInteraction()
canvas.graphicContext.undoGraphic() canvas.graphicContext.undoGraphic(for: event)
renderer.redrawsGraphicRender = true renderer.redrawsGraphicRender = true
renderer.resize(on: renderView, to: renderView.drawableSize) renderer.resize(on: renderView, to: renderView.drawableSize)
renderView.draw() renderView.draw()

View File

@@ -20,4 +20,8 @@ extension CGRect {
func muliply(by factor: CGFloat) -> CGRect { func muliply(by factor: CGFloat) -> CGRect {
CGRect(origin: origin.muliply(by: factor), size: size.multiply(by: factor)) CGRect(origin: origin.muliply(by: factor), size: size.multiply(by: factor))
} }
var box: Box {
Box(minX: minX, minY: minY, maxX: maxX, maxY: maxY)
}
} }