Skip to content

Commit d57627c

Browse files
authored
Merge pull request #62 from carlo-/main
- Fix `findNode` method by scaling node radius with viewport transform - Unify size calculation for marks with and without specified symbolShape
2 parents 045ee7f + d775fe6 commit d57627c

File tree

8 files changed

+87
-104
lines changed

8 files changed

+87
-104
lines changed

Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/Lattice.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ struct Lattice: View {
4040
Series(0..<(width*width)) { i in
4141
let _i = Double(i / width) / Double(width)
4242
let _j = Double(i % width) / Double(width)
43-
NodeMark(id: i, radius: 3.0)
43+
NodeMark(id: i)
4444
.foregroundStyle(Color(red: 1, green: _i, blue: _j))
4545
.stroke()
4646
}

Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/MyRing.swift

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,27 +19,31 @@ struct MyRing: View {
1919
ForceDirectedGraph(states: graphStates) {
2020
Series(0..<20) { i in
2121
NodeMark(id: 3 * i + 0)
22-
.symbol(.circle)
23-
.symbolSize(radius:4.0)
22+
.symbolSize(radius: 6.0)
2423
.foregroundStyle(.green)
24+
.stroke(.clear)
2525
NodeMark(id: 3 * i + 1)
2626
.symbol(.pentagon)
27-
.symbolSize(radius:5.0)
27+
.symbolSize(radius:10)
2828
.foregroundStyle(.blue)
29+
.stroke(.clear)
2930
NodeMark(id: 3 * i + 2)
3031
.symbol(.circle)
3132
.symbolSize(radius:6.0)
3233
.foregroundStyle(.yellow)
34+
.stroke(.clear)
3335

3436
LinkMark(from: 3 * i + 0, to: 3 * i + 1)
3537
LinkMark(from: 3 * i + 1, to: 3 * i + 2)
3638

3739
LinkMark(from: 3 * i + 0, to: 3 * ((i + 1) % 20) + 0)
3840
LinkMark(from: 3 * i + 1, to: 3 * ((i + 1) % 20) + 1)
3941
LinkMark(from: 3 * i + 2, to: 3 * ((i + 1) % 20) + 2)
40-
.stroke(.black, StrokeStyle(lineWidth: 2.0, lineCap: .round, lineJoin: .round))
42+
4143

4244
}
45+
.stroke(.black, StrokeStyle(lineWidth: 1.5, lineCap: .round, lineJoin: .round))
46+
4347
} force: {
4448
ManyBodyForce(strength: -15)
4549
LinkForce(

Sources/Grape/Contents/NodeMark.swift

Lines changed: 5 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,15 @@
11
import SwiftUI
22
import simd
33

4-
public struct NodeMark<NodeID: Hashable>: GraphContent & Identifiable {
5-
6-
// public enum LabelDisplayStrategy {
7-
// case auto
8-
// case specified(Bool)
9-
// case byPageRank((Double) -> Bool)
10-
// }
11-
12-
// public enum LabelPositioning {
13-
// case bottomOfMark
14-
// case topOfMark
15-
// case startAfterMark
16-
// case endBeforeMark
17-
// }
4+
public struct NodeMark<NodeID: Hashable>: GraphContent, Identifiable, Equatable {
185

196
public var id: NodeID
207

21-
// public var fill: Color
22-
// public var strokeColor: Color?
23-
// public var strokeWidth: Double
24-
public var radius: Double
25-
// public var label: String?
26-
// public var labelColor: Color
27-
// public var labelDisplayStrategy: LabelDisplayStrategy
28-
// public var labelPositioning: LabelPositioning
298
@inlinable
309
public init(
31-
id: NodeID,
32-
radius: Double = 4.0
10+
id: NodeID
3311
) {
3412
self.id = id
35-
self.radius = radius
3613
}
3714

3815
@inlinable
@@ -42,11 +19,11 @@ public struct NodeMark<NodeID: Hashable>: GraphContent & Identifiable {
4219
self,
4320
context.states.currentShading,
4421
context.states.currentStroke,
45-
context.states.currentSymbolShape
22+
context.states.currentSymbolShapeOrSize
4623
)
4724
)
4825
context.states.currentID = .node(id)
49-
context.nodeRadiusSquaredLookup[id] = simd_length_squared(
26+
context.nodeHitSizeAreaLookup[id] = simd_length_squared(
5027
context.states.currentSymbolSizeOrDefault.simd)
5128
}
5229
}
@@ -56,11 +33,4 @@ extension NodeMark: CustomDebugStringConvertible {
5633
public var debugDescription: String {
5734
return "Node(id: \(id))"
5835
}
59-
}
60-
61-
extension NodeMark: Equatable {
62-
@inlinable
63-
public static func == (lhs: Self, rhs: Self) -> Bool {
64-
return lhs.id == rhs.id && lhs.radius == rhs.radius
65-
}
66-
}
36+
}

Sources/Grape/Views/ForceDirectedGraphModel.findNode.swift

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,30 @@ extension ForceDirectedGraphModel {
77
internal func findNode(
88
at locationInSimulationCoordinate: SIMD2<Double>
99
) -> NodeID? {
10+
11+
let viewportScale = self.finalTransform.scale
12+
1013
for i in simulationContext.storage.kinetics.range.reversed() {
1114
let iNodeID = simulationContext.nodeIndices[i]
1215
guard
13-
let iRadius2 = graphRenderingContext.nodeRadiusSquaredLookup[
16+
let iRadius2 = graphRenderingContext.nodeHitSizeAreaLookup[
1417
simulationContext.nodeIndices[i]
1518
]
1619
else { continue }
1720
let iPos = simulationContext.storage.kinetics.position[i]
1821

19-
20-
if simd_length_squared(locationInSimulationCoordinate - iPos) <= iRadius2
21-
{
22+
/// https://github.com/li3zhen1/Grape/pull/62#issue-2753932460
23+
///
24+
/// ```swift
25+
/// let actualRadius = pow((iRadius2 * 0.5), 0.5) * 0.5
26+
/// let scaledRadius = actualRadius / max(.ulpOfOne, viewportScale)
27+
/// let scaledRadius2 = pow(scaledRadius, 2.0)
28+
/// ```
29+
///
30+
let scaledRadius2 = iRadius2 / max(.ulpOfOne, (8.0 * viewportScale * viewportScale))
31+
let length2 = simd_length_squared(locationInSimulationCoordinate - iPos)
32+
33+
if length2 <= scaledRadius2 {
2234
return iNodeID
2335
}
2436
}

Sources/Grape/Views/ForceDirectedGraphModel.swift

Lines changed: 37 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ public final class ForceDirectedGraphModel<Content: GraphContent> {
116116
let ticksPerSecond: Double
117117

118118
@usableFromInline
119-
// @MainActor
119+
// @MainActor
120120
var scheduledTimer: Timer? = nil
121121

122122
@usableFromInline
@@ -360,8 +360,10 @@ extension ForceDirectedGraphModel {
360360
let p =
361361
if let pathBuilder = op.path {
362362
{
363-
let sourceNodeRadius = sqrt(graphRenderingContext.nodeRadiusSquaredLookup[op.mark.id.source] ?? 0) / 2
364-
let targetNodeRadius = sqrt(graphRenderingContext.nodeRadiusSquaredLookup[op.mark.id.target] ?? 0) / 2
363+
let sourceNodeRadius =
364+
sqrt(graphRenderingContext.nodeHitSizeAreaLookup[op.mark.id.source] ?? 0) / 2
365+
let targetNodeRadius =
366+
sqrt(graphRenderingContext.nodeHitSizeAreaLookup[op.mark.id.target] ?? 0) / 2
365367
let angle = atan2(targetPos.y - sourcePos.y, targetPos.x - sourcePos.x)
366368
let sourceOffset = SIMD2<Double>(
367369
cos(angle) * sourceNodeRadius, sin(angle) * sourceNodeRadius
@@ -405,60 +407,41 @@ extension ForceDirectedGraphModel {
405407
continue
406408
}
407409
let pos = viewportPositions[id]
408-
if let path = op.path {
409-
graphicsContext.transform = .init(translationX: pos.x, y: pos.y)
410-
graphicsContext.fill(
411-
path,
412-
with: op.fill ?? .defaultNodeShading
413-
)
414-
if let strokeEffect = op.stroke {
415-
switch strokeEffect.color {
416-
case .color(let color):
417-
graphicsContext.stroke(
418-
path,
419-
with: .color(color),
420-
style: strokeEffect.style ?? .defaultLinkStyle
421-
)
422-
case .clip:
423-
graphicsContext.blendMode = .clear
424-
graphicsContext.stroke(
425-
path,
426-
with: .color(.black),
427-
style: strokeEffect.style ?? .defaultLinkStyle
410+
411+
graphicsContext.transform = .init(translationX: pos.x, y: pos.y)
412+
413+
let finalizedPath: Path =
414+
switch op.pathOrSymbolSize {
415+
case .path(let path): path
416+
case .symbolSize(let size):
417+
Path(
418+
ellipseIn: CGRect(
419+
origin: CGPoint(x: -size.width / 2, y: -size.height / 2),
420+
size: size
428421
)
429-
graphicsContext.blendMode = .normal
430-
}
431-
}
432-
} else {
433-
graphicsContext.transform = .identity
434-
let rect = CGRect(
435-
origin: (pos - op.mark.radius).cgPoint,
436-
size: CGSize(
437-
width: op.mark.radius * 2, height: op.mark.radius * 2
438422
)
439-
)
440-
graphicsContext.fill(
441-
Path(ellipseIn: rect),
442-
with: op.fill ?? .defaultNodeShading
443-
)
423+
}
444424

445-
if let strokeEffect = op.stroke {
446-
switch strokeEffect.color {
447-
case .color(let color):
448-
graphicsContext.stroke(
449-
Path(ellipseIn: rect),
450-
with: .color(color),
451-
style: strokeEffect.style ?? .defaultLinkStyle
452-
)
453-
case .clip:
454-
graphicsContext.blendMode = .clear
455-
graphicsContext.stroke(
456-
Path(ellipseIn: rect),
457-
with: .color(.black),
458-
style: strokeEffect.style ?? .defaultLinkStyle
459-
)
460-
graphicsContext.blendMode = .normal
461-
}
425+
graphicsContext.fill(
426+
finalizedPath,
427+
with: op.fill ?? .defaultNodeShading
428+
)
429+
if let strokeEffect = op.stroke {
430+
switch strokeEffect.color {
431+
case .color(let color):
432+
graphicsContext.stroke(
433+
finalizedPath,
434+
with: .color(color),
435+
style: strokeEffect.style ?? .defaultLinkStyle
436+
)
437+
case .clip:
438+
graphicsContext.blendMode = .clear
439+
graphicsContext.stroke(
440+
finalizedPath,
441+
with: .color(.black),
442+
style: strokeEffect.style ?? .defaultLinkStyle
443+
)
444+
graphicsContext.blendMode = .normal
462445
}
463446
}
464447
}

Sources/Grape/Views/GraphRenderingContext.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,9 @@ public struct _GraphRenderingContext<NodeID: Hashable> {
2424
@usableFromInline
2525
internal var nodeOperations: [RenderOperation<NodeID>.Node] = []
2626

27+
/// A lookup table for the hit area of each node (width * height).
2728
@usableFromInline
28-
internal var nodeRadiusSquaredLookup: [NodeID: Double] = [:]
29+
internal var nodeHitSizeAreaLookup: [NodeID: Double] = [:]
2930

3031
@usableFromInline
3132
internal var linkOperations: [RenderOperation<NodeID>.Link] = []

Sources/Grape/Views/GraphRenderingStates.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,13 @@ internal struct GraphRenderingStates<NodeID: Hashable> {
3434
var symbolShape: [Path] = []
3535

3636
@inlinable
37-
var currentSymbolShape: Path? { symbolShape.last }
37+
var currentSymbolShapeOrSize: PathOrSymbolSize {
38+
if let shape = symbolShape.last {
39+
return .path(shape)
40+
} else {
41+
return .symbolSize(currentSymbolSizeOrDefault)
42+
}
43+
}
3844

3945
@usableFromInline
4046
var symbolSize: [CGSize] = []

Sources/Grape/Views/RenderOperation.swift

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
import SwiftUI
22

3+
@usableFromInline
4+
enum PathOrSymbolSize {
5+
case path(Path)
6+
case symbolSize(CGSize)
7+
}
8+
39
@usableFromInline
410
internal enum RenderOperation<NodeID: Hashable> {
11+
512
@usableFromInline
613
struct Node {
714
@usableFromInline
@@ -11,19 +18,19 @@ internal enum RenderOperation<NodeID: Hashable> {
1118
@usableFromInline
1219
let stroke: GraphContentEffect.Stroke?
1320
@usableFromInline
14-
let path: Path?
21+
let pathOrSymbolSize: PathOrSymbolSize
1522

1623
@inlinable
1724
init(
1825
_ mark: NodeMark<NodeID>,
1926
_ fill: GraphicsContext.Shading?,
2027
_ stroke: GraphContentEffect.Stroke?,
21-
_ path: Path?
28+
_ pathOrSymbolSize: PathOrSymbolSize
2229
) {
2330
self.mark = mark
2431
self.fill = fill
2532
self.stroke = stroke
26-
self.path = path
33+
self.pathOrSymbolSize = pathOrSymbolSize
2734
}
2835
}
2936

@@ -53,7 +60,7 @@ extension RenderOperation.Node: Equatable {
5360
@inlinable
5461
internal static func == (lhs: Self, rhs: Self) -> Bool {
5562
let fillEq = lhs.fill == nil && rhs.fill == nil
56-
let pathEq = lhs.path == nil && rhs.path == nil
63+
let pathEq = lhs.pathOrSymbolSize == nil && rhs.pathOrSymbolSize == nil
5764
return lhs.mark == rhs.mark
5865
&& fillEq
5966
&& lhs.stroke == rhs.stroke

0 commit comments

Comments
 (0)