Skip to content

Commit

Permalink
Merge pull request #3 from li3zhen1/dev
Browse files Browse the repository at this point in the history
implement: CollideForce
  • Loading branch information
li3zhen1 authored Oct 10, 2023
2 parents d9a7d56 + ab33312 commit 20f7b64
Show file tree
Hide file tree
Showing 9 changed files with 262 additions and 33 deletions.
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ A visualization-purposed force simulation library.
![Force Directed Graph](./Assets/ForceDirectedGraph.png)


This is a force directed graph visualizing the data from [Force Directed Graph Component](https://observablehq.com/@d3/force-directed-graph-component), running at 120FPS on a SwiftUI Canvas. Take a closer look at the animation!
This is a force directed graph visualizing the data from [Force Directed Graph Component](https://observablehq.com/@d3/force-directed-graph-component), running at 120FPS on a SwiftUI Canvas. Take a closer look at the animation!

https://github.com/li3zhen1/Grape/assets/45376537/0a494ca0-7b98-44d0-a917-6dcc18e2eeae

Expand All @@ -25,6 +25,11 @@ https://github.com/li3zhen1/Grape/assets/45376537/0a494ca0-7b98-44d0-a917-6dcc18
| LinkForce ||
| ManyBodyForce ||
| CenterForce ||
| CollisionForce | |
| CollideForce | |
| PositionForce | |
| RadialForce | |


#### Perfomance

Currently iterating the example graph 120 times in release build takes 0.046 seconds on a 32GB M1 Max. (77 vertices, 254 edges, link, with manybody, center and link forces)
2 changes: 2 additions & 0 deletions Sources/ForceSimulation/Simulation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@ public class Simulation<N> where N: Identifiable/*, E: EdgeLike, E.VertexID == V
@inlinable public func updateNode(index: [SimulationNode<NodeID>].Index, update: (inout SimulationNode<NodeID>) -> Void) {
update(&simulationNodes[index])
}


}


Expand Down
2 changes: 1 addition & 1 deletion Sources/ForceSimulation/forces/CenterForce.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import QuadTree

public class CenterForce<N> : Force where N : Identifiable {
final public class CenterForce<N> : Force where N : Identifiable {

public var center: Vector2f
public var strength: Float
Expand Down
144 changes: 137 additions & 7 deletions Sources/ForceSimulation/forces/CollideForce.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,85 @@ import QuadTree

enum CollideForceError: Error {
case applyBeforeSimulationInitialized
case maxRadiusCannotBeCalculatedAfterRemoval
}

/// A delegate for finding the maximum radius (of nodes) in a quad.
final class MaxRadiusQuadTreeDelegate<N>: QuadDelegate where N : Identifiable {

typealias Node = N
typealias Property = Float

public var maxNodeRadius: Float

public class CollideForce<N> where N : Identifiable {
var radiusProvider: (N.ID) -> Float

let radius: CollideRadius
let iterationsPerTick: Int
init(
radiusProvider: @escaping(N.ID) -> Float
) {
self.radiusProvider = radiusProvider
self.maxNodeRadius = 0.0
}

internal init(
initialMaxNodeRadius: Float = 0.0,
radiusProvider: @escaping(N.ID) -> Float
) {
self.maxNodeRadius = initialMaxNodeRadius
self.radiusProvider = radiusProvider
}

func didAddNode(_ node: N, at position: Vector2f) {
let p = radiusProvider(node.id)
maxNodeRadius = max(maxNodeRadius, p)
}

func didRemoveNode(_ node: N, at position: Vector2f) {
if radiusProvider(node.id) >= maxNodeRadius {
// 🤯 for Collide force, set to 0 is fine (?)
maxNodeRadius = 0
}
}

func copy() -> Self {
return Self(
initialMaxNodeRadius: self.maxNodeRadius,
radiusProvider: self.radiusProvider
)
}

func createNew() -> Self {
return Self(
radiusProvider: self.radiusProvider
)
}

}


final public class CollideForce<N> where N : Identifiable {
var radius: CollideRadius {
didSet(newValue) {
guard let sim = self.simulation else { return }
calculatedRadius = newValue.calculated(sim.simulationNodes)
}
}
var calculatedRadius: [N.ID: Float] = [:]

var strength: Float

let iterationsPerTick: Int

weak var simulation: Simulation<N>?

internal init(
radius: CollideRadius,
strength: Float = 1.0,
iterationsPerTick: Int = 1
) {
self.radius = radius
self.iterationsPerTick = iterationsPerTick
self.strength = strength
}
}

Expand All @@ -36,7 +96,17 @@ public extension CollideForce {
enum CollideRadius{
case constant(Float)
case varied( (N.ID) -> Float )
case polarCoordinatesOnRad( (Float, N.ID) -> Float )
}
}

public extension CollideForce.CollideRadius {
func calculated<SimNode>(_ nodes: [SimNode]) -> [N.ID: Float] where SimNode: Identifiable, SimNode.ID == N.ID {
switch self {
case .constant(let r):
return Dictionary(uniqueKeysWithValues: nodes.map { ($0.id, r) })
case .varied(let r):
return Dictionary(uniqueKeysWithValues: nodes.map { ($0.id, r($0.id)) })
}
}
}

Expand All @@ -46,9 +116,69 @@ extension CollideForce: Force {
guard let sim = self.simulation else { return }

for _ in 0..<iterationsPerTick {
// guard let quad = try? QuadTree(nodes: sim.simulationNodes.map { ($0, $0.position) }) else { break }

guard let quad = try? QuadTree2(
nodes: sim.simulationNodes.map { ($0, $0.position) },
getQuadDelegate: {
MaxRadiusQuadTreeDelegate() {
switch self.radius {
case .constant(let r):
return r
case .varied(let r):
return r($0)
}
}
}
) else { break }

for i in sim.simulationNodes.indices {
let iNode = sim.simulationNodes[i]
let iNodeId = iNode.id
let iR = self.calculatedRadius[iNodeId]!
let iR2 = iR * iR;
let iPosition = iNode.position + iNode.velocity;

quad.visit { quadNode in

let maxRadiusOfQuad = quadNode.quadDelegate.maxNodeRadius
let deltaR = maxRadiusOfQuad + iR;

if !quadNode.nodes.isEmpty {
for jNodeId in quadNode.nodes.keys {
// is leaf, make sure every collision happens once.
guard let j = sim.nodeIndexLookup[jNodeId], j > i else { continue }
let jR = self.calculatedRadius[jNodeId]!
let jNode = sim.simulationNodes[j]
var deltaPosition = iPosition - (jNode.position + jNode.velocity)
let l = deltaPosition.lengthSquared()

let deltaR = iR + jR;
if l < deltaR * deltaR {

var l = deltaPosition.jiggled().length()
l = (deltaR - l) / l * self.strength;

deltaPosition *= l;
let jR2 = jR*jR

let k = jR2 / (iR2 + jR2);

deltaPosition*=l;

sim.simulationNodes[i].velocity += deltaPosition * k;
sim.simulationNodes[j].velocity -= deltaPosition * (1-k);
}
}
return false
}

return !(
quadNode.quad.x0 > iPosition.x + deltaR
|| quadNode.quad.x1 < iPosition.x - deltaR
|| quadNode.quad.y0 > iPosition.y + deltaR
|| quadNode.quad.y1 < iPosition.y - deltaR
);
}
}
}
}

}
2 changes: 1 addition & 1 deletion Sources/ForceSimulation/forces/LinkForce.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ enum LinkForceError: Error {
case useBeforeSimulationInitialized
}

public class LinkForce<N> : Force where N : Identifiable {
final public class LinkForce<N> : Force where N : Identifiable {

public class LinkLookup {
public let source: [N.ID: [N.ID]]
Expand Down
Loading

0 comments on commit 20f7b64

Please sign in to comment.