mirror of
https://github.com/dscyrescotti/Memola.git
synced 2026-03-21 00:49:27 +01:00
feat: implement rtree
This commit is contained in:
@@ -12,6 +12,9 @@
|
||||
EC0D14262BF7A8C9009BFE5F /* PenObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC0D14252BF7A8C9009BFE5F /* PenObject.swift */; };
|
||||
EC0D14282BF7BF20009BFE5F /* ContextMenuViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC0D14272BF7BF20009BFE5F /* ContextMenuViewModifier.swift */; };
|
||||
EC1B783D2BFA0AC9005A34E2 /* Toolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC1B783C2BFA0AC9005A34E2 /* Toolbar.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 */; };
|
||||
EC3565542BEFC6AD00A4E0BF /* View++.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC3565532BEFC6AD00A4E0BF /* View++.swift */; };
|
||||
EC3565562BEFC7B300A4E0BF /* NSManagedObject++.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC3565552BEFC7B300A4E0BF /* NSManagedObject++.swift */; };
|
||||
@@ -95,6 +98,9 @@
|
||||
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>"; };
|
||||
EC1B783C2BFA0AC9005A34E2 /* Toolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Toolbar.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>"; };
|
||||
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>"; };
|
||||
@@ -217,6 +223,16 @@
|
||||
path = Toolbar;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
EC2BEBF22C0F5FE1005DB0AF /* RTree */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
EC2BEBF32C0F5FF7005DB0AF /* RTree.swift */,
|
||||
EC2BEBF52C0F600D005DB0AF /* Box.swift */,
|
||||
EC2BEBF72C0F601A005DB0AF /* Node.swift */,
|
||||
);
|
||||
path = RTree;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
EC5050042BF65CBC00B4D86E /* Core */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -362,6 +378,7 @@
|
||||
ECA7387E2BE5FE4200A4542E /* Canvas */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
EC2BEBF22C0F5FE1005DB0AF /* RTree */,
|
||||
ECA738F92BE6130000A4542E /* Geometries */,
|
||||
ECA738812BE5FEEE00A4542E /* Abstracts */,
|
||||
ECA738992BE6018900A4542E /* Buffers */,
|
||||
@@ -768,6 +785,7 @@
|
||||
ECA738A62BE6023F00A4542E /* GridUniforms.swift in Sources */,
|
||||
ECA738D72BE60FC100A4542E /* SolidPointStrokeGenerator.swift in Sources */,
|
||||
EC3565542BEFC6AD00A4E0BF /* View++.swift in Sources */,
|
||||
EC2BEBF62C0F600D005DB0AF /* Box.swift in Sources */,
|
||||
ECA738832BE5FEFE00A4542E /* RenderPass.swift in Sources */,
|
||||
ECEC01A82BEE11BA006DA24C /* QuadShape.swift in Sources */,
|
||||
ECA738862BE5FF2500A4542E /* Canvas.swift in Sources */,
|
||||
@@ -794,6 +812,7 @@
|
||||
EC4538892BEBCAE000A86FEC /* Quad.swift in Sources */,
|
||||
ECE883BD2C00AA170045C53D /* EraserStroke.swift in Sources */,
|
||||
ECA7388F2BE600DA00A4542E /* Grid.metal in Sources */,
|
||||
EC2BEBF42C0F5FF7005DB0AF /* RTree.swift in Sources */,
|
||||
ECA738C92BE60EF700A4542E /* GraphicContext.swift in Sources */,
|
||||
ECA738F62BE612B700A4542E /* MTLDevice++.swift in Sources */,
|
||||
EC0D14212BF79C73009BFE5F /* ToolObject.swift in Sources */,
|
||||
@@ -812,6 +831,7 @@
|
||||
ECA738C12BE60E5300A4542E /* PenStyle.swift in Sources */,
|
||||
ECA738DE2BE610A000A4542E /* ViewPortRenderPass.swift in Sources */,
|
||||
EC7F6BEC2BE5E6E300A34A7B /* MemolaApp.swift in Sources */,
|
||||
EC2BEBF82C0F601A005DB0AF /* Node.swift in Sources */,
|
||||
ECA738A02BE601E400A4542E /* ViewPortVertex.swift in Sources */,
|
||||
EC0D14282BF7BF20009BFE5F /* ContextMenuViewModifier.swift in Sources */,
|
||||
ECA738BC2BE60E0300A4542E /* Tool.swift in Sources */,
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
312
Memola/Canvas/RTree/RTree.swift
Normal file
312
Memola/Canvas/RTree/RTree.swift
Normal file
@@ -0,0 +1,312 @@
|
||||
//
|
||||
// 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()
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user