diff --git a/Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/ContentView.swift b/Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/ContentView.swift index 799a2a4..2e0f9cf 100644 --- a/Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/ContentView.swift +++ b/Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/ContentView.swift @@ -13,17 +13,11 @@ import CoreGraphics let colors: [GraphicsContext.Shading] = [ GraphicsContext.Shading.color(red: 17.0/255, green: 181.0/255, blue: 174.0/255), - GraphicsContext.Shading.color(red: 64.0/255, green: 70.0/255, blue: 201.0/255), - GraphicsContext.Shading.color(red: 246.0/255, green: 133.0/255, blue: 18.0/255), - GraphicsContext.Shading.color(red: 222.0/255, green: 60.0/255, blue: 130.0/255), - GraphicsContext.Shading.color(red: 17.0/255, green: 181.0/255, blue: 174.0/255), - GraphicsContext.Shading.color(red: 114.0/255, green: 224.0/255, blue: 106.0/255), - GraphicsContext.Shading.color(red: 22.0/255, green: 124.0/255, blue: 243.0/255), GraphicsContext.Shading.color(red: 115.0/255, green: 38.0/255, blue: 211.0/255), GraphicsContext.Shading.color(red: 232.0/255, green: 198.0/255, blue: 0.0/255), @@ -39,39 +33,43 @@ struct MiserableNode: Identifiable { } -typealias MiserableSimulation = Simulation -typealias MiserableLinkForce = LinkForce - struct ContentView: View { @State var points: [Vector2d] = [] - var sim: MiserableSimulation + var sim: Simulation2D let data: Miserable - var linkForce: MiserableLinkForce + var linkForce: LinkForce init() { + + self.data = getData(miserables) - self.sim = Simulation(nodeIds: data.nodes.map {$0.id}, alphaDecay: 0.01) + self.sim = Simulation2D(nodeIds: data.nodes.map {$0.id}, alphaDecay: 0.01) + sim.createManyBodyForce(strength: -12) self.linkForce = sim.createLinkForce( data.links.map { l in (l.source, l.target) }, stiffness: .weightedByDegree { _, _ in 1.0 }, originalLength: .constant(35) ) - sim.createCenterForce(center: Vector2d(0, 0), strength: 0.4) + sim.createCenterForce(center: [0, 0], strength: 0.4) sim.createCollideForce(radius: .constant(3)) } var body: some View { NavigationStack { + + /// This is only an example. You probably don't want to handle such large data structures on a SwiftUI Canvas. Canvas { context, sz in + + /// Drawing lines for l in self.data.links { if let s = self.data.nodes.firstIndex(where: { $0.id == l.source}), let t = self.data.nodes.firstIndex(where: { $0.id == l.target}) { - // draw a line from s to t + /// draw a line from s to t let x1 = CGFloat( 300.0 + self.sim.nodePositions[s].x ) let y1 = CGFloat( 200.0 - self.sim.nodePositions[s].y ) let x2 = CGFloat( 300.0 + self.sim.nodePositions[t].x ) @@ -84,6 +82,8 @@ struct ContentView: View { } } + + /// Drawing points for i in self.points.indices { let x = 300.0 + points[i].x - 4.0 @@ -105,10 +105,17 @@ struct ContentView: View { }.toolbar { Button(action: { - Timer.scheduledTimer(withTimeInterval: 1/60, repeats: true) { t in - sim.tick() + + /// Note that currently `Simulation` is not aware of time. It just ticks 120 times and so the points will be moving fast. + Timer.scheduledTimer(withTimeInterval: 1/120, repeats: true) { t in + + /// This is a CPU-bound task. Try to move it to other places. + self.sim.tick() self.points = sim.nodePositions + } + + }, label: { HStack { Image(systemName: "play.fill") diff --git a/Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/ForceDirectedLatticeView.swift b/Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/ForceDirectedLatticeView.swift new file mode 100644 index 0000000..527c14f --- /dev/null +++ b/Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/ForceDirectedLatticeView.swift @@ -0,0 +1,18 @@ +// +// ForceDirectedLatticeView.swift +// ForceDirectedGraphExample +// +// Created by li3zhen1 on 10/18/23. +// + +import SwiftUI + +struct ForceDirectedLatticeView: View { + var body: some View { + Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) + } +} + +#Preview { + ForceDirectedLatticeView() +} diff --git a/README.md b/README.md index 0efafdf..f15ca69 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,14 @@ sim.createLinkForce(links) sim.createCenterForce(center: Vector2d(0, 0), strength: 0.4) sim.createCollideForce(radius: .constant(3)) +/// Force is ready to start! run `tick` to iterate the simulation. + +for i in 0..<120 { + sim.tick() + let positions = sim.nodePositions + /// Do something with the positions. +} + ``` See [Example](https://github.com/li3zhen1/Grape/tree/main/Examples/ForceDirectedGraphExample) for more details. @@ -122,7 +130,7 @@ Also, this is how you create a 4D simulation. (Though I don't know what good it Grape uses simd to calculate position and velocity. Currently it takes ~0.12 seconds to iterate 120 times over the example graph(2D). (77 vertices, 254 edges, with manybody, center, collide and link forces. Release build on a M1 Max) -Due to the iteration over simd lanes, going 3D will hurt performance. (~0.19 seconds for the same graph and same configs.) +Due to the iteration over simd lanes, going 3D will hurt performance. (~0.16 seconds for the same graph and same configs.)
diff --git a/Sources/ForceSimulation/Simulation.swift b/Sources/ForceSimulation/Simulation.swift index 2b89cec..5443b89 100644 --- a/Sources/ForceSimulation/Simulation.swift +++ b/Sources/ForceSimulation/Simulation.swift @@ -18,20 +18,31 @@ public final class Simulation where NodeID: Hashable, V: VectorLike, /// Usually this is `Double` if you are on Apple platforms. public typealias Scalar = V.Scalar - public let initializedAlpha: Double public var alpha: Double public var alphaMin: Double public var alphaDecay: Double public var alphaTarget: Double + public var velocityDecay: V.Scalar public internal(set) var forces: [any ForceLike] = [] - + /// The position of points stored in simulation. + /// Ordered as the nodeIds you passed in when initializing simulation. + /// They are always updated. public internal(set) var nodePositions: [V] + + /// The velocities of points stored in simulation. + /// Ordered as the nodeIds you passed in when initializing simulation. + /// They are always updated. public internal(set) var nodeVelocities: [V] + + + /// The fixed positions of points stored in simulation. + /// Ordered as the nodeIds you passed in when initializing simulation. + /// They are always updated. public internal(set) var nodeFixations: [V?] public private(set) var nodeIds: [NodeID] @@ -41,11 +52,11 @@ public final class Simulation where NodeID: Hashable, V: VectorLike, /// Create a new simulation. /// - Parameters: /// - nodeIds: Hashable identifiers for the nodes. Force simulation calculate them by order once created. - /// - alpha: - /// - alphaMin: + /// - alpha: + /// - alphaMin: /// - alphaDecay: The larger the value, the faster the simulation converges to the final result. - /// - alphaTarget: - /// - velocityDecay: + /// - alphaTarget: + /// - velocityDecay: /// - getInitialPosition: The closure to set the initial position of the node. If not provided, the initial position is set to zero. public init( nodeIds: [NodeID], @@ -78,8 +89,7 @@ public final class Simulation where NodeID: Hashable, V: VectorLike, self.nodeVelocities = Array(repeating: .zero, count: nodeIds.count) self.nodeFixations = Array(repeating: nil, count: nodeIds.count) - - + self.nodeIdToIndexLookup.reserveCapacity(nodeIds.count) for i in nodeIds.indices { self.nodeIdToIndexLookup[nodeIds[i]] = i @@ -88,11 +98,20 @@ public final class Simulation where NodeID: Hashable, V: VectorLike, } - @inlinable internal func getIndex(of nodeId: NodeID) -> Int { + /// Get the index in the nodeArray for `nodeId` + /// - **Complexity**: O(1) + public func getIndex(of nodeId: NodeID) -> Int { return nodeIdToIndexLookup[nodeId]! } + + /// Reset the alpha. The points will move faster as alpha gets larger. + public func resetAlpha(_ alpha: Double) { + self.alpha = alpha + } /// Run the simulation for a number of iterations. + /// Goes through all the forces created. + /// The forces will call `apply` then the positions and velocities will be modified. /// - Parameter iterationCount: Default to 1. public func tick(iterationCount: UInt = 1) { for _ in 0.. where NodeID: Hashable, V: VectorLike, } } - #if canImport(simd) -public typealias Simulation2D = Simulation where NodeID: Hashable + public typealias Simulation2D = Simulation where NodeID: Hashable -public typealias Simulation3D = Simulation where NodeID: Hashable + public typealias Simulation3D = Simulation where NodeID: Hashable #endif diff --git a/Sources/ForceSimulation/Utils.swift b/Sources/ForceSimulation/Utils.swift index 83e6314..12f2b91 100644 --- a/Sources/ForceSimulation/Utils.swift +++ b/Sources/ForceSimulation/Utils.swift @@ -48,13 +48,10 @@ extension VectorLike where Scalar == Double { } } -extension Vector2d { - @inlinable public func jiggled() -> Self { - return Vector2d(x.jiggled(), y.jiggled()) - } -} -/// A Hashable identifier for an edge. + +/// A Hashable identifier for an edge. It’s a utility type for preserving the +/// `Hashable` conformance. public struct EdgeID: Hashable where NodeID: Hashable { public let source: NodeID public let target: NodeID @@ -65,6 +62,7 @@ public struct EdgeID: Hashable where NodeID: Hashable { } } + public protocol PrecalculatableNodeProperty { associatedtype NodeID: Hashable associatedtype V: VectorLike where V.Scalar == Double diff --git a/Sources/ForceSimulation/forces/CollideForce.swift b/Sources/ForceSimulation/forces/CollideForce.swift index 0977693..104fefa 100644 --- a/Sources/ForceSimulation/forces/CollideForce.swift +++ b/Sources/ForceSimulation/forces/CollideForce.swift @@ -40,7 +40,6 @@ struct MaxRadiusTreeDelegate: NDTreeDelegate where NodeID: Hashable, } - /// A force that prevents nodes from overlapping. public final class CollideForce: ForceLike where NodeID: Hashable, V: VectorLike, V.Scalar == Double { @@ -76,10 +75,9 @@ where NodeID: Hashable, V: VectorLike, V.Scalar == Double { guard let sim = self.simulation else { return } for _ in 0...cover(of: sim.nodePositions) - - + let tree = NDTree>( box: coveringBox, clusterDistance: 1e-5 ) { @@ -92,7 +90,7 @@ where NodeID: Hashable, V: VectorLike, V.Scalar == Double { } } } - + for i in sim.nodePositions.indices { tree.add(i, at: sim.nodePositions[i]) } @@ -111,7 +109,7 @@ where NodeID: Hashable, V: VectorLike, V.Scalar == Double { if t.nodePosition != nil { for j in t.nodeIndices { -// print("\(i)<=>\(j)") + // print("\(i)<=>\(j)") // is leaf, make sure every collision happens once. guard j > i else { continue } diff --git a/Sources/ForceSimulation/forces/DirectionForce.swift b/Sources/ForceSimulation/forces/DirectionForce.swift index 4d2c330..86e5ed1 100644 --- a/Sources/ForceSimulation/forces/DirectionForce.swift +++ b/Sources/ForceSimulation/forces/DirectionForce.swift @@ -12,29 +12,31 @@ final public class DirectionForce: ForceLike where NodeID: Hashable, V: VectorLike, V.Scalar == Double { public enum Direction { - case x - case y + case x + case y case entryOfVector(Int) } public enum Strength { case constant(Double) - case varied( (NodeID) -> Double ) + case varied((NodeID) -> Double) } public enum TargetOnDirection { case constant(V.Scalar) - case varied( (NodeID) -> V.Scalar ) + case varied((NodeID) -> V.Scalar) } - + public var strength: Strength public var direction: Int public var calculatedStrength: [Double] = [] public var targetOnDirection: TargetOnDirection public var calculatedTargetOnDirection: [V.Scalar] = [] - internal init(direction: Direction, targetOnDirection: TargetOnDirection, strength: Strength = .constant(1.0)) { - - + internal init( + direction: Direction, targetOnDirection: TargetOnDirection, + strength: Strength = .constant(1.0) + ) { + self.strength = strength self.direction = direction.lane self.targetOnDirection = targetOnDirection @@ -50,17 +52,17 @@ where NodeID: Hashable, V: VectorLike, V.Scalar == Double { public func apply(alpha: Double) { guard let sim = self.simulation else { return } - let lane = self.direction + let lane = self.direction for i in sim.nodePositions.indices { - sim.nodeVelocities[i][lane] += ( - self.calculatedTargetOnDirection[i] - sim.nodePositions[i][lane] - ) * self.calculatedStrength[i] * alpha + sim.nodeVelocities[i][lane] += + (self.calculatedTargetOnDirection[i] - sim.nodePositions[i][lane]) + * self.calculatedStrength[i] * alpha } } } extension DirectionForce.Strength: PrecalculatableNodeProperty { - public func calculated(for simulation: Simulation) -> [Double] { + public func calculated(for simulation: Simulation) -> [Double] { switch self { case .constant(let value): return Array(repeating: value, count: simulation.nodeIds.count) @@ -71,7 +73,7 @@ extension DirectionForce.Strength: PrecalculatableNodeProperty { } extension DirectionForce.TargetOnDirection: PrecalculatableNodeProperty { - public func calculated(for simulation: Simulation) -> [Double] { + public func calculated(for simulation: Simulation) -> [Double] { switch self { case .constant(let value): return Array(repeating: value, count: simulation.nodeIds.count) @@ -81,9 +83,8 @@ extension DirectionForce.TargetOnDirection: PrecalculatableNodeProperty { } } - extension DirectionForce.Direction { - @inlinable var lane: Int { + @inlinable var lane: Int { switch self { case .x: return 0 case .y: return 1 @@ -92,11 +93,11 @@ extension DirectionForce.Direction { } } -public extension Simulation { +extension Simulation { /// Create a direction force, Similar to https://d3js.org/d3-force/position @discardableResult - func createPositionForce( + public func createPositionForce( direction: DirectionForce.Direction, targetOnDirection: DirectionForce.TargetOnDirection, strength: DirectionForce.Strength = .constant(1.0) @@ -111,4 +112,3 @@ public extension Simulation { return force } } - diff --git a/Sources/ForceSimulation/forces/LinkForce.swift b/Sources/ForceSimulation/forces/LinkForce.swift index ebe89d6..0374d2a 100644 --- a/Sources/ForceSimulation/forces/LinkForce.swift +++ b/Sources/ForceSimulation/forces/LinkForce.swift @@ -11,15 +11,15 @@ enum LinkForceError: Error { case useBeforeSimulationInitialized } -/// A force that represents links between nodes. +/// A force that represents links between nodes. final public class LinkForce: ForceLike where NodeID: Hashable, V: VectorLike, V.Scalar == Double { /// public enum LinkStiffness { case constant(Double) - case varied( (EdgeID, LinkLookup) -> Double ) - case weightedByDegree( k: (EdgeID, LinkLookup) -> Double ) + case varied((EdgeID, LinkLookup) -> Double) + case weightedByDegree(k: (EdgeID, LinkLookup) -> Double) } var linkStiffness: LinkStiffness var calculatedStiffness: [Double] = [] @@ -94,11 +94,9 @@ where NodeID: Hashable, V: VectorLike, V.Scalar == Double { public func apply(alpha: Double) { guard let sim = self.simulation else { return } - - for _ in 0.. [Double] } - - extension LinkForce.LinkLength: PrecalculatableEdgeProperty { func calculated( for links: [EdgeID], connectionLookupTable: LinkForce.LinkLookup @@ -174,8 +169,6 @@ extension LinkForce.LinkLength: PrecalculatableEdgeProperty { } } - - extension LinkForce.LinkStiffness: PrecalculatableEdgeProperty { func calculated( for links: [EdgeID], @@ -190,7 +183,8 @@ extension LinkForce.LinkStiffness: PrecalculatableEdgeProperty { } case .weightedByDegree(let k): return links.map { link in - k(link, lookup) / Double( + k(link, lookup) + / Double( min( lookup.count[link.source, default: 0], lookup.count[link.target, default: 0] @@ -201,11 +195,8 @@ extension LinkForce.LinkStiffness: PrecalculatableEdgeProperty { } } - - extension Simulation { - /// Create a link force, Similar to https://d3js.org/d3-force/link @discardableResult public func createLinkForce( diff --git a/Sources/ForceSimulation/forces/ManyBodyForce.swift b/Sources/ForceSimulation/forces/ManyBodyForce.swift index c277200..5f4fe2c 100644 --- a/Sources/ForceSimulation/forces/ManyBodyForce.swift +++ b/Sources/ForceSimulation/forces/ManyBodyForce.swift @@ -11,7 +11,6 @@ enum ManyBodyForceError: Error { case buildQuadTreeBeforeSimulationInitialized } - struct MassQuadtreeDelegate: NDTreeDelegate where NodeID: Hashable, V: VectorLike { public var accumulatedMass: Double = .zero @@ -83,7 +82,7 @@ where NodeID: Hashable, V: VectorLike, V.Scalar == Double { public enum NodeMass { case constant(Double) - case varied( (NodeID) -> Double ) + case varied((NodeID) -> Double) } var mass: NodeMass = .constant(1.0) var precalculatedMass: [Double] = [] @@ -122,9 +121,8 @@ where NodeID: Hashable, V: VectorLike, V.Scalar == Double { var forces: [V] = [] public func apply(alpha: Double) { guard let simulation else { return } -// guard - try! calculateForce(alpha: alpha) //else { return } - + // guard + try! calculateForce(alpha: alpha) //else { return } for i in simulation.nodeVelocities.indices { simulation.nodeVelocities[i] += self.forces[i] / precalculatedMass[i] @@ -180,7 +178,7 @@ where NodeID: Hashable, V: VectorLike, V.Scalar == Double { } -// var forces = [V](repeating: .zero, count: sim.nodePositions.count) + // var forces = [V](repeating: .zero, count: sim.nodePositions.count) for i in sim.nodePositions.indices { var f = V.zero @@ -236,50 +234,50 @@ where NodeID: Hashable, V: VectorLike, V.Scalar == Double { // } guard t.delegate.accumulatedCount > 0 else { return false } - let centroid = t.delegate.accumulatedMassWeightedPositions / t.delegate.accumulatedMass + let centroid = + t.delegate.accumulatedMassWeightedPositions / t.delegate.accumulatedMass let vec = centroid - sim.nodePositions[i] let boxWidth = (t.box.p1 - t.box.p0)[0] var distanceSquared = vec.jiggled().lengthSquared() - + let farEnough: Bool = (distanceSquared * self.theta2) > (boxWidth * boxWidth) - -// let distance = distanceSquared.squareRoot() - + + // let distance = distanceSquared.squareRoot() + if distanceSquared < self.distanceMin2 { distanceSquared = (self.distanceMin2 * distanceSquared).squareRoot() } if farEnough { - + guard distanceSquared < self.distanceMax2 else { return true } - + /// Workaround for "The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions" let k: Double = - self.strength * alpha * t.delegate.accumulatedMass / distanceSquared // distanceSquared.squareRoot() - + self.strength * alpha * t.delegate.accumulatedMass / distanceSquared // distanceSquared.squareRoot() + f += vec * k return false - + } else if t.children != nil { return true } - if t.isFilledLeaf { - -// for j in t.nodeIndices { -// if j != i { -// let k: Double = -// self.strength * alpha * self.precalculatedMass[j] / distanceSquared / distanceSquared.squareRoot() -// f += vec * k -// } -// } - if t.nodeIndices.contains(i) {return false} - + + // for j in t.nodeIndices { + // if j != i { + // let k: Double = + // self.strength * alpha * self.precalculatedMass[j] / distanceSquared / distanceSquared.squareRoot() + // f += vec * k + // } + // } + if t.nodeIndices.contains(i) { return false } + let massAcc = t.delegate.accumulatedMass -// t.nodeIndices.contains(i) ? (t.delegate.accumulatedMass-self.precalculatedMass[i]) : (t.delegate.accumulatedMass) - let k: Double = self.strength * alpha * massAcc / distanceSquared // distanceSquared.squareRoot() + // t.nodeIndices.contains(i) ? (t.delegate.accumulatedMass-self.precalculatedMass[i]) : (t.delegate.accumulatedMass) + let k: Double = self.strength * alpha * massAcc / distanceSquared // distanceSquared.squareRoot() f += vec * k return false } else { @@ -288,7 +286,7 @@ where NodeID: Hashable, V: VectorLike, V.Scalar == Double { } forces[i] = f } -// return forces + // return forces } } diff --git a/Sources/ForceSimulation/forces/RadialForce.swift b/Sources/ForceSimulation/forces/RadialForce.swift index b0a11ac..01fce1f 100644 --- a/Sources/ForceSimulation/forces/RadialForce.swift +++ b/Sources/ForceSimulation/forces/RadialForce.swift @@ -23,8 +23,8 @@ where NodeID: Hashable, V: VectorLike, V.Scalar == Double { /// Radius accessor public enum NodeRadius { - case constant( V.Scalar ) - case varied( (NodeID) -> V.Scalar ) + case constant(V.Scalar) + case varied((NodeID) -> V.Scalar) } public var radius: NodeRadius private var calculatedRadius: [V.Scalar] = [] @@ -32,14 +32,11 @@ where NodeID: Hashable, V: VectorLike, V.Scalar == Double { /// Strength accessor public enum Strength { case constant(Double) - case varied( (NodeID) -> Double ) + case varied((NodeID) -> Double) } public var strength: Strength private var calculatedStrength: [Double] = [] - - - public init(center: V, radius: NodeRadius, strength: Strength) { self.center = center self.radius = radius @@ -88,7 +85,7 @@ extension Simulation { /// Create a radial force, Similar to https://d3js.org/d3-force/position @discardableResult public func createRadialForce( - center: V = .zero, + center: V = .zero, radius: RadialForce.NodeRadius, strength: RadialForce.Strength = .constant(0.1) ) -> RadialForce { diff --git a/Sources/NDTree/NDTree.swift b/Sources/NDTree/NDTree.swift index 6d59aad..f04b6cb 100644 --- a/Sources/NDTree/NDTree.swift +++ b/Sources/NDTree/NDTree.swift @@ -1,6 +1,6 @@ // // NDTree.swift -// +// // // Created by li3zhen1 on 10/14/23. // @@ -12,8 +12,15 @@ public protocol NDTreeDelegate { associatedtype NodeID: Hashable associatedtype V: VectorLike + /// Called when a node is added on a node, regardless of whether the node is internal or leaf. + /// If you add `n` points to the root, this method will be called `n` times in the root delegate, + /// although it is probably not containing points now. + /// - Parameters: + /// - node: The nodeID of the node that is added. + /// - position: The position of the node that is added. mutating func didAddNode(_ node: NodeID, at position: V) + /// Called when a node is removed on a node, regardless of whether the node is internal or leaf. mutating func didRemoveNode(_ node: NodeID, at position: V) /// Copy object. This method is called when the root box is not large enough to cover the new nodes. @@ -30,26 +37,25 @@ public protocol NDTreeDelegate { /// - Note: `NDTree` is a generic type that can be used in any dimension. /// `NDTree` is a reference type. public final class NDTree where V: VectorLike, D: NDTreeDelegate, D.V == V { - + public typealias NodeIndex = D.NodeID - + public typealias Direction = Int - + public typealias Box = NDBox - + public private(set) var box: Box - + public private(set) var children: [NDTree]? - + public private(set) var nodePosition: V? public private(set) var nodeIndices: [NodeIndex] - + public let clusterDistance: V.Scalar private let clusterDistanceSquared: V.Scalar - - + public private(set) var delegate: D - + private init( box: Box, clusterDistance: V.Scalar, @@ -62,7 +68,6 @@ public final class NDTree where V: VectorLike, D: NDTreeDelegate, D.V == V self.delegate = parentDelegate.spawn() } - public init( box: Box, clusterDistance: V.Scalar, @@ -74,187 +79,174 @@ public final class NDTree where V: VectorLike, D: NDTreeDelegate, D.V == V self.nodeIndices = [] self.delegate = buildRootDelegate() } - + public convenience init( covering nodes: [NodeIndex: V], clusterDistance: V.Scalar, buildRootDelegate: () -> D ) { let coveringBox = Box.cover(of: Array(nodes.values)) - self.init(box: coveringBox, - clusterDistance: clusterDistance, - buildRootDelegate: buildRootDelegate) + self.init( + box: coveringBox, + clusterDistance: clusterDistance, + buildRootDelegate: buildRootDelegate) for (i, p) in nodes { add(i, at: p) } } - - + public func add(_ nodeIndex: NodeIndex, at point: V) { cover(point) - addWithoutCover(nodeIndex, at: point) - } - + private func addWithoutCover(_ nodeIndex: NodeIndex, at point: V) { defer { delegate.didAddNode(nodeIndex, at: point) } - + guard let children = self.children else { if nodePosition == nil { nodeIndices.append(nodeIndex) nodePosition = point return - } - else if nodePosition == point || nodePosition!.distanceSquared(to: point) < clusterDistanceSquared { + } else if nodePosition == point + || nodePosition!.distanceSquared(to: point) < clusterDistanceSquared + { nodeIndices.append(nodeIndex) return - } - else { - + } else { + let spawned = Self.spawnChildren( box, -// V.directionCount, + // Self.directionCount, clusterDistance, /*&*/delegate ) - + if let nodePosition { let direction = getIndexInChildren(nodePosition, relativeTo: box.center) spawned[direction].nodeIndices = self.nodeIndices spawned[direction].nodePosition = self.nodePosition spawned[direction].delegate = self.delegate.copy() -// self.delegate = self.delegate.copy() - - - -// for ni in nodeIndices { -// delegate.didAddNode(ni, at: nodePosition) -// } - + // self.delegate = self.delegate.copy() + + // for ni in nodeIndices { + // delegate.didAddNode(ni, at: nodePosition) + // } + self.nodeIndices = [] self.nodePosition = nil } - + let directionOfNewNode = getIndexInChildren(point, relativeTo: box.center) spawned[directionOfNewNode].addWithoutCover(nodeIndex, at: point) - + self.children = spawned return } } - + let directionOfNewNode = getIndexInChildren(point, relativeTo: box.center) children[directionOfNewNode].addWithoutCover(nodeIndex, at: point) - + return } - + + /// Expand the current node multiple times by calling `expand(towards:)`, until the point is covered. + /// - Parameter point: The point to be covered. private func cover(_ point: V) { if box.contains(point) { return } - + repeat { let direction = getIndexInChildren(point, relativeTo: box.p0) expand(towards: direction) } while !box.contains(point) } - - + + /// Expand the current node towards a direction. The expansion + /// will double the size on each dimension. Then the data in delegate will be copied to the new children. + /// - Parameter direction: An Integer between 0 and `directionCount - 1`, where `directionCount` equals to 2^(dimension of the vector). private func expand(towards direction: Direction) { - let nailedDirection = (V.directionCount - 1) - direction + let nailedDirection = (Self.directionCount - 1) - direction let nailedCorner = box.getCorner(of: nailedDirection) - + let _corner = box.getCorner(of: direction) - let expandedCorner = (_corner+_corner) - nailedCorner - + let expandedCorner = (_corner + _corner) - nailedCorner + let newRootBox = Box(nailedCorner, expandedCorner) - + let copiedCurrentNode = shallowCopy() var spawned = Self.spawnChildren( newRootBox, -// V.directionCount, + // Self.directionCount, clusterDistance, /*&*/delegate ) - + spawned[nailedDirection] = copiedCurrentNode - + self.box = newRootBox self.children = spawned self.nodeIndices = [] self.delegate = delegate.copy() } - + + /// The children count of a node in NDTree. + /// Should be equal to the 2^(dimension of the vector). + /// For example, a 2D vector should have 4 children, a 3D vector should have 8 children. + /// This property is a getter property but it is probably be inlined. + @inlinable static var directionCount: Int { 1 << V.scalarCount } + private static func spawnChildren( _ _box: Box, _ _clusterDistance: V.Scalar, _ _delegate: D ) -> [NDTree] { - - -// var spawned = Array(repeating: _box, count: _directionCount) -// -// -// -// let center = _box.center -// -// for j in spawned.indices { -// for i in 0..> i) & 0b1 -// -// // TODO: use simd mask -// if isOnTheHigherRange != 0 { -// spawned[j].p0[i] = center[i] -// } else { -// spawned[j].p1[i] = center[i] -// } -// } -// } -// var result = [NDTree]() -// result.reserveCapacity(_directionCount) -// for b in spawned { -// result.append(NDTree(box: b, clusterDistance: _clusterDistance, parentDelegate: /*&*/_delegate)) -// } - + var result = [NDTree]() - result.reserveCapacity(V.directionCount) + result.reserveCapacity(Self.directionCount) let center = _box.center - - for j in 0..> i) & 0b1 - - // TODO: use simd mask - if isOnTheHigherRange != 0 { - __box.p0[i] = center[i] - } else { - __box.p1[i] = center[i] - } - } - result.append(NDTree(box: __box, clusterDistance: _clusterDistance, parentDelegate: /*&*/_delegate)) + + for j in 0..> i) & 0b1 + + // TODO: use simd mask + if isOnTheHigherRange != 0 { + __box.p0[i] = center[i] + } else { + __box.p1[i] = center[i] + } + } + result.append( + NDTree( + box: __box, clusterDistance: _clusterDistance, parentDelegate: /*&*/ _delegate) + ) } - + return result } - - /// Copy object while holding the same reference to children + + /// Copy object while holding the same reference to children. + /// Consider this function something you would do when working with linked list. private func shallowCopy() -> NDTree { - let copy = NDTree(box: box, clusterDistance: clusterDistance, parentDelegate: /*&*/delegate) - + let copy = NDTree( + box: box, clusterDistance: clusterDistance, parentDelegate: /*&*/ delegate) + copy.nodeIndices = nodeIndices copy.nodePosition = nodePosition copy.children = children copy.delegate = delegate - + return copy } - - + + /// Get the index of the child that contains the point. + /// **Complexity**: O(n*(2^n)), where n is the dimension of the vector. private func getIndexInChildren(_ point: V, relativeTo originalPoint: V) -> Int { var index = 0 for i in 0.. where V: VectorLike, D: NDTreeDelegate, D.V == V } extension NDTree where D.NodeID == Int { + + /// Initialize a NDTree with a list of points and a key path to the vector. + /// - Parameters: + /// - points: A list of points. The points are only used to calculate the covering box. You should still call `add` to add the points to the tree. + /// - clusterDistance: If 2 points are close enough, they will be clustered into the same leaf node. + /// - buildRootDelegate: A closure that tells the tree how to initialize the data you want to store in the root. + /// The closure is called only once. The `NDTreeDelegate` will then be created in children tree nods by calling `spawn` on the root delegate. public convenience init( covering points: [V], clusterDistance: V.Scalar, buildRootDelegate: () -> D ) { let coveringBox = Box.cover(of: points) - self.init(box: coveringBox, clusterDistance: clusterDistance, buildRootDelegate: buildRootDelegate) + self.init( + box: coveringBox, clusterDistance: clusterDistance, buildRootDelegate: buildRootDelegate + ) for i in points.indices { add(i, at: points[i]) } } - + + /// Initialize a NDTree with a list of points and a key path to the vector. + /// - Parameters: + /// - points: A list of points. The points are only used to calculate the covering box. You should still call `add` to add the points to the tree. + /// - keyPath: A key path to the vector in the element of the list. + /// - clusterDistance: If 2 points are close enough, they will be clustered into the same leaf node. + /// - buildRootDelegate: A closure that tells the tree how to initialize the data you want to store in the root. + /// The closure is called only once. The `NDTreeDelegate` will then be created in children tree nods by calling `spawn` on the root delegate. public convenience init( covering points: [T], keyPath: KeyPath, @@ -287,22 +295,30 @@ extension NDTree where D.NodeID == Int { buildRootDelegate: () -> D ) { let coveringBox = Box.cover(of: points, keyPath: keyPath) - self.init(box: coveringBox, clusterDistance: clusterDistance, buildRootDelegate: buildRootDelegate) + self.init( + box: coveringBox, clusterDistance: clusterDistance, buildRootDelegate: buildRootDelegate + ) for i in points.indices { add(i, at: points[i][keyPath: keyPath]) } } } - - extension NDTree { + + /// The bounding box of the current node @inlinable public var extent: Box { box } - + + /// Returns true is the current tree node is leaf. Does not guarantee that the tree node has point in it. @inlinable public var isLeaf: Bool { children == nil } + + /// Returns true is the current tree node is internal. Internal tree node are always empty and do not contain any points. @inlinable public var isInternalNode: Bool { children != nil } - + + /// Returns true is the current tree node is leaf and has point in it. @inlinable public var isFilledLeaf: Bool { nodePosition != nil } + + /// Returns true is the current tree node is leaf and does not have point in it. @inlinable public var isEmptyLeaf: Bool { nodePosition == nil } } diff --git a/Sources/NDTree/Quadtree+Octree.swift b/Sources/NDTree/Quadtree+Octree.swift index da2f625..1585d22 100644 --- a/Sources/NDTree/Quadtree+Octree.swift +++ b/Sources/NDTree/Quadtree+Octree.swift @@ -27,7 +27,7 @@ } - public static let directionCount = 4 + // public static let directionCount = 4 } extension simd_double3: VectorLike { @@ -48,7 +48,7 @@ return (self - to).length() } - public static let directionCount = 8 + // public static let directionCount = 8 } diff --git a/Sources/NDTree/VectorLike.swift b/Sources/NDTree/VectorLike.swift index eb6a9c7..be3a28b 100644 --- a/Sources/NDTree/VectorLike.swift +++ b/Sources/NDTree/VectorLike.swift @@ -13,12 +13,6 @@ /// that do not support `simd`. public protocol VectorLike: CustomStringConvertible, Decodable, Encodable, ExpressibleByArrayLiteral, Hashable { - /// The children count of a node in NDTree. - /// Should be equal to the 2^(dimension of the vector). - /// For example, a 2D vector should have 4 children, a 3D vector should have 8 children. - /// This property should be implemented even if you are using `simd`. - static var directionCount: Int { get } - associatedtype Scalar: FloatingPoint, Decodable, Encodable, Hashable, CustomDebugStringConvertible diff --git a/docs/index/availability.index b/docs/index/availability.index new file mode 100644 index 0000000..05be8ee Binary files /dev/null and b/docs/index/availability.index differ diff --git a/docs/index/data.mdb b/docs/index/data.mdb new file mode 100755 index 0000000..861d19e Binary files /dev/null and b/docs/index/data.mdb differ diff --git a/docs/index/navigator.index b/docs/index/navigator.index new file mode 100644 index 0000000..b318276 Binary files /dev/null and b/docs/index/navigator.index differ