mirror of
https://github.com/dscyrescotti/Memola.git
synced 2026-05-17 05:07:11 +02:00
feat: replace array with rtree
This commit is contained in:
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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? {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user