mirror of
https://github.com/dscyrescotti/Memola.git
synced 2026-04-25 10:08:34 +02:00
Merge pull request #41 from dscyrescotti/feature/r-tree-impl
Implement RTree Data Structure
This commit is contained in:
@@ -12,6 +12,10 @@
|
|||||||
EC0D14262BF7A8C9009BFE5F /* PenObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC0D14252BF7A8C9009BFE5F /* PenObject.swift */; };
|
EC0D14262BF7A8C9009BFE5F /* PenObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC0D14252BF7A8C9009BFE5F /* PenObject.swift */; };
|
||||||
EC0D14282BF7BF20009BFE5F /* ContextMenuViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC0D14272BF7BF20009BFE5F /* ContextMenuViewModifier.swift */; };
|
EC0D14282BF7BF20009BFE5F /* ContextMenuViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC0D14272BF7BF20009BFE5F /* ContextMenuViewModifier.swift */; };
|
||||||
EC1B783D2BFA0AC9005A34E2 /* Toolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC1B783C2BFA0AC9005A34E2 /* Toolbar.swift */; };
|
EC1B783D2BFA0AC9005A34E2 /* Toolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC1B783C2BFA0AC9005A34E2 /* Toolbar.swift */; };
|
||||||
|
EC2106AD2C10C2A700FBE27C /* AnyStroke.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC2106AC2C10C2A700FBE27C /* AnyStroke.swift */; };
|
||||||
|
EC2BEBF42C0F5FF7005DB0AF /* RTree.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC2BEBF32C0F5FF7005DB0AF /* RTree.swift */; };
|
||||||
|
EC2BEBF62C0F600D005DB0AF /* Box.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC2BEBF52C0F600D005DB0AF /* Box.swift */; };
|
||||||
|
EC2BEBF82C0F601A005DB0AF /* Node.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC2BEBF72C0F601A005DB0AF /* Node.swift */; };
|
||||||
EC3565522BEFC65F00A4E0BF /* NSManagedObjectContext++.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC3565512BEFC65F00A4E0BF /* NSManagedObjectContext++.swift */; };
|
EC3565522BEFC65F00A4E0BF /* NSManagedObjectContext++.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC3565512BEFC65F00A4E0BF /* NSManagedObjectContext++.swift */; };
|
||||||
EC3565542BEFC6AD00A4E0BF /* View++.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC3565532BEFC6AD00A4E0BF /* View++.swift */; };
|
EC3565542BEFC6AD00A4E0BF /* View++.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC3565532BEFC6AD00A4E0BF /* View++.swift */; };
|
||||||
EC3565562BEFC7B300A4E0BF /* NSManagedObject++.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC3565552BEFC7B300A4E0BF /* NSManagedObject++.swift */; };
|
EC3565562BEFC7B300A4E0BF /* NSManagedObject++.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC3565552BEFC7B300A4E0BF /* NSManagedObject++.swift */; };
|
||||||
@@ -95,6 +99,10 @@
|
|||||||
EC0D14252BF7A8C9009BFE5F /* PenObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PenObject.swift; sourceTree = "<group>"; };
|
EC0D14252BF7A8C9009BFE5F /* PenObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PenObject.swift; sourceTree = "<group>"; };
|
||||||
EC0D14272BF7BF20009BFE5F /* ContextMenuViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextMenuViewModifier.swift; sourceTree = "<group>"; };
|
EC0D14272BF7BF20009BFE5F /* ContextMenuViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextMenuViewModifier.swift; sourceTree = "<group>"; };
|
||||||
EC1B783C2BFA0AC9005A34E2 /* Toolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Toolbar.swift; sourceTree = "<group>"; };
|
EC1B783C2BFA0AC9005A34E2 /* Toolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Toolbar.swift; sourceTree = "<group>"; };
|
||||||
|
EC2106AC2C10C2A700FBE27C /* AnyStroke.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyStroke.swift; sourceTree = "<group>"; };
|
||||||
|
EC2BEBF32C0F5FF7005DB0AF /* RTree.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RTree.swift; sourceTree = "<group>"; };
|
||||||
|
EC2BEBF52C0F600D005DB0AF /* Box.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Box.swift; sourceTree = "<group>"; };
|
||||||
|
EC2BEBF72C0F601A005DB0AF /* Node.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Node.swift; sourceTree = "<group>"; };
|
||||||
EC3565512BEFC65F00A4E0BF /* NSManagedObjectContext++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSManagedObjectContext++.swift"; sourceTree = "<group>"; };
|
EC3565512BEFC65F00A4E0BF /* NSManagedObjectContext++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSManagedObjectContext++.swift"; sourceTree = "<group>"; };
|
||||||
EC3565532BEFC6AD00A4E0BF /* View++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View++.swift"; sourceTree = "<group>"; };
|
EC3565532BEFC6AD00A4E0BF /* View++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View++.swift"; sourceTree = "<group>"; };
|
||||||
EC3565552BEFC7B300A4E0BF /* NSManagedObject++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSManagedObject++.swift"; sourceTree = "<group>"; };
|
EC3565552BEFC7B300A4E0BF /* NSManagedObject++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSManagedObject++.swift"; sourceTree = "<group>"; };
|
||||||
@@ -217,6 +225,16 @@
|
|||||||
path = Toolbar;
|
path = Toolbar;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
EC2BEBF22C0F5FE1005DB0AF /* RTree */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
EC2BEBF32C0F5FF7005DB0AF /* RTree.swift */,
|
||||||
|
EC2BEBF52C0F600D005DB0AF /* Box.swift */,
|
||||||
|
EC2BEBF72C0F601A005DB0AF /* Node.swift */,
|
||||||
|
);
|
||||||
|
path = RTree;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
EC5050042BF65CBC00B4D86E /* Core */ = {
|
EC5050042BF65CBC00B4D86E /* Core */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -362,6 +380,7 @@
|
|||||||
ECA7387E2BE5FE4200A4542E /* Canvas */ = {
|
ECA7387E2BE5FE4200A4542E /* Canvas */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
EC2BEBF22C0F5FE1005DB0AF /* RTree */,
|
||||||
ECA738F92BE6130000A4542E /* Geometries */,
|
ECA738F92BE6130000A4542E /* Geometries */,
|
||||||
ECA738812BE5FEEE00A4542E /* Abstracts */,
|
ECA738812BE5FEEE00A4542E /* Abstracts */,
|
||||||
ECA738992BE6018900A4542E /* Buffers */,
|
ECA738992BE6018900A4542E /* Buffers */,
|
||||||
@@ -636,6 +655,7 @@
|
|||||||
ECE883B92C009DCA0045C53D /* Core */ = {
|
ECE883B92C009DCA0045C53D /* Core */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
EC2106AC2C10C2A700FBE27C /* AnyStroke.swift */,
|
||||||
ECE883BE2C00AB440045C53D /* Stroke.swift */,
|
ECE883BE2C00AB440045C53D /* Stroke.swift */,
|
||||||
ECA738D32BE60F9100A4542E /* StrokeGenerator.swift */,
|
ECA738D32BE60F9100A4542E /* StrokeGenerator.swift */,
|
||||||
ECE883C02C00C9CB0045C53D /* StrokeStyle.swift */,
|
ECE883C02C00C9CB0045C53D /* StrokeStyle.swift */,
|
||||||
@@ -768,6 +788,7 @@
|
|||||||
ECA738A62BE6023F00A4542E /* GridUniforms.swift in Sources */,
|
ECA738A62BE6023F00A4542E /* GridUniforms.swift in Sources */,
|
||||||
ECA738D72BE60FC100A4542E /* SolidPointStrokeGenerator.swift in Sources */,
|
ECA738D72BE60FC100A4542E /* SolidPointStrokeGenerator.swift in Sources */,
|
||||||
EC3565542BEFC6AD00A4E0BF /* View++.swift in Sources */,
|
EC3565542BEFC6AD00A4E0BF /* View++.swift in Sources */,
|
||||||
|
EC2BEBF62C0F600D005DB0AF /* Box.swift in Sources */,
|
||||||
ECA738832BE5FEFE00A4542E /* RenderPass.swift in Sources */,
|
ECA738832BE5FEFE00A4542E /* RenderPass.swift in Sources */,
|
||||||
ECEC01A82BEE11BA006DA24C /* QuadShape.swift in Sources */,
|
ECEC01A82BEE11BA006DA24C /* QuadShape.swift in Sources */,
|
||||||
ECA738862BE5FF2500A4542E /* Canvas.swift in Sources */,
|
ECA738862BE5FF2500A4542E /* Canvas.swift in Sources */,
|
||||||
@@ -794,6 +815,7 @@
|
|||||||
EC4538892BEBCAE000A86FEC /* Quad.swift in Sources */,
|
EC4538892BEBCAE000A86FEC /* Quad.swift in Sources */,
|
||||||
ECE883BD2C00AA170045C53D /* EraserStroke.swift in Sources */,
|
ECE883BD2C00AA170045C53D /* EraserStroke.swift in Sources */,
|
||||||
ECA7388F2BE600DA00A4542E /* Grid.metal in Sources */,
|
ECA7388F2BE600DA00A4542E /* Grid.metal in Sources */,
|
||||||
|
EC2BEBF42C0F5FF7005DB0AF /* RTree.swift in Sources */,
|
||||||
ECA738C92BE60EF700A4542E /* GraphicContext.swift in Sources */,
|
ECA738C92BE60EF700A4542E /* GraphicContext.swift in Sources */,
|
||||||
ECA738F62BE612B700A4542E /* MTLDevice++.swift in Sources */,
|
ECA738F62BE612B700A4542E /* MTLDevice++.swift in Sources */,
|
||||||
EC0D14212BF79C73009BFE5F /* ToolObject.swift in Sources */,
|
EC0D14212BF79C73009BFE5F /* ToolObject.swift in Sources */,
|
||||||
@@ -812,8 +834,10 @@
|
|||||||
ECA738C12BE60E5300A4542E /* PenStyle.swift in Sources */,
|
ECA738C12BE60E5300A4542E /* PenStyle.swift in Sources */,
|
||||||
ECA738DE2BE610A000A4542E /* ViewPortRenderPass.swift in Sources */,
|
ECA738DE2BE610A000A4542E /* ViewPortRenderPass.swift in Sources */,
|
||||||
EC7F6BEC2BE5E6E300A34A7B /* MemolaApp.swift in Sources */,
|
EC7F6BEC2BE5E6E300A34A7B /* MemolaApp.swift in Sources */,
|
||||||
|
EC2BEBF82C0F601A005DB0AF /* Node.swift in Sources */,
|
||||||
ECA738A02BE601E400A4542E /* ViewPortVertex.swift in Sources */,
|
ECA738A02BE601E400A4542E /* ViewPortVertex.swift in Sources */,
|
||||||
EC0D14282BF7BF20009BFE5F /* ContextMenuViewModifier.swift in Sources */,
|
EC0D14282BF7BF20009BFE5F /* ContextMenuViewModifier.swift in Sources */,
|
||||||
|
EC2106AD2C10C2A700FBE27C /* AnyStroke.swift in Sources */,
|
||||||
ECA738BC2BE60E0300A4542E /* Tool.swift in Sources */,
|
ECA738BC2BE60E0300A4542E /* Tool.swift in Sources */,
|
||||||
ECA738972BE6014200A4542E /* Graphic.metal in Sources */,
|
ECA738972BE6014200A4542E /* Graphic.metal in Sources */,
|
||||||
ECA7388A2BE6006A00A4542E /* PipelineStates.swift in Sources */,
|
ECA7388A2BE6006A00A4542E /* PipelineStates.swift in Sources */,
|
||||||
|
|||||||
@@ -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<AnyStroke>(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.anyStroke, 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.anyStroke, 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.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 {
|
||||||
@@ -85,15 +92,14 @@ 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 let stroke = _stroke.stroke(as: PenStroke.self), stroke.isEmpty else { continue }
|
||||||
stroke.stroke(as: PenStroke.self)?.loadQuads()
|
stroke.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,13 +157,15 @@ 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)
|
||||||
currentStroke.saveQuads(to: currentStroke.quads.endIndex)
|
tree.insert(currentStroke.anyStroke, in: currentStroke.strokeBox)
|
||||||
withPersistence(\.backgroundContext) { context in
|
withPersistence(\.backgroundContext) { [currentStroke] context in
|
||||||
|
guard let stroke = currentStroke.stroke(as: PenStroke.self) else { return }
|
||||||
|
stroke.object?.bounds = stroke.bounds
|
||||||
try context.saveIfNeeded()
|
try context.saveIfNeeded()
|
||||||
if let stroke = currentStroke.stroke(as: PenStroke.self)?.object {
|
if let object = stroke.object {
|
||||||
context.refresh(stroke, mergeChanges: false)
|
context.refresh(object, mergeChanges: false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
previousStroke = currentStroke
|
previousStroke = currentStroke
|
||||||
@@ -167,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.anyStroke, 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
|
||||||
|
|||||||
28
Memola/Canvas/Geometries/Stroke/Core/AnyStroke.swift
Normal file
28
Memola/Canvas/Geometries/Stroke/Core/AnyStroke.swift
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
//
|
||||||
|
// AnyStroke.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 6/5/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct AnyStroke: Equatable, Comparable {
|
||||||
|
var value: any Stroke
|
||||||
|
|
||||||
|
init(_ value: any Stroke) {
|
||||||
|
self.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
static func == (lhs: AnyStroke, rhs: AnyStroke) -> Bool {
|
||||||
|
lhs.value.id == rhs.value.id
|
||||||
|
}
|
||||||
|
|
||||||
|
static func < (lhs: AnyStroke, rhs: AnyStroke) -> Bool {
|
||||||
|
lhs.value.createdAt < rhs.value.createdAt
|
||||||
|
}
|
||||||
|
|
||||||
|
func stroke<S: Stroke>(as type: S.Type) -> S? {
|
||||||
|
value.stroke(as: type)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 }
|
||||||
@@ -18,8 +18,6 @@ protocol Stroke: AnyObject, Drawable, Hashable, Equatable {
|
|||||||
var quads: [Quad] { get set }
|
var quads: [Quad] { get set }
|
||||||
var penStyle: any PenStyle { get set }
|
var penStyle: any PenStyle { get set }
|
||||||
|
|
||||||
var batchIndex: Int { get set }
|
|
||||||
var quadIndex: Int { get set }
|
|
||||||
var keyPoints: [CGPoint] { get set }
|
var keyPoints: [CGPoint] { get set }
|
||||||
var movingAverage: MovingAverage { get set }
|
var movingAverage: MovingAverage { get set }
|
||||||
|
|
||||||
@@ -32,8 +30,6 @@ protocol Stroke: AnyObject, Drawable, Hashable, Equatable {
|
|||||||
func finish(at point: CGPoint)
|
func finish(at point: CGPoint)
|
||||||
|
|
||||||
func addQuad(at point: CGPoint, rotation: CGFloat, shape: QuadShape)
|
func addQuad(at point: CGPoint, rotation: CGFloat, shape: QuadShape)
|
||||||
func removeQuads(from index: Int)
|
|
||||||
func saveQuads(to index: Int)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Stroke {
|
extension Stroke {
|
||||||
@@ -47,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)
|
||||||
}
|
}
|
||||||
@@ -78,11 +78,6 @@ extension Stroke {
|
|||||||
)
|
)
|
||||||
quads.append(quad)
|
quads.append(quad)
|
||||||
}
|
}
|
||||||
|
|
||||||
func removeQuads(from index: Int) {
|
|
||||||
let dropCount = quads.endIndex - max(1, index)
|
|
||||||
quads.removeLast(dropCount)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Stroke {
|
extension Stroke {
|
||||||
@@ -116,10 +111,18 @@ 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 {
|
||||||
func stroke<S: Stroke>(as type: S.Type) -> S? {
|
func stroke<S: Stroke>(as type: S.Type) -> S? {
|
||||||
self as? S
|
self as? S
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var anyStroke: AnyStroke {
|
||||||
|
AnyStroke(self)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,9 +29,6 @@ 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:
|
||||||
let quadIndex = stroke.quadIndex + 1
|
|
||||||
stroke.removeQuads(from: quadIndex)
|
|
||||||
stroke.saveQuads(to: quadIndex)
|
|
||||||
let index = stroke.keyPoints.endIndex - 1
|
let index = stroke.keyPoints.endIndex - 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])
|
||||||
@@ -42,7 +39,7 @@ struct SolidPointStrokeGenerator: StrokeGenerator {
|
|||||||
end = CGPoint.middle(p1: stroke.keyPoints[index - 1], p2: stroke.keyPoints[index])
|
end = CGPoint.middle(p1: stroke.keyPoints[index - 1], p2: stroke.keyPoints[index])
|
||||||
addCurve(from: start, to: end, by: control, on: stroke)
|
addCurve(from: start, to: end, by: control, on: stroke)
|
||||||
default:
|
default:
|
||||||
smoothOutPath(on: stroke)
|
adjustKeyPoint(on: stroke)
|
||||||
let index = stroke.keyPoints.endIndex - 1
|
let index = stroke.keyPoints.endIndex - 1
|
||||||
let start = CGPoint.middle(p1: stroke.keyPoints[index - 2], p2: stroke.keyPoints[index - 1])
|
let start = CGPoint.middle(p1: stroke.keyPoints[index - 2], p2: stroke.keyPoints[index - 1])
|
||||||
let control = stroke.keyPoints[index - 1]
|
let control = stroke.keyPoints[index - 1]
|
||||||
@@ -60,29 +57,6 @@ struct SolidPointStrokeGenerator: StrokeGenerator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func smoothOutPath(on stroke: any Stroke) {
|
|
||||||
let quadIndex = stroke.quadIndex + 1
|
|
||||||
stroke.removeQuads(from: quadIndex)
|
|
||||||
stroke.saveQuads(to: quadIndex)
|
|
||||||
adjustKeyPoint(on: stroke)
|
|
||||||
switch stroke.keyPoints.endIndex {
|
|
||||||
case 4:
|
|
||||||
let index = stroke.keyPoints.endIndex - 2
|
|
||||||
let start = stroke.keyPoints[index - 2]
|
|
||||||
let end = CGPoint.middle(p1: stroke.keyPoints[index - 2], p2: stroke.keyPoints[index - 1])
|
|
||||||
let control = CGPoint.middle(p1: start, p2: end)
|
|
||||||
addCurve(from: start, to: end, by: control, on: stroke)
|
|
||||||
fallthrough
|
|
||||||
default:
|
|
||||||
let index = stroke.keyPoints.endIndex - 2
|
|
||||||
let start = CGPoint.middle(p1: stroke.keyPoints[index - 2], p2: stroke.keyPoints[index - 1])
|
|
||||||
let control = stroke.keyPoints[index - 1]
|
|
||||||
let end = CGPoint.middle(p1: stroke.keyPoints[index - 1], p2: stroke.keyPoints[index])
|
|
||||||
addCurve(from: start, to: end, by: control, on: stroke)
|
|
||||||
}
|
|
||||||
stroke.quadIndex = stroke.quads.endIndex - 1
|
|
||||||
}
|
|
||||||
|
|
||||||
private func adjustKeyPoint(on stroke: any Stroke) {
|
private func adjustKeyPoint(on stroke: any Stroke) {
|
||||||
let index = stroke.keyPoints.endIndex - 1
|
let index = stroke.keyPoints.endIndex - 1
|
||||||
let prev = stroke.keyPoints[index - 1]
|
let prev = stroke.keyPoints[index - 1]
|
||||||
|
|||||||
@@ -18,8 +18,6 @@ final class EraserStroke: Stroke, @unchecked Sendable {
|
|||||||
var quads: [Quad]
|
var quads: [Quad]
|
||||||
var penStyle: any PenStyle
|
var penStyle: any PenStyle
|
||||||
|
|
||||||
var batchIndex: Int = 0
|
|
||||||
var quadIndex: Int = -1
|
|
||||||
var keyPoints: [CGPoint] = []
|
var keyPoints: [CGPoint] = []
|
||||||
var movingAverage: MovingAverage = MovingAverage(windowSize: 3)
|
var movingAverage: MovingAverage = MovingAverage(windowSize: 3)
|
||||||
|
|
||||||
@@ -43,8 +41,4 @@ final class EraserStroke: Stroke, @unchecked Sendable {
|
|||||||
self.quads = quads
|
self.quads = quads
|
||||||
self.penStyle = style.penStyle
|
self.penStyle = style.penStyle
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveQuads(to index: Int) {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,8 +19,6 @@ final class PenStroke: Stroke, @unchecked Sendable {
|
|||||||
var quads: [Quad]
|
var quads: [Quad]
|
||||||
var penStyle: any PenStyle
|
var penStyle: any PenStyle
|
||||||
|
|
||||||
var batchIndex: Int = 0
|
|
||||||
var quadIndex: Int = -1
|
|
||||||
var keyPoints: [CGPoint] = []
|
var keyPoints: [CGPoint] = []
|
||||||
var movingAverage: MovingAverage = MovingAverage(windowSize: 3)
|
var movingAverage: MovingAverage = MovingAverage(windowSize: 3)
|
||||||
|
|
||||||
@@ -71,30 +69,31 @@ final class PenStroke: Stroke, @unchecked Sendable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveQuads(to index: Int) {
|
func addQuad(at point: CGPoint, rotation: CGFloat, shape: QuadShape) {
|
||||||
let quads = Array(quads[batchIndex..<index])
|
let quad = Quad(
|
||||||
batchIndex = index
|
origin: point,
|
||||||
withPersistence(\.backgroundContext) { [weak self, object] context in
|
size: thickness,
|
||||||
guard let self else { return }
|
rotation: rotation,
|
||||||
var topLeft: CGPoint = CGPoint(x: bounds[0], y: bounds[1])
|
shape: shape.rawValue,
|
||||||
var bottomRight: CGPoint = CGPoint(x: bounds[2], y: bounds[3])
|
color: color
|
||||||
for _quad in quads {
|
)
|
||||||
let quad = QuadObject(\.backgroundContext)
|
quads.append(quad)
|
||||||
quad.originX = _quad.originX.cgFloat
|
bounds = [
|
||||||
quad.originY = _quad.originY.cgFloat
|
min(quad.originX.cgFloat, bounds[0]),
|
||||||
quad.size = _quad.size.cgFloat
|
min(quad.originY.cgFloat, bounds[1]),
|
||||||
quad.rotation = _quad.rotation.cgFloat
|
max(quad.originX.cgFloat, bounds[2]),
|
||||||
quad.shape = _quad.shape
|
max(quad.originY.cgFloat, bounds[3])
|
||||||
quad.color = _quad.getColor()
|
]
|
||||||
quad.stroke = object
|
withPersistence(\.backgroundContext) { [object, _quad = quad] context in
|
||||||
object?.quads.add(quad)
|
let quad = QuadObject(\.backgroundContext)
|
||||||
topLeft.x = min(quad.originX, topLeft.x)
|
quad.originX = _quad.originX.cgFloat
|
||||||
topLeft.y = min(quad.originY, topLeft.y)
|
quad.originY = _quad.originY.cgFloat
|
||||||
bottomRight.x = max(quad.originX, bottomRight.x)
|
quad.size = _quad.size.cgFloat
|
||||||
bottomRight.y = max(quad.originY, bottomRight.y)
|
quad.rotation = _quad.rotation.cgFloat
|
||||||
}
|
quad.shape = _quad.shape
|
||||||
bounds = [topLeft.x, topLeft.y, bottomRight.x, bottomRight.y]
|
quad.color = _quad.getColor()
|
||||||
object?.bounds = bounds
|
quad.stroke = object
|
||||||
|
object?.quads.add(quad)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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? {
|
||||||
|
|||||||
60
Memola/Canvas/RTree/Box.swift
Normal file
60
Memola/Canvas/RTree/Box.swift
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
//
|
||||||
|
// Box.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 6/4/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct Box: Equatable, Decodable {
|
||||||
|
var minX: Double
|
||||||
|
var minY: Double
|
||||||
|
var maxX: Double
|
||||||
|
var maxY: Double
|
||||||
|
|
||||||
|
init(minX: Double, minY: Double, maxX: Double, maxY: Double) {
|
||||||
|
self.minX = minX
|
||||||
|
self.minY = minY
|
||||||
|
self.maxX = maxX
|
||||||
|
self.maxY = maxY
|
||||||
|
}
|
||||||
|
|
||||||
|
var area: Double {
|
||||||
|
(maxX - minX) * (maxY - minY)
|
||||||
|
}
|
||||||
|
|
||||||
|
var margin: Double {
|
||||||
|
(maxX - minX) + (maxY - minY)
|
||||||
|
}
|
||||||
|
|
||||||
|
func enlargedArea(for box: Box) -> Double {
|
||||||
|
(max(box.maxX, maxX) - min(box.minX, minX)) * (max(box.maxY, maxY) - min(box.minY, minY))
|
||||||
|
}
|
||||||
|
|
||||||
|
func intersects(with box: Box) -> Bool {
|
||||||
|
box.minX <= maxX && box.minY <= maxY && box.maxX >= minX && box.maxY >= minY
|
||||||
|
}
|
||||||
|
|
||||||
|
func contains(with box: Box) -> Bool {
|
||||||
|
minX <= box.minX && minY <= box.minY && box.maxX <= maxX && box.maxY <= maxY
|
||||||
|
}
|
||||||
|
|
||||||
|
func intersectedArea(on box: Box) -> Double {
|
||||||
|
let minX = max(minX, box.minX)
|
||||||
|
let minY = max(minY, box.minY)
|
||||||
|
let maxX = min(maxX, box.maxX)
|
||||||
|
let maxY = min(maxY, box.maxY)
|
||||||
|
|
||||||
|
return max(0, maxX - minX) * max(0, maxY - minY)
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func enlarge(for box: Box) {
|
||||||
|
minX = min(minX, box.minX)
|
||||||
|
minY = min(minY, box.minY)
|
||||||
|
maxX = max(maxX, box.maxX)
|
||||||
|
maxY = max(maxY, box.maxY)
|
||||||
|
}
|
||||||
|
|
||||||
|
static var infinity = Box(minX: .infinity, minY: .infinity, maxX: -.infinity, maxY: -.infinity)
|
||||||
|
}
|
||||||
35
Memola/Canvas/RTree/Node.swift
Normal file
35
Memola/Canvas/RTree/Node.swift
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
//
|
||||||
|
// Node.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 6/4/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
class Node<T> where T: Equatable & Comparable {
|
||||||
|
var box: Box
|
||||||
|
var value: T?
|
||||||
|
var isLeaf: Bool
|
||||||
|
var height: Int
|
||||||
|
var children: [Node]
|
||||||
|
|
||||||
|
init(box: Box, value: T? = nil, isLeaf: Bool, height: Int, children: [Node] = []) {
|
||||||
|
self.box = box
|
||||||
|
self.value = value
|
||||||
|
self.isLeaf = isLeaf
|
||||||
|
self.height = height
|
||||||
|
self.children = children
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateBox() {
|
||||||
|
box = .infinity
|
||||||
|
for node in children {
|
||||||
|
box.enlarge(for: node.box)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func createNode(in box: Box = .infinity, for value: T? = nil, with children: [Node] = []) -> Node {
|
||||||
|
Node(box: box, value: value, isLeaf: true, height: 1, children: children)
|
||||||
|
}
|
||||||
|
}
|
||||||
316
Memola/Canvas/RTree/RTree.swift
Normal file
316
Memola/Canvas/RTree/RTree.swift
Normal file
@@ -0,0 +1,316 @@
|
|||||||
|
//
|
||||||
|
// RTree.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 6/4/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
class RTree<T> where T: Equatable & Comparable {
|
||||||
|
private var root: Node<T>
|
||||||
|
private let maxEntries: Int
|
||||||
|
private let minEntries: Int
|
||||||
|
|
||||||
|
init(maxEntries: Int = 9) {
|
||||||
|
self.maxEntries = max(4, maxEntries)
|
||||||
|
self.minEntries = max(2, Int(ceil(Double(maxEntries) * 0.4)))
|
||||||
|
self.root = Node<T>.createNode()
|
||||||
|
}
|
||||||
|
|
||||||
|
var isEmpty: Bool {
|
||||||
|
root.children.isEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Retrival
|
||||||
|
func traverse() -> [T] {
|
||||||
|
_traverse(from: root)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func _traverse(from _root: Node<T>) -> [T] {
|
||||||
|
var result: [T] = []
|
||||||
|
var queue: [Node<T>] = [_root]
|
||||||
|
while let node = queue.first {
|
||||||
|
queue.removeFirst()
|
||||||
|
if node.isLeaf {
|
||||||
|
let children = node.children
|
||||||
|
.compactMap { $0.value }
|
||||||
|
.sorted(by: <)
|
||||||
|
result = _merge(result, children)
|
||||||
|
} else {
|
||||||
|
queue.append(contentsOf: node.children)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private func _merge(_ left: [T], _ right: [T]) -> [T] {
|
||||||
|
var mergedArray: [T] = []
|
||||||
|
var leftIndex = 0
|
||||||
|
var rightIndex = 0
|
||||||
|
|
||||||
|
while leftIndex < left.count && rightIndex < right.count {
|
||||||
|
if left[leftIndex] < right[rightIndex] {
|
||||||
|
mergedArray.append(left[leftIndex])
|
||||||
|
leftIndex += 1
|
||||||
|
} else {
|
||||||
|
mergedArray.append(right[rightIndex])
|
||||||
|
rightIndex += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mergedArray.append(contentsOf: left[leftIndex...])
|
||||||
|
mergedArray.append(contentsOf: right[rightIndex...])
|
||||||
|
|
||||||
|
return mergedArray
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Search
|
||||||
|
func search(box: Box) -> [T] {
|
||||||
|
guard box.intersects(with: root.box) else { return [] }
|
||||||
|
var result: [T] = []
|
||||||
|
var queue: [Node<T>] = [root]
|
||||||
|
while let node = queue.first {
|
||||||
|
queue.removeFirst()
|
||||||
|
for childNode in node.children {
|
||||||
|
if box.intersects(with: childNode.box) {
|
||||||
|
if node.isLeaf {
|
||||||
|
if let value = childNode.value {
|
||||||
|
result = _merge(result, [value])
|
||||||
|
}
|
||||||
|
} else if box.contains(with: childNode.box) {
|
||||||
|
result = _merge(result, _traverse(from: childNode))
|
||||||
|
} else {
|
||||||
|
queue.append(childNode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Insertion
|
||||||
|
func insert(_ value: T, in box: Box) {
|
||||||
|
let node: Node = Node<T>.createNode(in: box, for: value)
|
||||||
|
_insert(node, level: root.height - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func _insert(_ node: Node<T>, level: Int) {
|
||||||
|
let box = node.box
|
||||||
|
var path: [Node<T>] = []
|
||||||
|
var level = level
|
||||||
|
let leafNode = _chooseSubtree(for: box, from: root, at: level, into: &path)
|
||||||
|
leafNode.children.append(node)
|
||||||
|
leafNode.box.enlarge(for: box)
|
||||||
|
while level >= 0 {
|
||||||
|
guard path[level].children.count > maxEntries else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
_split(on: path, at: level)
|
||||||
|
level -= 1
|
||||||
|
}
|
||||||
|
_adjustParentBoxes(with: box, through: path, at: level)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func _chooseSubtree(for box: Box, from rootNode: Node<T>, at level: Int, into path: inout [Node<T>]) -> Node<T> {
|
||||||
|
var node = rootNode
|
||||||
|
while true {
|
||||||
|
path.append(node)
|
||||||
|
if node.isLeaf || path.count - 1 == level { break }
|
||||||
|
var minArea: Double = .infinity
|
||||||
|
var minEnlargement: Double = .infinity
|
||||||
|
var targetNode: Node<T>?
|
||||||
|
for node in node.children {
|
||||||
|
let area = node.box.area
|
||||||
|
let enlargement = box.enlargedArea(for: node.box) - area
|
||||||
|
if enlargement < minEnlargement {
|
||||||
|
minEnlargement = enlargement
|
||||||
|
minArea = area < minArea ? area : minArea;
|
||||||
|
targetNode = node
|
||||||
|
} else if enlargement == minEnlargement {
|
||||||
|
if area < minArea {
|
||||||
|
minArea = area
|
||||||
|
targetNode = node
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
node = targetNode ?? node.children[0]
|
||||||
|
}
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Removal
|
||||||
|
@discardableResult
|
||||||
|
func remove(_ value: T, in box: Box) -> T? {
|
||||||
|
var node: Node<T>? = root
|
||||||
|
|
||||||
|
var path: [Node<T>] = []
|
||||||
|
var indices: [Int] = []
|
||||||
|
var parent: Node<T>?
|
||||||
|
var i: Int = 0
|
||||||
|
var goingUp: Bool = false
|
||||||
|
|
||||||
|
while node != nil || !path.isEmpty {
|
||||||
|
guard let currentNode = node else {
|
||||||
|
node = path.popLast()
|
||||||
|
parent = path.last
|
||||||
|
i = indices.popLast() ?? 0
|
||||||
|
goingUp = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if currentNode.isLeaf, let index = _findIndex(of: value, nodes: currentNode.children) {
|
||||||
|
let removedNode = currentNode.children.remove(at: index)
|
||||||
|
path.append(currentNode)
|
||||||
|
_condense(path)
|
||||||
|
return removedNode.value
|
||||||
|
}
|
||||||
|
if !goingUp && !currentNode.isLeaf && currentNode.box.contains(with: box) {
|
||||||
|
path.append(currentNode)
|
||||||
|
indices.append(i)
|
||||||
|
i = 0
|
||||||
|
parent = currentNode
|
||||||
|
node = currentNode.children[0]
|
||||||
|
} else if let parent {
|
||||||
|
i += 1
|
||||||
|
node = parent.children[safe: i]
|
||||||
|
goingUp = false
|
||||||
|
} else {
|
||||||
|
node = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private func _findIndex(of value: T, nodes: [Node<T>]) -> Int? {
|
||||||
|
for (index, node) in nodes.enumerated() {
|
||||||
|
if node.value == value { return index }
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private func _condense(_ path: [Node<T>]) {
|
||||||
|
var i = path.count - 1
|
||||||
|
while i >= 0 {
|
||||||
|
let node = path[i]
|
||||||
|
if node.children.isEmpty {
|
||||||
|
if i > 0 {
|
||||||
|
var siblings = path[i - 1].children
|
||||||
|
if let index = siblings.firstIndex(where: { $0 === node }) {
|
||||||
|
siblings.remove(at: index)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
root = .createNode()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_calculateBox(of: node)
|
||||||
|
}
|
||||||
|
i -= 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Splitting
|
||||||
|
private func _split(on path: [Node<T>], at level: Int) {
|
||||||
|
let node = path[level]
|
||||||
|
let numOfChildren = node.children.count
|
||||||
|
let minNumOfChildren = minEntries
|
||||||
|
|
||||||
|
_chooseSplitAxis(for: node, with: numOfChildren, by: minNumOfChildren)
|
||||||
|
let splitIndex = _chooseSplitIndex(for: node, with: numOfChildren, by: minNumOfChildren)
|
||||||
|
|
||||||
|
let children = Array(node.children[splitIndex..<node.children.count])
|
||||||
|
node.children.removeSubrange(splitIndex..<node.children.count)
|
||||||
|
let newNode: Node = Node<T>.createNode(with: children)
|
||||||
|
newNode.height = node.height
|
||||||
|
newNode.isLeaf = node.isLeaf
|
||||||
|
|
||||||
|
node.updateBox()
|
||||||
|
newNode.updateBox()
|
||||||
|
|
||||||
|
if level > 0 {
|
||||||
|
path[level - 1].children.append(newNode)
|
||||||
|
} else {
|
||||||
|
_splitRoot(with: node, and: newNode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func _splitRoot(with node: Node<T>, and newNode: Node<T>) {
|
||||||
|
root = Node<T>.createNode(with: [node, newNode])
|
||||||
|
root.height = node.height + 1
|
||||||
|
root.isLeaf = false
|
||||||
|
root.updateBox()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func _chooseSplitAxis(for node: Node<T>, with numOfChildren: Int, by minEntries: Int) {
|
||||||
|
let comparatorX: (Node<T>, Node<T>) -> Bool = { $0.box.minX < $1.box.minX }
|
||||||
|
let comparatorY: (Node<T>, Node<T>) -> Bool = { $0.box.minY < $1.box.minY }
|
||||||
|
let xMargin = _calculateDistributionMargin(for: node, with: numOfChildren, by: minEntries, using: comparatorX)
|
||||||
|
let yMargin = _calculateDistributionMargin(for: node, with: numOfChildren, by: minEntries, using: comparatorY)
|
||||||
|
if xMargin < yMargin {
|
||||||
|
node.children.sort(by: comparatorX)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func _calculateDistributionMargin(for node: Node<T>, with numOfChildren: Int, by minEntries: Int, using comparator: (Node<T>, Node<T>) -> Bool) -> Double {
|
||||||
|
node.children.sort(by: comparator)
|
||||||
|
let leftNode = _mergeChildNodes(of: node, from: 0, to: minEntries)
|
||||||
|
let rightNode = _mergeChildNodes(of: node, from: numOfChildren - minEntries, to: numOfChildren)
|
||||||
|
var margin = leftNode.box.margin + rightNode.box.margin
|
||||||
|
|
||||||
|
for index in minEntries..<numOfChildren - minEntries {
|
||||||
|
let node = node.children[index]
|
||||||
|
leftNode.box.enlarge(for: node.box)
|
||||||
|
margin += leftNode.box.margin
|
||||||
|
}
|
||||||
|
for index in stride(from: numOfChildren - minEntries - 1, through: minEntries, by: -1) {
|
||||||
|
let node = node.children[index]
|
||||||
|
rightNode.box.enlarge(for: node.box)
|
||||||
|
margin += rightNode.box.margin
|
||||||
|
}
|
||||||
|
return margin
|
||||||
|
}
|
||||||
|
|
||||||
|
private func _chooseSplitIndex(for node: Node<T>, with numOfChildren: Int, by minEntries: Int) -> Int {
|
||||||
|
var index: Int?
|
||||||
|
var minOverlap: Double = .infinity
|
||||||
|
var minArea: Double = .infinity
|
||||||
|
for idx in minEntries...numOfChildren - minEntries {
|
||||||
|
let node1 = _mergeChildNodes(of: node, from: 0, to: idx)
|
||||||
|
let node2 = _mergeChildNodes(of: node, from: idx, to: numOfChildren)
|
||||||
|
|
||||||
|
let overlap = node1.box.intersectedArea(on: node2.box)
|
||||||
|
let area = node1.box.area + node2.box.area
|
||||||
|
|
||||||
|
if overlap < minOverlap {
|
||||||
|
minOverlap = overlap
|
||||||
|
index = idx
|
||||||
|
minArea = min(area, minArea)
|
||||||
|
} else if overlap == minOverlap {
|
||||||
|
if area < minArea {
|
||||||
|
minArea = area
|
||||||
|
index = idx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return index ?? numOfChildren - minEntries
|
||||||
|
}
|
||||||
|
|
||||||
|
private func _adjustParentBoxes(with box: Box, through path: [Node<T>], at level: Int) {
|
||||||
|
for index in stride(from: level, through: 0, by: -1) {
|
||||||
|
path[index].box.enlarge(for: box)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func _calculateBox(of node: Node<T>) {
|
||||||
|
_mergeChildNodes(of: node, from: 0, to: node.children.count, into: node)
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
private func _mergeChildNodes(of node: Node<T>, from start: Int, to end: Int, into newNode: Node<T> = .createNode()) -> Node<T> {
|
||||||
|
newNode.box = .infinity
|
||||||
|
for index in start..<end {
|
||||||
|
let node = node.children[index]
|
||||||
|
newNode.box.enlarge(for: node.box)
|
||||||
|
}
|
||||||
|
return newNode
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -45,7 +45,8 @@ 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 {
|
for _stroke in graphicContext.tree.search(box: canvas.bounds.box) {
|
||||||
|
let stroke = _stroke.value
|
||||||
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