Skip to content

Commit

Permalink
Merge pull request #62 from carlo-/main
Browse files Browse the repository at this point in the history
- Fix `findNode` method by scaling node radius with viewport transform
- Unify size calculation for marks with and without specified symbolShape
  • Loading branch information
li3zhen1 authored Dec 29, 2024
2 parents 045ee7f + d775fe6 commit d57627c
Show file tree
Hide file tree
Showing 8 changed files with 87 additions and 104 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ struct Lattice: View {
Series(0..<(width*width)) { i in
let _i = Double(i / width) / Double(width)
let _j = Double(i % width) / Double(width)
NodeMark(id: i, radius: 3.0)
NodeMark(id: i)
.foregroundStyle(Color(red: 1, green: _i, blue: _j))
.stroke()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,27 +19,31 @@ struct MyRing: View {
ForceDirectedGraph(states: graphStates) {
Series(0..<20) { i in
NodeMark(id: 3 * i + 0)
.symbol(.circle)
.symbolSize(radius:4.0)
.symbolSize(radius: 6.0)
.foregroundStyle(.green)
.stroke(.clear)
NodeMark(id: 3 * i + 1)
.symbol(.pentagon)
.symbolSize(radius:5.0)
.symbolSize(radius:10)
.foregroundStyle(.blue)
.stroke(.clear)
NodeMark(id: 3 * i + 2)
.symbol(.circle)
.symbolSize(radius:6.0)
.foregroundStyle(.yellow)
.stroke(.clear)

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

LinkMark(from: 3 * i + 0, to: 3 * ((i + 1) % 20) + 0)
LinkMark(from: 3 * i + 1, to: 3 * ((i + 1) % 20) + 1)
LinkMark(from: 3 * i + 2, to: 3 * ((i + 1) % 20) + 2)
.stroke(.black, StrokeStyle(lineWidth: 2.0, lineCap: .round, lineJoin: .round))


}
.stroke(.black, StrokeStyle(lineWidth: 1.5, lineCap: .round, lineJoin: .round))

} force: {
ManyBodyForce(strength: -15)
LinkForce(
Expand Down
40 changes: 5 additions & 35 deletions Sources/Grape/Contents/NodeMark.swift
Original file line number Diff line number Diff line change
@@ -1,38 +1,15 @@
import SwiftUI
import simd

public struct NodeMark<NodeID: Hashable>: GraphContent & Identifiable {

// public enum LabelDisplayStrategy {
// case auto
// case specified(Bool)
// case byPageRank((Double) -> Bool)
// }

// public enum LabelPositioning {
// case bottomOfMark
// case topOfMark
// case startAfterMark
// case endBeforeMark
// }
public struct NodeMark<NodeID: Hashable>: GraphContent, Identifiable, Equatable {

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
@inlinable
public init(
id: NodeID,
radius: Double = 4.0
id: NodeID
) {
self.id = id
self.radius = radius
}

@inlinable
Expand All @@ -42,11 +19,11 @@ public struct NodeMark<NodeID: Hashable>: GraphContent & Identifiable {
self,
context.states.currentShading,
context.states.currentStroke,
context.states.currentSymbolShape
context.states.currentSymbolShapeOrSize
)
)
context.states.currentID = .node(id)
context.nodeRadiusSquaredLookup[id] = simd_length_squared(
context.nodeHitSizeAreaLookup[id] = simd_length_squared(
context.states.currentSymbolSizeOrDefault.simd)
}
}
Expand All @@ -56,11 +33,4 @@ extension NodeMark: CustomDebugStringConvertible {
public var debugDescription: String {
return "Node(id: \(id))"
}
}

extension NodeMark: Equatable {
@inlinable
public static func == (lhs: Self, rhs: Self) -> Bool {
return lhs.id == rhs.id && lhs.radius == rhs.radius
}
}
}
20 changes: 16 additions & 4 deletions Sources/Grape/Views/ForceDirectedGraphModel.findNode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,30 @@ extension ForceDirectedGraphModel {
internal func findNode(
at locationInSimulationCoordinate: SIMD2<Double>
) -> NodeID? {

let viewportScale = self.finalTransform.scale

for i in simulationContext.storage.kinetics.range.reversed() {
let iNodeID = simulationContext.nodeIndices[i]
guard
let iRadius2 = graphRenderingContext.nodeRadiusSquaredLookup[
let iRadius2 = graphRenderingContext.nodeHitSizeAreaLookup[
simulationContext.nodeIndices[i]
]
else { continue }
let iPos = simulationContext.storage.kinetics.position[i]


if simd_length_squared(locationInSimulationCoordinate - iPos) <= iRadius2
{
/// https://github.com/li3zhen1/Grape/pull/62#issue-2753932460
///
/// ```swift
/// let actualRadius = pow((iRadius2 * 0.5), 0.5) * 0.5
/// let scaledRadius = actualRadius / max(.ulpOfOne, viewportScale)
/// let scaledRadius2 = pow(scaledRadius, 2.0)
/// ```
///
let scaledRadius2 = iRadius2 / max(.ulpOfOne, (8.0 * viewportScale * viewportScale))
let length2 = simd_length_squared(locationInSimulationCoordinate - iPos)

if length2 <= scaledRadius2 {
return iNodeID
}
}
Expand Down
91 changes: 37 additions & 54 deletions Sources/Grape/Views/ForceDirectedGraphModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ public final class ForceDirectedGraphModel<Content: GraphContent> {
let ticksPerSecond: Double

@usableFromInline
// @MainActor
// @MainActor
var scheduledTimer: Timer? = nil

@usableFromInline
Expand Down Expand Up @@ -360,8 +360,10 @@ extension ForceDirectedGraphModel {
let p =
if let pathBuilder = op.path {
{
let sourceNodeRadius = sqrt(graphRenderingContext.nodeRadiusSquaredLookup[op.mark.id.source] ?? 0) / 2
let targetNodeRadius = sqrt(graphRenderingContext.nodeRadiusSquaredLookup[op.mark.id.target] ?? 0) / 2
let sourceNodeRadius =
sqrt(graphRenderingContext.nodeHitSizeAreaLookup[op.mark.id.source] ?? 0) / 2
let targetNodeRadius =
sqrt(graphRenderingContext.nodeHitSizeAreaLookup[op.mark.id.target] ?? 0) / 2
let angle = atan2(targetPos.y - sourcePos.y, targetPos.x - sourcePos.x)
let sourceOffset = SIMD2<Double>(
cos(angle) * sourceNodeRadius, sin(angle) * sourceNodeRadius
Expand Down Expand Up @@ -405,60 +407,41 @@ extension ForceDirectedGraphModel {
continue
}
let pos = viewportPositions[id]
if let path = op.path {
graphicsContext.transform = .init(translationX: pos.x, y: pos.y)
graphicsContext.fill(
path,
with: op.fill ?? .defaultNodeShading
)
if let strokeEffect = op.stroke {
switch strokeEffect.color {
case .color(let color):
graphicsContext.stroke(
path,
with: .color(color),
style: strokeEffect.style ?? .defaultLinkStyle
)
case .clip:
graphicsContext.blendMode = .clear
graphicsContext.stroke(
path,
with: .color(.black),
style: strokeEffect.style ?? .defaultLinkStyle

graphicsContext.transform = .init(translationX: pos.x, y: pos.y)

let finalizedPath: Path =
switch op.pathOrSymbolSize {
case .path(let path): path
case .symbolSize(let size):
Path(
ellipseIn: CGRect(
origin: CGPoint(x: -size.width / 2, y: -size.height / 2),
size: size
)
graphicsContext.blendMode = .normal
}
}
} else {
graphicsContext.transform = .identity
let rect = CGRect(
origin: (pos - op.mark.radius).cgPoint,
size: CGSize(
width: op.mark.radius * 2, height: op.mark.radius * 2
)
)
graphicsContext.fill(
Path(ellipseIn: rect),
with: op.fill ?? .defaultNodeShading
)
}

if let strokeEffect = op.stroke {
switch strokeEffect.color {
case .color(let color):
graphicsContext.stroke(
Path(ellipseIn: rect),
with: .color(color),
style: strokeEffect.style ?? .defaultLinkStyle
)
case .clip:
graphicsContext.blendMode = .clear
graphicsContext.stroke(
Path(ellipseIn: rect),
with: .color(.black),
style: strokeEffect.style ?? .defaultLinkStyle
)
graphicsContext.blendMode = .normal
}
graphicsContext.fill(
finalizedPath,
with: op.fill ?? .defaultNodeShading
)
if let strokeEffect = op.stroke {
switch strokeEffect.color {
case .color(let color):
graphicsContext.stroke(
finalizedPath,
with: .color(color),
style: strokeEffect.style ?? .defaultLinkStyle
)
case .clip:
graphicsContext.blendMode = .clear
graphicsContext.stroke(
finalizedPath,
with: .color(.black),
style: strokeEffect.style ?? .defaultLinkStyle
)
graphicsContext.blendMode = .normal
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion Sources/Grape/Views/GraphRenderingContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,9 @@ public struct _GraphRenderingContext<NodeID: Hashable> {
@usableFromInline
internal var nodeOperations: [RenderOperation<NodeID>.Node] = []

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

@usableFromInline
internal var linkOperations: [RenderOperation<NodeID>.Link] = []
Expand Down
8 changes: 7 additions & 1 deletion Sources/Grape/Views/GraphRenderingStates.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,13 @@ internal struct GraphRenderingStates<NodeID: Hashable> {
var symbolShape: [Path] = []

@inlinable
var currentSymbolShape: Path? { symbolShape.last }
var currentSymbolShapeOrSize: PathOrSymbolSize {
if let shape = symbolShape.last {
return .path(shape)
} else {
return .symbolSize(currentSymbolSizeOrDefault)
}
}

@usableFromInline
var symbolSize: [CGSize] = []
Expand Down
15 changes: 11 additions & 4 deletions Sources/Grape/Views/RenderOperation.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import SwiftUI

@usableFromInline
enum PathOrSymbolSize {
case path(Path)
case symbolSize(CGSize)
}

@usableFromInline
internal enum RenderOperation<NodeID: Hashable> {

@usableFromInline
struct Node {
@usableFromInline
Expand All @@ -11,19 +18,19 @@ internal enum RenderOperation<NodeID: Hashable> {
@usableFromInline
let stroke: GraphContentEffect.Stroke?
@usableFromInline
let path: Path?
let pathOrSymbolSize: PathOrSymbolSize

@inlinable
init(
_ mark: NodeMark<NodeID>,
_ fill: GraphicsContext.Shading?,
_ stroke: GraphContentEffect.Stroke?,
_ path: Path?
_ pathOrSymbolSize: PathOrSymbolSize
) {
self.mark = mark
self.fill = fill
self.stroke = stroke
self.path = path
self.pathOrSymbolSize = pathOrSymbolSize
}
}

Expand Down Expand Up @@ -53,7 +60,7 @@ extension RenderOperation.Node: Equatable {
@inlinable
internal static func == (lhs: Self, rhs: Self) -> Bool {
let fillEq = lhs.fill == nil && rhs.fill == nil
let pathEq = lhs.path == nil && rhs.path == nil
let pathEq = lhs.pathOrSymbolSize == nil && rhs.pathOrSymbolSize == nil
return lhs.mark == rhs.mark
&& fillEq
&& lhs.stroke == rhs.stroke
Expand Down

0 comments on commit d57627c

Please sign in to comment.