Skip to content

Commit 632d86c

Browse files
authored
Merge pull request #65 from li3zhen1/GraphProxy
Implement GraphProxy for tap, drag, magnify gestures
2 parents 9498614 + fad72ac commit 632d86c

File tree

11 files changed

+462
-298
lines changed

11 files changed

+462
-298
lines changed

Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/MermaidVisualization.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ struct MermaidVisualization: View {
107107
}
108108
.graphOverlay(content: { proxy in
109109
Rectangle().fill(.clear).contentShape(Rectangle())
110+
.withGraphDragGesture(proxy)
110111
.onTapGesture { value in
111112
if let nodeID = proxy.locateNode(at: .init(x: value.x, y: value.y)) {
112113
guard let nodeID = nodeID as? String else { return }

Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/MyRing.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,24 @@ struct MyRing: View {
5353
CenterForce()
5454
CollideForce()
5555
}
56+
.graphOverlay { proxy in
57+
Rectangle().fill(.clear).contentShape(Rectangle())
58+
.withGraphDragGesture(proxy, action: describe)
59+
.withGraphMagnifyGesture(proxy)
60+
}
5661
.toolbar {
5762
GraphStateToggle(graphStates: graphStates)
5863
}
5964
}
65+
66+
func describe(_ state: GraphDragState?) {
67+
switch state {
68+
case .node(let anyHashable):
69+
print("Dragging \(anyHashable as! Int)")
70+
case .background(let start):
71+
print("Dragging \(start)")
72+
case nil:
73+
print("Drag ended")
74+
}
75+
}
6076
}

Sources/ForceSimulation/Kinetics.swift

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,44 +7,48 @@ where Vector: SimulatableVector & L2NormCalculatable {
77
///
88
/// Ordered as the nodeIds you passed in when initializing simulation.
99
/// They are always updated.
10-
public var position: UnsafeArray<Vector>
10+
@usableFromInline
11+
package var position: UnsafeArray<Vector>
1112

1213
// public var positionBufferPointer: UnsafeMutablePointer<Vector>
1314

1415
/// The velocities of points stored in simulation.
1516
///
1617
/// Ordered as the nodeIds you passed in when initializing simulation.
1718
/// They are always updated.
18-
public var velocity: UnsafeArray<Vector>
19+
@usableFromInline
20+
package var velocity: UnsafeArray<Vector>
1921

2022
// public var velocityBufferPointer: UnsafeMutablePointer<Vector>
2123

2224
/// The fixed positions of points stored in simulation.
2325
///
2426
/// Ordered as the nodeIds you passed in when initializing simulation.
2527
/// They are always updated.
26-
public var fixation: UnsafeArray<Vector?>
28+
@usableFromInline
29+
package var fixation: UnsafeArray<Vector?>
2730

2831

2932
public var validCount: Int
3033
public var alpha: Vector.Scalar
3134
public let alphaMin: Vector.Scalar
3235
public let alphaDecay: Vector.Scalar
3336
public let alphaTarget: Vector.Scalar
34-
3537
public let velocityDecay: Vector.Scalar
3638

3739
@usableFromInline
3840
var randomGenerator: Vector.Scalar.Generator
3941

4042

41-
public let links: [EdgeID<Int>]
43+
44+
@usableFromInline
45+
package let links: [EdgeID<Int>]
4246

4347
// public var validRanges: [Range<Int>]
4448
// public var validRanges: Range<Int>
4549

4650
@inlinable
47-
public var range: Range<Int> {
51+
package var range: Range<Int> {
4852
return 0..<validCount
4953
}
5054

@@ -77,6 +81,21 @@ where Vector: SimulatableVector & L2NormCalculatable {
7781
self.randomGenerator = .init()
7882
}
7983

84+
@inlinable
85+
package static var empty: Kinetics<Vector> {
86+
Kinetics(
87+
links: [],
88+
initialAlpha: 0,
89+
alphaMin: 0,
90+
alphaDecay: 0,
91+
alphaTarget: 0,
92+
velocityDecay: 0,
93+
position: [],
94+
velocity: [],
95+
fixation: []
96+
)
97+
}
98+
8099
}
81100

82101
extension Kinetics {
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import ForceSimulation
2+
import SwiftUI
3+
4+
public enum GraphDragState {
5+
case node(AnyHashable)
6+
case background(start: SIMD2<Double>)
7+
}
8+
9+
#if !os(tvOS)
10+
11+
@usableFromInline
12+
struct GraphDragModifier: ViewModifier {
13+
14+
@inlinable
15+
public var dragGesture: some Gesture {
16+
DragGesture(
17+
minimumDistance: Self.minimumDragDistance,
18+
coordinateSpace: .local
19+
)
20+
.onChanged(onChanged)
21+
.onEnded(onEnded)
22+
}
23+
24+
@inlinable
25+
public func body(content: Content) -> some View {
26+
content.gesture(dragGesture)
27+
}
28+
29+
@inlinable
30+
@State
31+
public var dragState: GraphDragState?
32+
33+
@usableFromInline
34+
let graphProxy: GraphProxy
35+
36+
@usableFromInline
37+
let action: ((GraphDragState?) -> Void)?
38+
39+
@inlinable
40+
init(
41+
graphProxy: GraphProxy,
42+
action: ((GraphDragState?) -> Void)? = nil
43+
) {
44+
self.graphProxy = graphProxy
45+
self.action = action
46+
}
47+
48+
@inlinable
49+
static var minimumDragDistance: CGFloat { 3.0 }
50+
51+
@inlinable
52+
static var minimumAlphaAfterDrag: CGFloat { 0.5 }
53+
54+
@inlinable
55+
public func onEnded(
56+
value: DragGesture.Value
57+
) {
58+
if dragState != nil {
59+
switch dragState {
60+
case .node(let nodeID):
61+
graphProxy.setNodeFixation(nodeID: nodeID, fixation: nil)
62+
case .background(let start):
63+
let delta = value.location.simd - start
64+
graphProxy.modelTransform.translate += delta
65+
dragState = .background(start: value.location.simd)
66+
case .none:
67+
break
68+
}
69+
dragState = .none
70+
}
71+
72+
if let action {
73+
action(dragState)
74+
}
75+
}
76+
77+
@inlinable
78+
public func onChanged(
79+
value: DragGesture.Value
80+
) {
81+
if dragState == nil {
82+
if let nodeID = graphProxy.locateNode(at: value.startLocation) {
83+
dragState = .node(nodeID)
84+
graphProxy.setNodeFixation(nodeID: nodeID, fixation: value.startLocation)
85+
} else {
86+
dragState = .background(start: value.location.simd)
87+
}
88+
} else {
89+
switch dragState {
90+
case .node(let nodeID):
91+
graphProxy.setNodeFixation(nodeID: nodeID, fixation: value.location)
92+
case .background(let start):
93+
let delta = value.location.simd - start
94+
graphProxy.modelTransform.translate += delta
95+
dragState = .background(start: value.location.simd)
96+
case .none:
97+
break
98+
}
99+
}
100+
101+
if let action {
102+
action(dragState)
103+
}
104+
}
105+
}
106+
107+
extension View {
108+
@inlinable
109+
public func withGraphDragGesture(
110+
_ proxy: GraphProxy,
111+
action: ((GraphDragState?) -> Void)? = nil
112+
) -> some View {
113+
self.modifier(GraphDragModifier(graphProxy: proxy, action: action))
114+
}
115+
}
116+
117+
#endif
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import SwiftUI
2+
3+
#if os(iOS) || os(macOS)
4+
public struct GraphMagnifyModifier: ViewModifier {
5+
6+
@usableFromInline
7+
let proxy: GraphProxy
8+
9+
@usableFromInline
10+
let action: (() -> Void)?
11+
12+
@inlinable
13+
init(_ proxy: GraphProxy, action: (() -> Void)? = nil) {
14+
self.proxy = proxy
15+
self.action = action
16+
}
17+
18+
@inlinable
19+
public func body(content: Content) -> some View {
20+
content.gesture(gesture)
21+
}
22+
23+
@inlinable
24+
public var gesture: some Gesture {
25+
MagnifyGesture(minimumScaleDelta: Self.minimumScaleDelta)
26+
.onChanged(onMagnifyChange)
27+
.onEnded(onMagnifyEnd)
28+
}
29+
30+
@inlinable
31+
static var minimumScaleDelta: CGFloat { 0.001 }
32+
33+
@inlinable
34+
static var minimumScale: CGFloat { 1e-2 }
35+
36+
@inlinable
37+
static var maximumScale: CGFloat { .infinity }
38+
39+
@inlinable
40+
static var magnificationDecay: CGFloat { 0.1 }
41+
42+
@inlinable
43+
internal func clamp(
44+
_ value: CGFloat,
45+
min: CGFloat,
46+
max: CGFloat
47+
) -> CGFloat {
48+
Swift.min(Swift.max(value, min), max)
49+
}
50+
51+
@inlinable
52+
internal func onMagnifyChange(
53+
_ value: MagnifyGesture.Value
54+
) {
55+
var startTransform: ViewportTransform
56+
if let t = self.proxy.lastTransformRecord {
57+
startTransform = t
58+
} else {
59+
self.proxy.lastTransformRecord = self.proxy.modelTransform
60+
startTransform = self.proxy.modelTransform
61+
}
62+
63+
let alpha = (startTransform.translate(by: self.proxy.obsoleteState.cgSize.simd / 2))
64+
.invert(value.startLocation.simd)
65+
66+
let newScale = clamp(
67+
value.magnification * startTransform.scale,
68+
min: Self.minimumScale,
69+
max: Self.maximumScale)
70+
71+
let newTranslate = (startTransform.scale - newScale) * alpha + startTransform.translate
72+
73+
let newModelTransform = ViewportTransform(
74+
translate: newTranslate,
75+
scale: newScale
76+
)
77+
self.proxy.modelTransform = newModelTransform
78+
79+
// guard let action = self.proxy._onGraphMagnified else { return }
80+
// action()
81+
}
82+
83+
@inlinable
84+
internal func onMagnifyEnd(
85+
_ value: MagnifyGesture.Value
86+
) {
87+
var startTransform: ViewportTransform
88+
if let t = self.proxy.lastTransformRecord {
89+
startTransform = t
90+
} else {
91+
self.proxy.lastTransformRecord = self.proxy.modelTransform
92+
startTransform = self.proxy.modelTransform
93+
}
94+
95+
let alpha = (startTransform.translate(by: self.proxy.obsoleteState.cgSize.simd / 2))
96+
.invert(value.startLocation.simd)
97+
98+
let newScale = clamp(
99+
value.magnification * startTransform.scale,
100+
min: Self.minimumScale,
101+
max: Self.maximumScale)
102+
103+
let newTranslate = (startTransform.scale - newScale) * alpha + startTransform.translate
104+
let newModelTransform = ViewportTransform(
105+
translate: newTranslate,
106+
scale: newScale
107+
)
108+
self.proxy.lastTransformRecord = nil
109+
self.proxy.modelTransform = newModelTransform
110+
111+
if let action {
112+
action()
113+
}
114+
}
115+
}
116+
117+
extension View {
118+
@inlinable
119+
public func withGraphMagnifyGesture(
120+
_ proxy: GraphProxy,
121+
action: (() -> Void)? = nil
122+
) -> some View {
123+
self.modifier(GraphMagnifyModifier(proxy, action: action))
124+
}
125+
}
126+
127+
#endif
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import SwiftUI
2+
3+
extension View {
4+
@inlinable
5+
public func withGraphTapGesture(
6+
_ proxy: GraphProxy,
7+
action: @escaping (AnyHashable) -> Void
8+
) -> some View {
9+
self.onTapGesture { value in
10+
if let nodeID = proxy.locateNode(at: .init(x: value.x, y: value.y)) {
11+
action(nodeID)
12+
}
13+
}
14+
}
15+
}

Sources/Grape/Grape.docc/Documentation.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,13 @@ Grape supports localization features. You can localize the labels in the graph v
3838
* ``Series``
3939
* ``GraphComponent``
4040

41-
41+
### Adding interactivity
42+
* ``GraphProxy``
43+
* ``SwiftUICore/View/graphOverlay(alignment:content:)``
44+
* ``SwiftUICore/View/graphBackground(alignment:content:)``
45+
* ``SwiftUICore/View/withGraphTapGesture(_:action:)``
46+
* ``SwiftUICore/View/withGraphDragGesture(_:action:)``
47+
* ``SwiftUICore/View/withGraphMagnifyGesture(_:action:)``
4248

4349
### Managing the view state
4450

0 commit comments

Comments
 (0)