Skip to content

Commit

Permalink
Merge pull request #22 from li3zhen1/swiftui
Browse files Browse the repository at this point in the history
Add minimum SwiftUI View
  • Loading branch information
li3zhen1 committed Nov 5, 2023
2 parents 26c6189 + 5fa6640 commit 15c2aa5
Show file tree
Hide file tree
Showing 15 changed files with 767 additions and 64 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand All @@ -18,6 +21,7 @@

/* Begin PBXFileReference section */
B719E4102AE5FBFC009D6C24 /* ForceDirectedLatticeView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ForceDirectedLatticeView.swift; sourceTree = "<group>"; };
B7A830CC2AF822BB00A7AF6B /* ForceDirectedGraphSwiftUIExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForceDirectedGraphSwiftUIExample.swift; sourceTree = "<group>"; };
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 = "<group>"; };
B7AFA55C2ADF4997009C7154 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
Expand All @@ -32,7 +36,9 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
B70B52AF2AF822FF00A1E6CD /* Grape in Frameworks */,
B7AFA56B2ADF49AA009C7154 /* ForceSimulation in Frameworks */,
B70B52AD2AF822FF00A1E6CD /* ForceSimulation in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -65,6 +71,7 @@
B7AFA5632ADF4999009C7154 /* ForceDirectedGraphExample.entitlements */,
B7AFA5602ADF4999009C7154 /* Preview Content */,
B7AFA56E2ADF49D6009C7154 /* Data.swift */,
B7A830CC2AF822BB00A7AF6B /* ForceDirectedGraphSwiftUIExample.swift */,
);
path = ForceDirectedGraphExample;
sourceTree = "<group>";
Expand Down Expand Up @@ -95,6 +102,8 @@
name = ForceDirectedGraphExample;
packageProductDependencies = (
B7AFA56A2ADF49AA009C7154 /* ForceSimulation */,
B70B52AC2AF822FF00A1E6CD /* ForceSimulation */,
B70B52AE2AF822FF00A1E6CD /* Grape */,
);
productName = ForceDirectedGraphExample;
productReference = B7AFA5572ADF4997009C7154 /* ForceDirectedGraphExample.app */;
Expand Down Expand Up @@ -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 */,
);
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ struct ForceDirectedGraphExampleApp: App {
var body: some Scene {
WindowGroup {
// ContentView().padding(0)
ForceDirectedLatticeView().padding(0)//.ignoresSafeArea()
// ForceDirectedLatticeView().padding(0)//.ignoresSafeArea()
ForceDirectedGraphSwiftUIExample()

}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
//
// ForceDirectedGraphSwiftUIExample.swift
// ForceDirectedGraphExample
//
// Created by li3zhen1 on 11/5/23.
//

import Foundation
import SwiftUI
import Grape


struct ForceDirectedGraphSwiftUIExample: View {
let graphController = ForceDirectedGraph2DController<Int>()
var body: some View {

ForceDirectedGraph(controller: graphController) {

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: {
LinkForce()
CenterForce()
ManyBodyForce()
}
.onAppear {
graphController.start()
}

}
}
39 changes: 25 additions & 14 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
// 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

let package = Package(
name: "Grape",
platforms: [
.macOS(.v11),
.iOS(.v14),
.watchOS(.v7),
.macOS(.v14),
.iOS(.v17),
.watchOS(.v9),
],

products: [
Expand All @@ -24,6 +24,11 @@ let package = Package(
targets: ["ForceSimulation"]
),

.library(
name: "Grape",
targets: ["Grape"]
),

],

dependencies: [
Expand All @@ -33,6 +38,12 @@ let package = Package(

targets: [

.target(
name: "Grape",
dependencies: ["ForceSimulation"],
path: "Sources/Grape"
),

// .target(
// name: "NDTree",
// path: "Sources/NDTree",
Expand All @@ -43,7 +54,7 @@ let package = Package(
// // "-whole-module-optimization",
// // "-Ounchecked",
// ]),

// ]
// ),

Expand All @@ -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(
Expand All @@ -83,6 +94,6 @@ let package = Package(
// // "-Ounchecked",
// ])
// ]
),
),
]
)
43 changes: 39 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
<img src="https://github.com/li3zhen1/Grape/actions/workflows/swift.yml/badge.svg" alt="swift workflow">
<a href="https://swiftpackageindex.com/li3zhen1/Grape"><img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fli3zhen1%2FGrape%2Fbadge%3Ftype%3Dswift-versions" alt="swift package index"></a>
<a href="https://swiftpackageindex.com/li3zhen1/Grape"><img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fli3zhen1%2FGrape%2Fbadge%3Ftype%3Dplatforms" alt="swift package index"></a>

</p>

<p align="center">A Swift library for force simulation and graph visualization.
Expand Down Expand Up @@ -67,11 +66,47 @@ Source code: [ForceDirectedGraph3D/ContentView.swift](https://github.com/li3zhen

## Usage

Grape provides 2 kinds of classes, `NDTree` and `Simulation`.
### `Grape`

> [!IMPORTANT]
> `ForceDirectedGraph` is only a minimal working example. Please refer to the next section to create a more complex view.
```swift
struct MyGraph: View {
let graphController = ForceDirectedGraph2DController<Int>()
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()
}

}
}
```



### `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`:

Expand Down Expand Up @@ -104,7 +139,7 @@ See [Example](https://github.com/li3zhen1/Grape/tree/main/Examples/ForceDirected

<br/>

### 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:

Expand Down
89 changes: 44 additions & 45 deletions Sources/ForceSimulation/Simulation2D/Simulation2D.CenterForce.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<NodeID>?
public var center: V
public var strength: V.Scalar
@usableFromInline weak var simulation: Simulation2D<NodeID>?

@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
Loading

0 comments on commit 15c2aa5

Please sign in to comment.