From 9ca8cd8fdde2cc0e9439fa83bc2e0803fad7c53f Mon Sep 17 00:00:00 2001
From: li3zhen1
Date: Sun, 5 Nov 2023 10:50:14 -0500
Subject: [PATCH 1/7] Add skeleton
---
Package.swift | 31 +++--
Sources/Grape/ForceDirectedGraph.swift | 80 +++++++++++++
Sources/Grape/ForceDirectedGraphContent.swift | 49 ++++++++
Sources/Grape/GraphContent.swift | 1 +
Sources/Grape/GraphContentBuilder.swift | 110 ++++++++++++++++++
Sources/Grape/LinkMark.swift | 78 +++++++++++++
Sources/Grape/NodeMark.swift | 52 +++++++++
7 files changed, 391 insertions(+), 10 deletions(-)
create mode 100644 Sources/Grape/ForceDirectedGraph.swift
create mode 100644 Sources/Grape/ForceDirectedGraphContent.swift
create mode 100644 Sources/Grape/GraphContent.swift
create mode 100644 Sources/Grape/GraphContentBuilder.swift
create mode 100644 Sources/Grape/LinkMark.swift
create mode 100644 Sources/Grape/NodeMark.swift
diff --git a/Package.swift b/Package.swift
index ba8781b..546fc34 100644
--- a/Package.swift
+++ b/Package.swift
@@ -24,6 +24,11 @@ let package = Package(
targets: ["ForceSimulation"]
),
+ .library(
+ name: "Grape",
+ targets: ["Grape"]
+ ),
+
],
dependencies: [
@@ -33,6 +38,12 @@ let package = Package(
targets: [
+ .target(
+ name: "Grape",
+ dependencies: ["ForceSimulation"],
+ path: "Sources/Grape"
+ ),
+
// .target(
// name: "NDTree",
// path: "Sources/NDTree",
@@ -43,7 +54,7 @@ let package = Package(
// // "-whole-module-optimization",
// // "-Ounchecked",
// ]),
-
+
// ]
// ),
@@ -62,14 +73,14 @@ let package = Package(
name: "ForceSimulation",
// dependencies: ["NDTree"],
path: "Sources/ForceSimulation"
- // ,
- // swiftSettings: [
- // .unsafeFlags([
- // "-cross-module-optimization",
- // // "-whole-module-optimization",
- // // "-Ounchecked",
- // ])
- // ]
+ // ,
+ // swiftSettings: [
+ // .unsafeFlags([
+ // "-cross-module-optimization",
+ // // "-whole-module-optimization",
+ // // "-Ounchecked",
+ // ])
+ // ]
),
.testTarget(
@@ -83,6 +94,6 @@ let package = Package(
// // "-Ounchecked",
// ])
// ]
- ),
+ ),
]
)
diff --git a/Sources/Grape/ForceDirectedGraph.swift b/Sources/Grape/ForceDirectedGraph.swift
new file mode 100644
index 0000000..9751b53
--- /dev/null
+++ b/Sources/Grape/ForceDirectedGraph.swift
@@ -0,0 +1,80 @@
+import ForceSimulation
+import SwiftUI
+
+public protocol ForceDescriptor {}
+
+public struct CenterForce: ForceDescriptor {
+ public var x: Double
+ public var y: Double
+ public var strength: Double
+}
+
+public struct ManyBodyForce: ForceDescriptor {
+ public var strength: Double
+ public var theta: Double
+ public var distanceMin: Double
+ public var distanceMax: Double
+}
+
+public struct LinkForce: ForceDescriptor {
+ public var strength: Double
+ public var distance: Double
+ public var iterations: Int
+}
+
+public struct CollideForce: ForceDescriptor {
+ public var strength: Double
+ public var radius: Double
+ public var iterations: Int
+}
+
+public struct DirectionForce: ForceDescriptor {
+ public enum Dimension: Hashable {
+ case x
+ case y
+ }
+ public var strength: Double
+ public var targetOnDirection: Double
+ public var direction: DirectionForce.Dimension
+}
+
+public struct ForceField {
+ public let forces: [ForceDescriptor]
+}
+
+@resultBuilder
+public struct ForceFieldBuilder {
+ public static func buildBlock(_ components: ForceDescriptor...) -> ForceField {
+ return ForceField(forces: components)
+ }
+}
+
+public struct ForceDirectedGraph: View {
+ public struct Content {
+ var nodes: [NodeMark]
+ var links: [LinkMark]
+ }
+
+ public struct ForceSpec {
+
+ }
+
+ public var body: some View {
+ EmptyView()
+ }
+
+ private let content: Content
+ private let simulation: Simulation2D
+ private let forceFieldDescriptor: ForceField
+
+ public init(
+ @GraphContentBuilder _ buildGraphContent: () -> PartialGraphMark,
+ @ForceFieldBuilder _ buildForceField: () -> ForceField
+ ) {
+ let graphMark = buildGraphContent()
+ self.content = Content(nodes: graphMark.nodes, links: graphMark.links)
+ self.simulation = .init(nodeIds: graphMark.nodes.map(\.id))
+ self.forceFieldDescriptor = buildForceField()
+ }
+
+}
diff --git a/Sources/Grape/ForceDirectedGraphContent.swift b/Sources/Grape/ForceDirectedGraphContent.swift
new file mode 100644
index 0000000..073f12e
--- /dev/null
+++ b/Sources/Grape/ForceDirectedGraphContent.swift
@@ -0,0 +1,49 @@
+
+
+public struct ForceDirectedGraphContent {
+
+ var nodes: [NodeMark]
+ var links: [LinkMark]
+
+ init(@GraphContentBuilder builder: () -> PartialGraphMark) {
+ let graphMark = builder()
+ self.nodes = graphMark.nodes
+ self.links = graphMark.links
+ }
+
+ init(nodes: [NodeMark], links: [LinkMark]) {
+ self.nodes = nodes
+ self.links = links
+ }
+}
+
+func test() {
+ let graph = ForceDirectedGraphContent {
+
+ NodeMark(id: 2, fill: .accentColor, radius: 3.0, label: "Hello")
+ NodeMark(id: 3)
+ NodeMark(id: 4)
+
+ 3 <-- 4
+ 4 --> 2
+
+ for i in 20..<40 {
+ NodeMark(id: i)
+ i --> i + 1
+ }
+
+ FullyConnected {
+ NodeMark(id: 8)
+ NodeMark(id: 9)
+ }
+
+ LinkMark(from: 3, to: 4)
+ LinkMark(from: 2, to: 4)
+
+ (2 --> 3) { link in
+ link.strokeColor = .red
+ }
+
+ }
+
+}
diff --git a/Sources/Grape/GraphContent.swift b/Sources/Grape/GraphContent.swift
new file mode 100644
index 0000000..e9ac79e
--- /dev/null
+++ b/Sources/Grape/GraphContent.swift
@@ -0,0 +1 @@
+public protocol GraphContent { }
\ No newline at end of file
diff --git a/Sources/Grape/GraphContentBuilder.swift b/Sources/Grape/GraphContentBuilder.swift
new file mode 100644
index 0000000..66801e7
--- /dev/null
+++ b/Sources/Grape/GraphContentBuilder.swift
@@ -0,0 +1,110 @@
+public final class PartialGraphMark: GraphContent {
+ var nodes: [NodeMark]
+ var links: [LinkMark]
+
+ init(nodes: [NodeMark], links: [LinkMark]) {
+ self.nodes = nodes
+ self.links = links
+ }
+
+ static var empty: PartialGraphMark {
+ return .init(nodes: [], links: [])
+ }
+
+ @discardableResult
+ func with(node: NodeMark) -> Self {
+ nodes.append(node)
+ return self
+ }
+
+ @discardableResult
+ func with(link: LinkMark) -> Self {
+ links.append(link)
+ return self
+ }
+
+ @discardableResult
+ func with(partial: PartialGraphMark) -> Self {
+ links.append(contentsOf: partial.links)
+ nodes.append(contentsOf: partial.nodes)
+ return self
+ }
+}
+
+public struct FullyConnected: GraphContent {
+ public var connectedPartial: PartialGraphMark
+ public init(@GraphContentBuilder builder: () -> PartialGraphMark) {
+ let result = builder()
+ result.links = []
+ for i in result.nodes.indices {
+ for j in i + 1..(from: result.nodes[i].id, to: result.nodes[j].id))
+ }
+ }
+ connectedPartial = result
+ }
+}
+
+@resultBuilder
+public struct GraphContentBuilder {
+
+ public typealias Link = LinkMark
+ public typealias Node = NodeMark
+ public typealias PartialGraph = PartialGraphMark
+
+ public static func buildPartialBlock(first content: Node) -> PartialGraph {
+ return PartialGraph(nodes: [content], links: [])
+ }
+
+ public static func buildPartialBlock(first content: Link) -> PartialGraph {
+ return PartialGraph(nodes: [], links: [content])
+ }
+
+ public static func buildPartialBlock(accumulated: PartialGraph, next: Link) -> PartialGraph {
+ return accumulated.with(link: next)
+ }
+
+ public static func buildPartialBlock(accumulated: PartialGraph, next: Node) -> PartialGraph {
+ return accumulated.with(node: next)
+ }
+
+ public static func buildPartialBlock(accumulated: PartialGraph, next: PartialGraph) -> PartialGraph {
+ return accumulated.with(partial: next)
+ }
+
+ public static func buildBlock() -> PartialGraph {
+ return PartialGraph.empty
+ }
+
+ public static func buildExpression(_ expression: (NodeID, NodeID)) -> Link {
+ return Link(from: expression.0, to: expression.1)
+ }
+
+ public static func buildExpression(_ expression: Node) -> Node {
+ return expression
+ }
+
+ public static func buildExpression(_ expression: Link) -> Link {
+ return expression
+ }
+
+ public static func buildExpression(_ expression: FullyConnected) -> PartialGraph {
+ return expression.connectedPartial
+ }
+
+ public static func buildArray(_ components: [PartialGraph]) -> PartialGraph {
+ let partial = PartialGraph(nodes: [], links: [])
+ for expr in components {
+ partial.with(partial: expr)
+ }
+ return partial
+ }
+
+ public static func buildExpression(
+ @GraphContentBuilder expression: () -> PartialGraphMark
+ ) -> PartialGraph {
+ return PartialGraph.empty
+ }
+
+}
diff --git a/Sources/Grape/LinkMark.swift b/Sources/Grape/LinkMark.swift
new file mode 100644
index 0000000..d0b39bd
--- /dev/null
+++ b/Sources/Grape/LinkMark.swift
@@ -0,0 +1,78 @@
+import ForceSimulation
+import SwiftUI
+
+@dynamicCallable
+public struct LinkMark: GraphContent {
+
+ public enum LabelDisplayStrategy {
+ case auto
+ case specified(Bool)
+ case byPageRank((Double, Double) -> Bool)
+ }
+
+ public enum LabelPositioning {
+ case auto
+ }
+
+ public enum ArrowStyle {
+ case none
+ case triangle
+ }
+
+ public var id: EdgeID
+
+ public var label: String?
+ public var labelColor: Color
+ public var labelDisplayStrategy: LabelDisplayStrategy
+ public var labelPositioning: LabelPositioning
+
+ public var strokeColor: Color
+ public var strokeWidth: Double
+ public var strokeDashArray: [Double]?
+
+ public var arrowStyle: ArrowStyle
+
+ public init(
+ from: NodeID,
+ to: NodeID,
+ label: String? = nil,
+ labelColor: Color = .gray,
+ labelDisplayStrategy: LabelDisplayStrategy = .auto,
+ labelPositioning: LabelPositioning = .auto,
+ strokeColor: Color = .gray.opacity(0.2),
+ strokeWidth: Double = 1.0,
+ strokeDashArray: [Double]? = nil,
+ arrowStyle: ArrowStyle = .none
+ ) {
+ self.id = .init(from, to)
+ self.label = label
+ self.labelColor = labelColor
+ self.labelDisplayStrategy = labelDisplayStrategy
+ self.labelPositioning = labelPositioning
+ self.strokeColor = strokeColor
+ self.strokeWidth = strokeWidth
+ self.strokeDashArray = strokeDashArray
+ self.arrowStyle = arrowStyle
+ }
+
+ public func dynamicallyCall(withArguments: [(inout Self) -> Void]) -> Self {
+ var _self = self
+ for argument in withArguments {
+ argument(&_self)
+ }
+ return _self
+ }
+}
+
+infix operator --> : AssignmentPrecedence
+infix operator <-- : AssignmentPrecedence
+
+extension Hashable {
+ public static func --> (lhs: Self, rhs: Self) -> LinkMark {
+ return LinkMark(from: lhs, to: rhs)
+ }
+
+ public static func <-- (lhs: Self, rhs: Self) -> LinkMark {
+ return LinkMark(from: lhs, to: rhs)
+ }
+}
diff --git a/Sources/Grape/NodeMark.swift b/Sources/Grape/NodeMark.swift
new file mode 100644
index 0000000..6dc1824
--- /dev/null
+++ b/Sources/Grape/NodeMark.swift
@@ -0,0 +1,52 @@
+import SwiftUI
+
+
+public struct NodeMark: GraphContent {
+
+ public enum LabelDisplayStrategy {
+ case auto
+ case specified(Bool)
+ case byPageRank((Double) -> Bool)
+ }
+
+ public enum LabelPositioning {
+ case bottomOfMark
+ case topOfMark
+ case startAfterMark
+ case endBeforeMark
+ }
+
+ public var id: NodeID
+
+ public var fill: Color
+ public var strokeColor: Color?
+ public var strokeWidth: Double
+ public var radius: Double
+ public var label: String?
+ public var labelColor: Color
+ public var labelDisplayStrategy: LabelDisplayStrategy
+ public var labelPositioning: LabelPositioning
+
+ public init(
+ id: NodeID,
+ fill: Color = .accentColor,
+ radius: Double = 3.0,
+ label: String? = nil,
+ labelColor: Color = .accentColor,
+ labelDisplayStrategy: LabelDisplayStrategy = .auto,
+ labelPositioning: LabelPositioning = .bottomOfMark,
+ strokeColor: Color? = nil,
+ strokeWidth: Double = 1
+ ) {
+ self.id = id
+ self.fill = fill
+ self.radius = radius
+ self.label = label
+ self.labelColor = labelColor
+ self.labelDisplayStrategy = labelDisplayStrategy
+ self.labelPositioning = labelPositioning
+
+ self.strokeColor = strokeColor
+ self.strokeWidth = strokeWidth
+ }
+}
From b916ec5e54ef20b7ae245e0b1e4e808747e6b1a9 Mon Sep 17 00:00:00 2001
From: li3zhen1
Date: Sun, 5 Nov 2023 11:37:10 -0500
Subject: [PATCH 2/7] Removing NodeID
---
Sources/Grape/ForceDescriptor.swift | 62 ++++++++++++++++++
Sources/Grape/ForceDirectedGraph.swift | 84 ++++++++++---------------
Sources/Grape/GraphContentBuilder.swift | 9 ++-
3 files changed, 103 insertions(+), 52 deletions(-)
create mode 100644 Sources/Grape/ForceDescriptor.swift
diff --git a/Sources/Grape/ForceDescriptor.swift b/Sources/Grape/ForceDescriptor.swift
new file mode 100644
index 0000000..4d1b432
--- /dev/null
+++ b/Sources/Grape/ForceDescriptor.swift
@@ -0,0 +1,62 @@
+import ForceSimulation
+import simd
+
+public protocol ForceDescriptor {
+ func attachToSimulation(_ simulation: Simulation2D)
+}
+
+public struct ForceField: ForceDescriptor {
+ public let forces: [ForceDescriptor]
+
+ public func attachToSimulation(_ simulation: Simulation2D) where NodeID : Hashable {
+ for forceDescriptor in forces {
+ forceDescriptor.attachToSimulation(simulation)
+ }
+ }
+}
+
+public struct CenterForce: ForceDescriptor {
+ public var x: Double
+ public var y: Double
+ public var strength: Double
+
+ public func attachToSimulation(_ simulation: Simulation2D) where NodeID : Hashable {
+ simulation.createCenterForce(center: [x, y], strength: strength)
+ }
+}
+
+public struct ManyBodyForce: ForceDescriptor {
+
+ public var strength: Double
+ public var theta: Double
+ public var distanceMin: Double
+ public var distanceMax: Double
+
+ public func attachToSimulation(_ simulation: Simulation2D) {
+ simulation.createManyBodyForce(strength: strength, nodeMass: .)
+ }
+}
+
+public struct LinkForce: ForceDescriptor {
+ public var strength: Double
+ public var distance: Double
+ public var iterations: Int
+}
+
+public struct CollideForce: ForceDescriptor {
+ public var strength: Double
+ public var radius: Double
+ public var iterations: Int
+}
+
+public struct DirectionForce: ForceDescriptor {
+ public enum Dimension: Hashable {
+ case x
+ case y
+ }
+ public var strength: Double
+ public var targetOnDirection: Double
+ public var direction: DirectionForce.Dimension
+}
+
+
diff --git a/Sources/Grape/ForceDirectedGraph.swift b/Sources/Grape/ForceDirectedGraph.swift
index 9751b53..f883c3b 100644
--- a/Sources/Grape/ForceDirectedGraph.swift
+++ b/Sources/Grape/ForceDirectedGraph.swift
@@ -1,47 +1,6 @@
import ForceSimulation
import SwiftUI
-public protocol ForceDescriptor {}
-
-public struct CenterForce: ForceDescriptor {
- public var x: Double
- public var y: Double
- public var strength: Double
-}
-
-public struct ManyBodyForce: ForceDescriptor {
- public var strength: Double
- public var theta: Double
- public var distanceMin: Double
- public var distanceMax: Double
-}
-
-public struct LinkForce: ForceDescriptor {
- public var strength: Double
- public var distance: Double
- public var iterations: Int
-}
-
-public struct CollideForce: ForceDescriptor {
- public var strength: Double
- public var radius: Double
- public var iterations: Int
-}
-
-public struct DirectionForce: ForceDescriptor {
- public enum Dimension: Hashable {
- case x
- case y
- }
- public var strength: Double
- public var targetOnDirection: Double
- public var direction: DirectionForce.Dimension
-}
-
-public struct ForceField {
- public let forces: [ForceDescriptor]
-}
-
@resultBuilder
public struct ForceFieldBuilder {
public static func buildBlock(_ components: ForceDescriptor...) -> ForceField {
@@ -51,25 +10,28 @@ public struct ForceFieldBuilder {
public struct ForceDirectedGraph: View {
public struct Content {
- var nodes: [NodeMark]
- var links: [LinkMark]
- }
-
- public struct ForceSpec {
+ public var nodes: [NodeMark]
+ public var links: [LinkMark]
+ public init(nodes: [NodeMark], links: [LinkMark]) {
+ self.nodes = nodes
+ self.links = links
+ }
}
+
public var body: some View {
EmptyView()
}
- private let content: Content
- private let simulation: Simulation2D
- private let forceFieldDescriptor: ForceField
+ @usableFromInline let content: Content
+ @usableFromInline let simulation: Simulation2D
+ @usableFromInline let forceFieldDescriptor: ForceField
+ @inlinable
public init(
@GraphContentBuilder _ buildGraphContent: () -> PartialGraphMark,
- @ForceFieldBuilder _ buildForceField: () -> ForceField
+ @ForceFieldBuilder forceField buildForceField: () -> ForceField
) {
let graphMark = buildGraphContent()
self.content = Content(nodes: graphMark.nodes, links: graphMark.links)
@@ -77,4 +39,26 @@ public struct ForceDirectedGraph: View {
self.forceFieldDescriptor = buildForceField()
}
+
+ @inlinable
+ func buildForceField() {
+ for forceDescriptor in forceFieldDescriptor.forces {
+ switch forceDescriptor {
+ case let centerForce as CenterForce:
+ simulation.addCenterForce(x: centerForce.x, y: centerForce.y, strength: centerForce.strength)
+ case let manyBodyForce as ManyBodyForce:
+ simulation.addManyBodyForce(strength: manyBodyForce.strength, theta: manyBodyForce.theta, distanceMin: manyBodyForce.distanceMin, distanceMax: manyBodyForce.distanceMax)
+ case let linkForce as LinkForce:
+ simulation.addLinkForce(strength: linkForce.strength, distance: linkForce.distance, iterations: linkForce.iterations)
+ case let collideForce as CollideForce:
+ simulation.addCollideForce(strength: collideForce.strength, radius: collideForce.radius, iterations: collideForce.iterations)
+ case let directionForce as DirectionForce:
+ simulation.addDirectionForce(strength: directionForce.strength, targetOnDirection: directionForce.targetOnDirection, direction: directionForce.direction)
+ default:
+ break
+ }
+ }
+ }
+
+
}
diff --git a/Sources/Grape/GraphContentBuilder.swift b/Sources/Grape/GraphContentBuilder.swift
index 66801e7..99bbfc1 100644
--- a/Sources/Grape/GraphContentBuilder.swift
+++ b/Sources/Grape/GraphContentBuilder.swift
@@ -1,29 +1,34 @@
public final class PartialGraphMark: GraphContent {
- var nodes: [NodeMark]
- var links: [LinkMark]
+ @usableFromInline var nodes: [NodeMark]
+ @usableFromInline var links: [LinkMark]
+ @inlinable
init(nodes: [NodeMark], links: [LinkMark]) {
self.nodes = nodes
self.links = links
}
+ @inlinable
static var empty: PartialGraphMark {
return .init(nodes: [], links: [])
}
@discardableResult
+ @inlinable
func with(node: NodeMark) -> Self {
nodes.append(node)
return self
}
@discardableResult
+ @inlinable
func with(link: LinkMark) -> Self {
links.append(link)
return self
}
@discardableResult
+ @initializes
func with(partial: PartialGraphMark) -> Self {
links.append(contentsOf: partial.links)
nodes.append(contentsOf: partial.nodes)
From efdb0856d0eab8facb56e4e863a3851c8192245b Mon Sep 17 00:00:00 2001
From: li3zhen1
Date: Sun, 5 Nov 2023 14:38:44 -0500
Subject: [PATCH 3/7] Add minimal SwiftUI implementation
---
.../project.pbxproj | 18 +++
.../ForceDirectedGraphExampleApp.swift | 4 +-
.../ForceDirectedGraphSwiftUIExample.swift | 36 ++++++
Package.swift | 8 +-
Sources/Grape/Contents/ForceDescriptor.swift | 101 ++++++++++++++++
.../Contents/ForceDirectedGraphContent.swift | 48 ++++++++
.../Grape/{ => Contents}/GraphContent.swift | 0
.../{ => Contents}/GraphContentBuilder.swift | 0
Sources/Grape/{ => Contents}/LinkMark.swift | 0
Sources/Grape/{ => Contents}/NodeMark.swift | 0
Sources/Grape/ForceDescriptor.swift | 62 ----------
Sources/Grape/ForceDirectedGraph.swift | 64 ----------
Sources/Grape/ForceDirectedGraphContent.swift | 49 --------
.../ForceDirectedGraph2DController.swift | 22 ++++
.../ForceDirectedGraph2DLayoutEngine.swift | 49 ++++++++
Sources/Grape/Views/ForceDirectedGraph.swift | 110 ++++++++++++++++++
16 files changed, 391 insertions(+), 180 deletions(-)
create mode 100644 Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/ForceDirectedGraphSwiftUIExample.swift
create mode 100644 Sources/Grape/Contents/ForceDescriptor.swift
create mode 100644 Sources/Grape/Contents/ForceDirectedGraphContent.swift
rename Sources/Grape/{ => Contents}/GraphContent.swift (100%)
rename Sources/Grape/{ => Contents}/GraphContentBuilder.swift (100%)
rename Sources/Grape/{ => Contents}/LinkMark.swift (100%)
rename Sources/Grape/{ => Contents}/NodeMark.swift (100%)
delete mode 100644 Sources/Grape/ForceDescriptor.swift
delete mode 100644 Sources/Grape/ForceDirectedGraph.swift
delete mode 100644 Sources/Grape/ForceDirectedGraphContent.swift
create mode 100644 Sources/Grape/Models/ForceDirectedGraph2DController.swift
create mode 100644 Sources/Grape/Models/ForceDirectedGraph2DLayoutEngine.swift
create mode 100644 Sources/Grape/Views/ForceDirectedGraph.swift
diff --git a/Examples/ForceDirectedGraphExample/ForceDirectedGraphExample.xcodeproj/project.pbxproj b/Examples/ForceDirectedGraphExample/ForceDirectedGraphExample.xcodeproj/project.pbxproj
index ef8f6c7..8c36c4d 100644
--- a/Examples/ForceDirectedGraphExample/ForceDirectedGraphExample.xcodeproj/project.pbxproj
+++ b/Examples/ForceDirectedGraphExample/ForceDirectedGraphExample.xcodeproj/project.pbxproj
@@ -7,7 +7,10 @@
objects = {
/* Begin PBXBuildFile section */
+ B70B52AD2AF822FF00A1E6CD /* ForceSimulation in Frameworks */ = {isa = PBXBuildFile; productRef = B70B52AC2AF822FF00A1E6CD /* ForceSimulation */; };
+ B70B52AF2AF822FF00A1E6CD /* Grape in Frameworks */ = {isa = PBXBuildFile; productRef = B70B52AE2AF822FF00A1E6CD /* Grape */; };
B719E4112AE5FBFC009D6C24 /* ForceDirectedLatticeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B719E4102AE5FBFC009D6C24 /* ForceDirectedLatticeView.swift */; };
+ B7A830CD2AF822BB00A7AF6B /* ForceDirectedGraphSwiftUIExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A830CC2AF822BB00A7AF6B /* ForceDirectedGraphSwiftUIExample.swift */; };
B7AFA55B2ADF4997009C7154 /* ForceDirectedGraphExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7AFA55A2ADF4997009C7154 /* ForceDirectedGraphExampleApp.swift */; };
B7AFA55D2ADF4997009C7154 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7AFA55C2ADF4997009C7154 /* ContentView.swift */; };
B7AFA55F2ADF4999009C7154 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B7AFA55E2ADF4999009C7154 /* Assets.xcassets */; };
@@ -18,6 +21,7 @@
/* Begin PBXFileReference section */
B719E4102AE5FBFC009D6C24 /* ForceDirectedLatticeView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ForceDirectedLatticeView.swift; sourceTree = ""; };
+ B7A830CC2AF822BB00A7AF6B /* ForceDirectedGraphSwiftUIExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForceDirectedGraphSwiftUIExample.swift; sourceTree = ""; };
B7AFA5572ADF4997009C7154 /* ForceDirectedGraphExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ForceDirectedGraphExample.app; sourceTree = BUILT_PRODUCTS_DIR; };
B7AFA55A2ADF4997009C7154 /* ForceDirectedGraphExampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForceDirectedGraphExampleApp.swift; sourceTree = ""; };
B7AFA55C2ADF4997009C7154 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
@@ -32,7 +36,9 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
+ B70B52AF2AF822FF00A1E6CD /* Grape in Frameworks */,
B7AFA56B2ADF49AA009C7154 /* ForceSimulation in Frameworks */,
+ B70B52AD2AF822FF00A1E6CD /* ForceSimulation in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -65,6 +71,7 @@
B7AFA5632ADF4999009C7154 /* ForceDirectedGraphExample.entitlements */,
B7AFA5602ADF4999009C7154 /* Preview Content */,
B7AFA56E2ADF49D6009C7154 /* Data.swift */,
+ B7A830CC2AF822BB00A7AF6B /* ForceDirectedGraphSwiftUIExample.swift */,
);
path = ForceDirectedGraphExample;
sourceTree = "";
@@ -95,6 +102,8 @@
name = ForceDirectedGraphExample;
packageProductDependencies = (
B7AFA56A2ADF49AA009C7154 /* ForceSimulation */,
+ B70B52AC2AF822FF00A1E6CD /* ForceSimulation */,
+ B70B52AE2AF822FF00A1E6CD /* Grape */,
);
productName = ForceDirectedGraphExample;
productReference = B7AFA5572ADF4997009C7154 /* ForceDirectedGraphExample.app */;
@@ -155,6 +164,7 @@
files = (
B7AFA55D2ADF4997009C7154 /* ContentView.swift in Sources */,
B719E4112AE5FBFC009D6C24 /* ForceDirectedLatticeView.swift in Sources */,
+ B7A830CD2AF822BB00A7AF6B /* ForceDirectedGraphSwiftUIExample.swift in Sources */,
B7AFA56F2ADF49D6009C7154 /* Data.swift in Sources */,
B7AFA55B2ADF4997009C7154 /* ForceDirectedGraphExampleApp.swift in Sources */,
);
@@ -362,6 +372,14 @@
/* End XCLocalSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
+ B70B52AC2AF822FF00A1E6CD /* ForceSimulation */ = {
+ isa = XCSwiftPackageProductDependency;
+ productName = ForceSimulation;
+ };
+ B70B52AE2AF822FF00A1E6CD /* Grape */ = {
+ isa = XCSwiftPackageProductDependency;
+ productName = Grape;
+ };
B7AFA56A2ADF49AA009C7154 /* ForceSimulation */ = {
isa = XCSwiftPackageProductDependency;
productName = ForceSimulation;
diff --git a/Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/ForceDirectedGraphExampleApp.swift b/Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/ForceDirectedGraphExampleApp.swift
index 83729b0..f8d5528 100644
--- a/Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/ForceDirectedGraphExampleApp.swift
+++ b/Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/ForceDirectedGraphExampleApp.swift
@@ -12,7 +12,9 @@ struct ForceDirectedGraphExampleApp: App {
var body: some Scene {
WindowGroup {
// ContentView().padding(0)
- ForceDirectedLatticeView().padding(0)//.ignoresSafeArea()
+// ForceDirectedLatticeView().padding(0)//.ignoresSafeArea()
+ ForceDirectedGraphSwiftUIExample()
+
}
}
}
diff --git a/Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/ForceDirectedGraphSwiftUIExample.swift b/Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/ForceDirectedGraphSwiftUIExample.swift
new file mode 100644
index 0000000..467e04f
--- /dev/null
+++ b/Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/ForceDirectedGraphSwiftUIExample.swift
@@ -0,0 +1,36 @@
+//
+// ForceDirectedGraphSwiftUIExample.swift
+// ForceDirectedGraphExample
+//
+// Created by li3zhen1 on 11/5/23.
+//
+
+import Foundation
+import SwiftUI
+import Grape
+
+
+struct ForceDirectedGraphSwiftUIExample: View {
+ let graphController = ForceDirectedGraph2DController()
+ var body: some View {
+ ForceDirectedGraph(controller: graphController) {
+ NodeMark(id: 0)
+ for i in 1..<10 {
+ NodeMark(id: i)
+ }
+
+ for i in 0..<9 {
+ LinkMark(from: i, to: i+1)
+ }
+
+ } forceField: {
+ LinkForce()
+ CenterForce()
+ ManyBodyForce()
+ }
+ .onAppear {
+ graphController.start()
+ }
+
+ }
+}
diff --git a/Package.swift b/Package.swift
index 546fc34..bf10135 100644
--- a/Package.swift
+++ b/Package.swift
@@ -1,4 +1,4 @@
-// swift-tools-version: 5.6
+// swift-tools-version: 5.9
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
@@ -6,9 +6,9 @@ import PackageDescription
let package = Package(
name: "Grape",
platforms: [
- .macOS(.v11),
- .iOS(.v14),
- .watchOS(.v7),
+ .macOS(.v14),
+ .iOS(.v17),
+ .watchOS(.v9),
],
products: [
diff --git a/Sources/Grape/Contents/ForceDescriptor.swift b/Sources/Grape/Contents/ForceDescriptor.swift
new file mode 100644
index 0000000..582b712
--- /dev/null
+++ b/Sources/Grape/Contents/ForceDescriptor.swift
@@ -0,0 +1,101 @@
+import ForceSimulation
+import simd
+
+public protocol ForceDescriptor {
+ func attachToSimulation(_ simulation: Simulation2D)
+}
+
+public struct ForceSet {
+ public let forces: [ForceDescriptor]
+}
+
+public struct CenterForce: ForceDescriptor {
+ public var x: Double
+ public var y: Double
+ public var strength: Double
+
+ public init(
+ x: Double = 0.0,
+ y: Double = 0.0,
+ strength: Double = 0.5
+ ) {
+ self.x = x
+ self.y = y
+ self.strength = strength
+ }
+
+ public func attachToSimulation(_ simulation: Simulation2D) {
+ simulation.createCenterForce(center: [x, y], strength: strength)
+ }
+}
+
+public struct ManyBodyForce: ForceDescriptor {
+
+ public var strength: Double
+ public var mass: Simulation2D.ManyBodyForce.NodeMass
+
+ public init(
+ strength: Double = -30.0,
+ mass: Simulation2D.ManyBodyForce.NodeMass = .constant(1.0)
+ ) {
+ self.strength = strength
+ self.mass = mass
+ }
+
+ public func attachToSimulation(_ simulation: Simulation2D) {
+ simulation.createManyBodyForce(strength: strength, nodeMass: mass)
+ }
+}
+
+public struct LinkForce: ForceDescriptor {
+ public var stiffness: Simulation2D.LinkForce.LinkStiffness
+ public var originalLength: Simulation2D.LinkForce.LinkLength
+ public var iterationsPerTick: UInt
+ @usableFromInline var links: [EdgeID]
+
+ public init(
+ originalLength: Simulation2D.LinkForce.LinkLength = .constant(30.0),
+ stiffness: Simulation2D.LinkForce.LinkStiffness = .weightedByDegree { _, _ in 1.0 },
+ iterationsPerTick: UInt = 1
+ ) {
+ self.stiffness = stiffness
+ self.originalLength = originalLength
+ self.iterationsPerTick = iterationsPerTick
+ self.links = []
+ }
+
+ public func attachToSimulation(_ simulation: Simulation2D) {
+ simulation.createLinkForce(links, stiffness: stiffness, originalLength: originalLength, iterationsPerTick: iterationsPerTick)
+ }
+}
+
+public struct CollideForce: ForceDescriptor {
+ public var strength: Double
+ public var radius: Simulation2D.CollideForce.CollideRadius = .constant(3.0)
+ public var iterationsPerTick: UInt = 1
+
+ public func attachToSimulation(_ simulation: Simulation2D) {
+ simulation.createCollideForce(radius: radius, strength: strength, iterationsPerTick: iterationsPerTick)
+ }
+}
+
+public struct DirectionForce: ForceDescriptor {
+
+ public var strength: Simulation2D.DirectionForce2D.Strength
+ public var targetOnDirection: Simulation2D.DirectionForce2D.TargetOnDirection
+ public var direction: Simulation2D.DirectionForce2D.Direction
+
+ public init(
+ direction: Simulation2D.DirectionForce2D.Direction,
+ targetOnDirection: Simulation2D.DirectionForce2D.TargetOnDirection,
+ strength: Simulation2D.DirectionForce2D.Strength = .constant(1.0)
+ ) {
+ self.strength = strength
+ self.direction = direction
+ self.targetOnDirection = targetOnDirection
+ }
+
+ public func attachToSimulation(_ simulation: Simulation2D) {
+ simulation.createPositionForce(direction: direction, targetOnDirection: targetOnDirection, strength: strength)
+ }
+}
diff --git a/Sources/Grape/Contents/ForceDirectedGraphContent.swift b/Sources/Grape/Contents/ForceDirectedGraphContent.swift
new file mode 100644
index 0000000..f2f9464
--- /dev/null
+++ b/Sources/Grape/Contents/ForceDirectedGraphContent.swift
@@ -0,0 +1,48 @@
+import SwiftUI
+
+public struct ForceDirectedGraphContent {
+
+ var nodes: [NodeMark]
+ var links: [LinkMark]
+
+ init(@GraphContentBuilder builder: () -> PartialGraphMark) {
+ let graphMark = builder()
+ self.nodes = graphMark.nodes
+ self.links = graphMark.links
+ }
+
+ init(nodes: [NodeMark], links: [LinkMark]) {
+ self.nodes = nodes
+ self.links = links
+ }
+}
+
+struct TestGraphView: View {
+
+ var controller = ForceDirectedGraph2DController()
+
+ var body: some View {
+ ForceDirectedGraph(controller: controller) {
+ NodeMark(id: 2, fill: .accentColor, radius: 3.0, label: "Hello")
+ NodeMark(id: 3)
+ NodeMark(id: 4)
+ 3 <-- 4
+ 4 --> 2
+ for i in 20..<40 {
+ NodeMark(id: i)
+ i --> i + 1
+ }
+ FullyConnected {
+ NodeMark(id: 8)
+ NodeMark(id: 9)
+ }
+ LinkMark(from: 3, to: 4)
+ LinkMark(from: 2, to: 4)
+ } forceField: {
+ LinkForce()
+ }
+ }
+
+
+
+}
diff --git a/Sources/Grape/GraphContent.swift b/Sources/Grape/Contents/GraphContent.swift
similarity index 100%
rename from Sources/Grape/GraphContent.swift
rename to Sources/Grape/Contents/GraphContent.swift
diff --git a/Sources/Grape/GraphContentBuilder.swift b/Sources/Grape/Contents/GraphContentBuilder.swift
similarity index 100%
rename from Sources/Grape/GraphContentBuilder.swift
rename to Sources/Grape/Contents/GraphContentBuilder.swift
diff --git a/Sources/Grape/LinkMark.swift b/Sources/Grape/Contents/LinkMark.swift
similarity index 100%
rename from Sources/Grape/LinkMark.swift
rename to Sources/Grape/Contents/LinkMark.swift
diff --git a/Sources/Grape/NodeMark.swift b/Sources/Grape/Contents/NodeMark.swift
similarity index 100%
rename from Sources/Grape/NodeMark.swift
rename to Sources/Grape/Contents/NodeMark.swift
diff --git a/Sources/Grape/ForceDescriptor.swift b/Sources/Grape/ForceDescriptor.swift
deleted file mode 100644
index 4d1b432..0000000
--- a/Sources/Grape/ForceDescriptor.swift
+++ /dev/null
@@ -1,62 +0,0 @@
-import ForceSimulation
-import simd
-
-public protocol ForceDescriptor {
- func attachToSimulation(_ simulation: Simulation2D)
-}
-
-public struct ForceField: ForceDescriptor {
- public let forces: [ForceDescriptor]
-
- public func attachToSimulation(_ simulation: Simulation2D) where NodeID : Hashable {
- for forceDescriptor in forces {
- forceDescriptor.attachToSimulation(simulation)
- }
- }
-}
-
-public struct CenterForce: ForceDescriptor {
- public var x: Double
- public var y: Double
- public var strength: Double
-
- public func attachToSimulation(_ simulation: Simulation2D) where NodeID : Hashable {
- simulation.createCenterForce(center: [x, y], strength: strength)
- }
-}
-
-public struct ManyBodyForce: ForceDescriptor {
-
- public var strength: Double
- public var theta: Double
- public var distanceMin: Double
- public var distanceMax: Double
-
- public func attachToSimulation(_ simulation: Simulation2D) {
- simulation.createManyBodyForce(strength: strength, nodeMass: .)
- }
-}
-
-public struct LinkForce: ForceDescriptor {
- public var strength: Double
- public var distance: Double
- public var iterations: Int
-}
-
-public struct CollideForce: ForceDescriptor {
- public var strength: Double
- public var radius: Double
- public var iterations: Int
-}
-
-public struct DirectionForce: ForceDescriptor {
- public enum Dimension: Hashable {
- case x
- case y
- }
- public var strength: Double
- public var targetOnDirection: Double
- public var direction: DirectionForce.Dimension
-}
-
-
diff --git a/Sources/Grape/ForceDirectedGraph.swift b/Sources/Grape/ForceDirectedGraph.swift
deleted file mode 100644
index f883c3b..0000000
--- a/Sources/Grape/ForceDirectedGraph.swift
+++ /dev/null
@@ -1,64 +0,0 @@
-import ForceSimulation
-import SwiftUI
-
-@resultBuilder
-public struct ForceFieldBuilder {
- public static func buildBlock(_ components: ForceDescriptor...) -> ForceField {
- return ForceField(forces: components)
- }
-}
-
-public struct ForceDirectedGraph: View {
- public struct Content {
- public var nodes: [NodeMark]
- public var links: [LinkMark]
-
- public init(nodes: [NodeMark], links: [LinkMark]) {
- self.nodes = nodes
- self.links = links
- }
- }
-
-
- public var body: some View {
- EmptyView()
- }
-
- @usableFromInline let content: Content
- @usableFromInline let simulation: Simulation2D
- @usableFromInline let forceFieldDescriptor: ForceField
-
- @inlinable
- public init(
- @GraphContentBuilder _ buildGraphContent: () -> PartialGraphMark,
- @ForceFieldBuilder forceField buildForceField: () -> ForceField
- ) {
- let graphMark = buildGraphContent()
- self.content = Content(nodes: graphMark.nodes, links: graphMark.links)
- self.simulation = .init(nodeIds: graphMark.nodes.map(\.id))
- self.forceFieldDescriptor = buildForceField()
- }
-
-
- @inlinable
- func buildForceField() {
- for forceDescriptor in forceFieldDescriptor.forces {
- switch forceDescriptor {
- case let centerForce as CenterForce:
- simulation.addCenterForce(x: centerForce.x, y: centerForce.y, strength: centerForce.strength)
- case let manyBodyForce as ManyBodyForce:
- simulation.addManyBodyForce(strength: manyBodyForce.strength, theta: manyBodyForce.theta, distanceMin: manyBodyForce.distanceMin, distanceMax: manyBodyForce.distanceMax)
- case let linkForce as LinkForce:
- simulation.addLinkForce(strength: linkForce.strength, distance: linkForce.distance, iterations: linkForce.iterations)
- case let collideForce as CollideForce:
- simulation.addCollideForce(strength: collideForce.strength, radius: collideForce.radius, iterations: collideForce.iterations)
- case let directionForce as DirectionForce:
- simulation.addDirectionForce(strength: directionForce.strength, targetOnDirection: directionForce.targetOnDirection, direction: directionForce.direction)
- default:
- break
- }
- }
- }
-
-
-}
diff --git a/Sources/Grape/ForceDirectedGraphContent.swift b/Sources/Grape/ForceDirectedGraphContent.swift
deleted file mode 100644
index 073f12e..0000000
--- a/Sources/Grape/ForceDirectedGraphContent.swift
+++ /dev/null
@@ -1,49 +0,0 @@
-
-
-public struct ForceDirectedGraphContent {
-
- var nodes: [NodeMark]
- var links: [LinkMark]
-
- init(@GraphContentBuilder builder: () -> PartialGraphMark) {
- let graphMark = builder()
- self.nodes = graphMark.nodes
- self.links = graphMark.links
- }
-
- init(nodes: [NodeMark], links: [LinkMark]) {
- self.nodes = nodes
- self.links = links
- }
-}
-
-func test() {
- let graph = ForceDirectedGraphContent {
-
- NodeMark(id: 2, fill: .accentColor, radius: 3.0, label: "Hello")
- NodeMark(id: 3)
- NodeMark(id: 4)
-
- 3 <-- 4
- 4 --> 2
-
- for i in 20..<40 {
- NodeMark(id: i)
- i --> i + 1
- }
-
- FullyConnected {
- NodeMark(id: 8)
- NodeMark(id: 9)
- }
-
- LinkMark(from: 3, to: 4)
- LinkMark(from: 2, to: 4)
-
- (2 --> 3) { link in
- link.strokeColor = .red
- }
-
- }
-
-}
diff --git a/Sources/Grape/Models/ForceDirectedGraph2DController.swift b/Sources/Grape/Models/ForceDirectedGraph2DController.swift
new file mode 100644
index 0000000..23bc4d9
--- /dev/null
+++ b/Sources/Grape/Models/ForceDirectedGraph2DController.swift
@@ -0,0 +1,22 @@
+import Observation
+
+
+@Observable
+public class ForceDirectedGraph2DController {
+
+ @ObservationIgnored
+ @usableFromInline
+ weak var layoutEngine: ForceDirectedGraph2DLayoutEngine?
+
+ public init() {
+
+ }
+
+ public func start() {
+ layoutEngine?.start()
+ }
+
+ public func stop() {
+ layoutEngine?.stop()
+ }
+}
\ No newline at end of file
diff --git a/Sources/Grape/Models/ForceDirectedGraph2DLayoutEngine.swift b/Sources/Grape/Models/ForceDirectedGraph2DLayoutEngine.swift
new file mode 100644
index 0000000..f72ccef
--- /dev/null
+++ b/Sources/Grape/Models/ForceDirectedGraph2DLayoutEngine.swift
@@ -0,0 +1,49 @@
+import ForceSimulation
+import Observation
+import SwiftUI
+
+protocol LayoutEngine {
+
+}
+
+@Observable
+public class ForceDirectedGraph2DLayoutEngine: LayoutEngine {
+
+ public var simulation: Simulation2D
+
+ @ObservationIgnored
+ let frameRate: Double = 60.0
+
+ @ObservationIgnored
+ var scheduledTimer: Timer? = nil
+
+ @inlinable
+ public init(initialSimulation: Simulation2D) {
+ self.simulation = initialSimulation
+ }
+
+ public func start() {
+ guard self.scheduledTimer == nil else { return }
+ self.scheduledTimer = Timer.scheduledTimer(withTimeInterval: 1.0 / frameRate, repeats: true) { [weak self] _ in
+ self?.tick()
+ }
+ }
+
+ public func stop() {
+ self.scheduledTimer?.invalidate()
+ self.scheduledTimer = nil
+ }
+
+ public func tick() {
+ withMutation(keyPath: \.simulation) {
+ simulation.tick()
+ }
+ }
+
+ public func tick(waitingForTickingOn queue: DispatchQueue) {
+ queue.asyncAndWait {
+ self.simulation.tick()
+ }
+ withMutation(keyPath: \.self.simulation) { }
+ }
+}
diff --git a/Sources/Grape/Views/ForceDirectedGraph.swift b/Sources/Grape/Views/ForceDirectedGraph.swift
new file mode 100644
index 0000000..8695bbc
--- /dev/null
+++ b/Sources/Grape/Views/ForceDirectedGraph.swift
@@ -0,0 +1,110 @@
+import ForceSimulation
+import SwiftUI
+
+@resultBuilder
+public struct ForceFieldBuilder {
+ public static func buildBlock(_ components: ForceDescriptor...) -> [ForceDescriptor] {
+ return components
+ }
+}
+
+public struct ForceDirectedGraph: View {
+ public struct Content {
+ public var nodes: [NodeMark]
+ public var links: [LinkMark]
+
+ public init(nodes: [NodeMark], links: [LinkMark]) {
+ self.nodes = nodes
+ self.links = links
+ }
+ }
+
+ @usableFromInline
+ var nodeIdToIndexLookup: [NodeID: Int]
+
+ public var body: some View {
+ Canvas { context, cgSize in
+ let centerX = cgSize.width / 2.0
+ let centerY = cgSize.height / 2.0
+
+ for i in model.simulation.nodePositions.indices {
+ let node = content.nodes[i]
+ let x = centerX + model.simulation.nodePositions[i].x
+ let y = centerY + model.simulation.nodePositions[i].y
+
+ let rect = CGRect(origin: .init(x: x, y: y), size: CGSize(width: 8.0, height: 8.0))
+
+ context.fill(
+ Path(ellipseIn: rect), with: .color(node.fill))
+ context.stroke(
+ Path(ellipseIn: rect), with: .color(Color(nsColor: .windowBackgroundColor)),
+ style: StrokeStyle(lineWidth: 1.5))
+ }
+ }
+ }
+
+ @inlinable
+ func paintNodes(context: inout GraphicsContext, centerX: Double, centerY: Double) {
+
+ for i in model.simulation.nodePositions.indices {
+ let node = content.nodes[i]
+ let x = centerX + model.simulation.nodePositions[i].x
+ let y = centerY + model.simulation.nodePositions[i].y
+
+ let rect = CGRect(origin: .init(x: x, y: y), size: CGSize(width: 8.0, height: 8.0))
+
+ context.fill(
+ Path(ellipseIn: rect), with: .color(node.fill))
+ context.stroke(
+ Path(ellipseIn: rect), with: .color(Color(nsColor: .windowBackgroundColor)),
+ style: StrokeStyle(lineWidth: 1.5))
+ }
+ }
+
+ @usableFromInline
+ @State var model: ForceDirectedGraph2DLayoutEngine
+ @usableFromInline var controller: ForceDirectedGraph2DController
+
+ @usableFromInline let content: Content
+ @usableFromInline let forceFieldDescriptor: [ForceDescriptor]
+
+ @inlinable
+ public init(
+ controller: ForceDirectedGraph2DController,
+ @GraphContentBuilder _ buildGraphContent: () -> PartialGraphMark,
+ @ForceFieldBuilder forceField buildForceField: () -> [ForceDescriptor]
+ ) {
+ let graphMark = buildGraphContent()
+ self.content = Content(nodes: graphMark.nodes, links: graphMark.links)
+
+ let lookup = Dictionary(
+ uniqueKeysWithValues: graphMark.nodes.enumerated().map { ($1.id, $0) })
+
+ let simulation = Simulation2D(nodeIds: Array(graphMark.nodes.indices))
+ self.forceFieldDescriptor = buildForceField()
+
+ for forceDescriptor in forceFieldDescriptor {
+ if var linkForceDescriptor = forceDescriptor as? LinkForce {
+ // inject links
+ linkForceDescriptor.links = content.links.map {
+ .init(
+ lookup[$0.id.source]!,
+ lookup[$0.id.target]!
+ )
+ }
+ linkForceDescriptor.attachToSimulation(simulation)
+ } else {
+ forceDescriptor.attachToSimulation(simulation)
+ }
+ }
+
+ self.nodeIdToIndexLookup = consume lookup
+ let model = ForceDirectedGraph2DLayoutEngine(
+ initialSimulation: consume simulation
+ )
+ controller.layoutEngine = model
+ self.model = model
+ self.controller = controller
+ }
+
+}
From 45e96e65c560210a4259aa2e9e47cc75fe5a1371 Mon Sep 17 00:00:00 2001
From: li3zhen1
Date: Sun, 5 Nov 2023 14:52:05 -0500
Subject: [PATCH 4/7] Fix complier crashing problems
---
.../ForceDirectedGraphSwiftUIExample.swift | 11 ++---
README.md | 43 +++++++++++++++++--
.../ForceDirectedGraph2DLayoutEngine.swift | 3 +-
Sources/Grape/Views/ForceDirectedGraph.swift | 6 +--
4 files changed, 50 insertions(+), 13 deletions(-)
diff --git a/Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/ForceDirectedGraphSwiftUIExample.swift b/Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/ForceDirectedGraphSwiftUIExample.swift
index 467e04f..6e6dd4f 100644
--- a/Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/ForceDirectedGraphSwiftUIExample.swift
+++ b/Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/ForceDirectedGraphSwiftUIExample.swift
@@ -13,13 +13,14 @@ import Grape
struct ForceDirectedGraphSwiftUIExample: View {
let graphController = ForceDirectedGraph2DController()
var body: some View {
+
ForceDirectedGraph(controller: graphController) {
- NodeMark(id: 0)
- for i in 1..<10 {
- NodeMark(id: i)
- }
- for i in 0..<9 {
+ NodeMark(id: 0, fill: .green)
+ NodeMark(id: 1, fill: .blue)
+ NodeMark(id: 2, fill: .yellow)
+
+ for i in 0..<2 {
LinkMark(from: i, to: i+1)
}
diff --git a/README.md b/README.md
index 02e35e4..35f2788 100644
--- a/README.md
+++ b/README.md
@@ -67,11 +67,48 @@ Source code: [ForceDirectedGraph3D/ContentView.swift](https://github.com/li3zhen
## Usage
-Grape provides 2 kinds of classes, `NDTree` and `Simulation`.
+### `Grape`
+
+`Grape` provides a SwiftUI view `ForceDirectedGraph`:
+
+```swift
+struct ForceDirectedGraphSwiftUIExample: View {
+ let graphController = ForceDirectedGraph2DController()
+ var body: some View {
+ ForceDirectedGraph(controller: graphController) {
+ // Declare nodes and links like you would do in Swift Charts.
+ NodeMark(id: 0, fill: .green)
+ NodeMark(id: 1, fill: .blue)
+ NodeMark(id: 2, fill: .yellow)
+ for i in 0..<2 {
+ LinkMark(from: i, to: i+1)
+ }
+
+ } forceField: {
+ // Declare forces like you would do in D3.js.
+ LinkForce()
+ CenterForce()
+ ManyBodyForce()
+ }
+ .onAppear {
+ // Let's start moving!
+ graphController.start()
+ }
+
+ }
+}
+```
+> [!NOTE]
+> `ForceDirectedGraph` is only a minimal working example. Please refere to the next section if you need a view that really works.
+
+
+### `ForceSimulation`
+
+`ForceSimulation` module provides 2 kinds of classes, `NDTree` and `Simulation`.
- `NDTree` is a KD-Tree data structure, which is used to accelerate the force simulation with [Barnes-Hut Approximation](https://jheer.github.io/barnes-hut/).
- `Simulation` is a force simulation class, that enables you to create any dimensional simulation with velocity Verlet integration.
-### Basic
+#### Basic
The basic concepts of simulations and forces can be found here: [Force simulations - D3](https://d3js.org/d3-force/simulation). You can simply create 2D or 3D simulations by using `Simulation2D` or `Simulation3D`:
@@ -104,7 +141,7 @@ See [Example](https://github.com/li3zhen1/Grape/tree/main/Examples/ForceDirected
-### Advanced
+#### Advanced
Grape provides a set of generic based types that works with any SIMD-like data structures. To integrate Grape into platforms where `import simd` isn't supported, or higher dimensions, you need to create a struct conforming to the `VectorLike` protocol. For ease of use, it's also recommended to add some type aliases. Here’s how you can do it:
diff --git a/Sources/Grape/Models/ForceDirectedGraph2DLayoutEngine.swift b/Sources/Grape/Models/ForceDirectedGraph2DLayoutEngine.swift
index f72ccef..f0315ec 100644
--- a/Sources/Grape/Models/ForceDirectedGraph2DLayoutEngine.swift
+++ b/Sources/Grape/Models/ForceDirectedGraph2DLayoutEngine.swift
@@ -9,7 +9,7 @@ protocol LayoutEngine {
@Observable
public class ForceDirectedGraph2DLayoutEngine: LayoutEngine {
- public var simulation: Simulation2D
+ var simulation: Simulation2D
@ObservationIgnored
let frameRate: Double = 60.0
@@ -17,7 +17,6 @@ public class ForceDirectedGraph2DLayoutEngine: LayoutEngine {
@ObservationIgnored
var scheduledTimer: Timer? = nil
- @inlinable
public init(initialSimulation: Simulation2D) {
self.simulation = initialSimulation
}
diff --git a/Sources/Grape/Views/ForceDirectedGraph.swift b/Sources/Grape/Views/ForceDirectedGraph.swift
index 8695bbc..dfd4370 100644
--- a/Sources/Grape/Views/ForceDirectedGraph.swift
+++ b/Sources/Grape/Views/ForceDirectedGraph.swift
@@ -43,7 +43,7 @@ public struct ForceDirectedGraph: View {
}
}
- @inlinable
+ // @inlinable
func paintNodes(context: inout GraphicsContext, centerX: Double, centerY: Double) {
for i in model.simulation.nodePositions.indices {
@@ -98,9 +98,9 @@ public struct ForceDirectedGraph: View {
}
}
- self.nodeIdToIndexLookup = consume lookup
+ self.nodeIdToIndexLookup = lookup
let model = ForceDirectedGraph2DLayoutEngine(
- initialSimulation: consume simulation
+ initialSimulation: simulation
)
controller.layoutEngine = model
self.model = model
From 1bafb9994a2029c4526323cd793c6110bd1a4433 Mon Sep 17 00:00:00 2001
From: li3zhen1
Date: Sun, 5 Nov 2023 14:53:33 -0500
Subject: [PATCH 5/7] Add SwiftUI Example
---
README.md | 2 -
.../Simulation2D.CenterForce.swift | 89 +++++++++----------
2 files changed, 44 insertions(+), 47 deletions(-)
diff --git a/README.md b/README.md
index 35f2788..6f620b3 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,6 @@
-
A Swift library for force simulation and graph visualization.
@@ -83,7 +82,6 @@ struct ForceDirectedGraphSwiftUIExample: View {
for i in 0..<2 {
LinkMark(from: i, to: i+1)
}
-
} forceField: {
// Declare forces like you would do in D3.js.
LinkForce()
diff --git a/Sources/ForceSimulation/Simulation2D/Simulation2D.CenterForce.swift b/Sources/ForceSimulation/Simulation2D/Simulation2D.CenterForce.swift
index ef6af29..8e99d09 100644
--- a/Sources/ForceSimulation/Simulation2D/Simulation2D.CenterForce.swift
+++ b/Sources/ForceSimulation/Simulation2D/Simulation2D.CenterForce.swift
@@ -5,63 +5,62 @@
// Created by li3zhen1 on 10/16/23.
//
-
#if canImport(simd)
-import simd
+ import simd
-extension Simulation2D {
- /// A force that drives nodes towards the center.
- ///
- /// Center force is relatively fast, the complexity is `O(n)`,
- /// where `n` is the number of nodes.
- /// See [Collide Force - D3](https://d3js.org/d3-force/collide).
- final public class CenterForce: ForceLike
- where NodeID: Hashable {
- public typealias V = simd_double2
+ extension Simulation2D {
+ /// A force that drives nodes towards the center.
+ ///
+ /// Center force is relatively fast, the complexity is `O(n)`,
+ /// where `n` is the number of nodes.
+ /// See [Collide Force - D3](https://d3js.org/d3-force/collide).
+ final public class CenterForce: ForceLike
+ where NodeID: Hashable {
+ public typealias V = simd_double2
- public var center: V
- public var strength: V.Scalar
- @usableFromInline weak var simulation: Simulation2D?
+ public var center: V
+ public var strength: V.Scalar
+ @usableFromInline weak var simulation: Simulation2D?
- @inlinable internal init(center: V, strength: V.Scalar) {
- self.center = center
- self.strength = strength
- }
+ @inlinable internal init(center: V, strength: V.Scalar) {
+ self.center = center
+ self.strength = strength
+ }
- public func apply() {
- guard let sim = self.simulation else { return }
- // let alpha = sim.alpha
+ public func apply() {
+ guard let sim = self.simulation else { return }
+ // let alpha = sim.alpha
- var meanPosition = V.zero
- for n in sim.nodePositions {
- meanPosition += n //.position
- }
- let delta = meanPosition * (self.strength / V.Scalar(sim.nodePositions.count))
+ var meanPosition = V.zero
+ for n in sim.nodePositions {
+ meanPosition += n //.position
+ }
+ let delta = meanPosition * (self.strength / V.Scalar(sim.nodePositions.count))
- for i in sim.nodePositions.indices {
- sim.nodePositions[i] -= delta
+ for i in sim.nodePositions.indices {
+ sim.nodePositions[i] -= delta
+ }
}
+
}
- }
+ /// Create a center force that drives nodes towards the center.
+ ///
+ /// Center force is relatively fast, the complexity is `O(n)`,
+ /// where `n` is the number of nodes.
+ /// See [Collide Force - D3](https://d3js.org/d3-force/collide).
+ /// - Parameters:
+ /// - center: The center of the force.
+ /// - strength: The strength of the force.
+ @discardableResult
+ public func createCenterForce(center: V, strength: V.Scalar = 0.1) -> CenterForce {
+ let f = CenterForce(center: center, strength: strength)
+ f.simulation = self
+ self.forces.append(f)
+ return f
+ }
- /// Create a center force that drives nodes towards the center.
- ///
- /// Center force is relatively fast, the complexity is `O(n)`,
- /// where `n` is the number of nodes.
- /// See [Collide Force - D3](https://d3js.org/d3-force/collide).
- /// - Parameters:
- /// - center: The center of the force.
- /// - strength: The strength of the force.
- @discardableResult
- public func createCenterForce(center: V, strength: V.Scalar = 0.1) -> CenterForce {
- let f = CenterForce(center: center, strength: strength)
- f.simulation = self
- self.forces.append(f)
- return f
}
-}
-
#endif
From 772917a5b848a3618118d3ed49f3d0acf179f25e Mon Sep 17 00:00:00 2001
From: li3zhen1
Date: Sun, 5 Nov 2023 14:55:06 -0500
Subject: [PATCH 6/7] Update Readme
---
README.md | 8 +++++---
1 file changed, 5 insertions(+), 3 deletions(-)
diff --git a/README.md b/README.md
index 6f620b3..1977f20 100644
--- a/README.md
+++ b/README.md
@@ -66,10 +66,13 @@ Source code: [ForceDirectedGraph3D/ContentView.swift](https://github.com/li3zhen
## Usage
-### `Grape`
+### `Grape` (WIP)
`Grape` provides a SwiftUI view `ForceDirectedGraph`:
+> [!IMPORTANT]
+> `ForceDirectedGraph` is only a minimal working example. Please refer to the next section to create a more complex view.
+
```swift
struct ForceDirectedGraphSwiftUIExample: View {
let graphController = ForceDirectedGraph2DController()
@@ -96,8 +99,7 @@ struct ForceDirectedGraphSwiftUIExample: View {
}
}
```
-> [!NOTE]
-> `ForceDirectedGraph` is only a minimal working example. Please refere to the next section if you need a view that really works.
+
### `ForceSimulation`
From 5fa66400e90377b11269f92a2cb45b8c8cc80393 Mon Sep 17 00:00:00 2001
From: li3zhen1
Date: Sun, 5 Nov 2023 15:02:21 -0500
Subject: [PATCH 7/7] Add Link Rendering
---
README.md | 6 +--
Sources/Grape/Views/ForceDirectedGraph.swift | 40 ++++++++++++++++----
2 files changed, 35 insertions(+), 11 deletions(-)
diff --git a/README.md b/README.md
index 1977f20..55533c9 100644
--- a/README.md
+++ b/README.md
@@ -66,15 +66,13 @@ Source code: [ForceDirectedGraph3D/ContentView.swift](https://github.com/li3zhen
## Usage
-### `Grape` (WIP)
-
-`Grape` provides a SwiftUI view `ForceDirectedGraph`:
+### `Grape`
> [!IMPORTANT]
> `ForceDirectedGraph` is only a minimal working example. Please refer to the next section to create a more complex view.
```swift
-struct ForceDirectedGraphSwiftUIExample: View {
+struct MyGraph: View {
let graphController = ForceDirectedGraph2DController()
var body: some View {
ForceDirectedGraph(controller: graphController) {
diff --git a/Sources/Grape/Views/ForceDirectedGraph.swift b/Sources/Grape/Views/ForceDirectedGraph.swift
index dfd4370..a4d87d2 100644
--- a/Sources/Grape/Views/ForceDirectedGraph.swift
+++ b/Sources/Grape/Views/ForceDirectedGraph.swift
@@ -27,18 +27,44 @@ public struct ForceDirectedGraph: View {
let centerX = cgSize.width / 2.0
let centerY = cgSize.height / 2.0
+ for i in self.content.links {
+ let source = self.nodeIdToIndexLookup[i.id.source]!
+ let target = self.nodeIdToIndexLookup[i.id.target]!
+
+ let sourceX = centerX + model.simulation.nodePositions[source].x
+ let sourceY = centerY + model.simulation.nodePositions[source].y
+ let targetX = centerX + model.simulation.nodePositions[target].x
+ let targetY = centerY + model.simulation.nodePositions[target].y
+
+ context.stroke(
+ Path { path in
+ path.move(to: CGPoint(x: sourceX, y: sourceY))
+ path.addLine(to: CGPoint(x: targetX, y: targetY))
+ },
+ with: .color(i.strokeColor),
+ style: StrokeStyle(lineWidth: i.strokeWidth)
+ )
+ }
+
for i in model.simulation.nodePositions.indices {
let node = content.nodes[i]
- let x = centerX + model.simulation.nodePositions[i].x
- let y = centerY + model.simulation.nodePositions[i].y
+ let x = centerX + model.simulation.nodePositions[i].x - node.radius
+ let y = centerY + model.simulation.nodePositions[i].y - node.radius
- let rect = CGRect(origin: .init(x: x, y: y), size: CGSize(width: 8.0, height: 8.0))
+ let rect = CGRect(
+ origin: .init(x: x, y: y),
+ size: CGSize(
+ width: node.radius * 2, height: node.radius * 2
+ )
+ )
context.fill(
Path(ellipseIn: rect), with: .color(node.fill))
- context.stroke(
- Path(ellipseIn: rect), with: .color(Color(nsColor: .windowBackgroundColor)),
- style: StrokeStyle(lineWidth: 1.5))
+ if let strokeColor = node.strokeColor {
+ context.stroke(
+ Path(ellipseIn: rect), with: .color(Color(strokeColor)),
+ style: StrokeStyle(lineWidth: node.strokeWidth))
+ }
}
}
}
@@ -97,7 +123,7 @@ public struct ForceDirectedGraph: View {
forceDescriptor.attachToSimulation(simulation)
}
}
-
+
self.nodeIdToIndexLookup = lookup
let model = ForceDirectedGraph2DLayoutEngine(
initialSimulation: simulation