diff --git a/Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/MermaidVisualization.swift b/Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/MermaidVisualization.swift index 9098d20..75987a1 100644 --- a/Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/MermaidVisualization.swift +++ b/Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/MermaidVisualization.swift @@ -109,8 +109,7 @@ struct MermaidVisualization: View { Rectangle().fill(.clear).contentShape(Rectangle()) .withGraphDragGesture(proxy) .onTapGesture { value in - if let nodeID = proxy.locateNode(at: .init(x: value.x, y: value.y)) { - guard let nodeID = nodeID as? String else { return } + if let nodeID = proxy.node(of: String.self, at: value) { model.tappedNode = nodeID } } diff --git a/Sources/Grape/Gestures/GraphDragGesture.swift b/Sources/Grape/Gestures/GraphDragGesture.swift index fb2dcd0..785c087 100644 --- a/Sources/Grape/Gestures/GraphDragGesture.swift +++ b/Sources/Grape/Gestures/GraphDragGesture.swift @@ -79,9 +79,8 @@ struct GraphDragModifier: ViewModifier { value: DragGesture.Value ) { if dragState == nil { - if let nodeID = graphProxy.locateNode(at: value.startLocation) { + if let nodeID = graphProxy.node(at: value.startLocation) { dragState = .node(nodeID) - graphProxy.setNodeFixation(nodeID: nodeID, fixation: value.startLocation) } else { dragState = .background(start: value.location.simd) diff --git a/Sources/Grape/Gestures/GraphTapGesture.swift b/Sources/Grape/Gestures/GraphTapGesture.swift index 124b699..38fdfdc 100644 --- a/Sources/Grape/Gestures/GraphTapGesture.swift +++ b/Sources/Grape/Gestures/GraphTapGesture.swift @@ -8,7 +8,7 @@ extension View { action: @escaping (AnyHashable) -> Void ) -> some View { self.onTapGesture { value in - if let nodeID = proxy.locateNode(at: .init(x: value.x, y: value.y)) { + if let nodeID = proxy.node(at: .init(x: value.x, y: value.y)) { action(nodeID) } } diff --git a/Sources/Grape/Modifiers/GraphProxy.swift b/Sources/Grape/Modifiers/GraphProxy.swift index dab46c5..4f1ff00 100644 --- a/Sources/Grape/Modifiers/GraphProxy.swift +++ b/Sources/Grape/Modifiers/GraphProxy.swift @@ -19,14 +19,22 @@ public struct GraphProxy { } extension GraphProxy: _AnyGraphProxyProtocol { + /// Find the node ID at the given location in the viewport coordinate, with specific type. + /// Returns `nil` if no node is found or the node is not of the specified type. + @inlinable + public func node(of type: ID.Type, at locationInViewportCoordinate: CGPoint) -> ID? where ID : Hashable { + storage?.node(of: type, at: locationInViewportCoordinate) + } + /// Find the type erased node ID at the given location in the viewport coordinate. + /// Returns `nil` if no node is found. @inlinable - public func locateNode(at locationInViewportCoordinate: CGPoint) -> AnyHashable? { - storage?.locateNode(at: locationInViewportCoordinate) + public func node(at locationInViewportCoordinate: CGPoint) -> AnyHashable? { + storage?.node(at: locationInViewportCoordinate) } @inlinable - public func setNodeFixation(nodeID: some Hashable, fixation: CGPoint?, minimumAlpha: Double = 0.5) { + public func setNodeFixation(nodeID: ID, fixation: CGPoint?, minimumAlpha: Double = 0.5) { storage?.setNodeFixation(nodeID: nodeID, fixation: fixation, minimumAlpha: minimumAlpha) } diff --git a/Sources/Grape/Views/ForceDirectedGraphModel.swift b/Sources/Grape/Views/ForceDirectedGraphModel.swift index eb345c8..bba7889 100644 --- a/Sources/Grape/Views/ForceDirectedGraphModel.swift +++ b/Sources/Grape/Views/ForceDirectedGraphModel.swift @@ -6,14 +6,15 @@ import SwiftUI @MainActor public protocol _AnyGraphProxyProtocol { @inlinable - func locateNode( + func node( at locationInViewportCoordinate: CGPoint ) -> AnyHashable? - + @inlinable + func node(of type: ID.Type, at locationInViewportCoordinate: CGPoint) -> ID? @inlinable - func setNodeFixation(nodeID: some Hashable, fixation: CGPoint?, minimumAlpha: Double) + func setNodeFixation(nodeID: ID, fixation: CGPoint?, minimumAlpha: Double) @inlinable var kineticAlpha: Double { get nonmutating set } @@ -32,8 +33,18 @@ public protocol _AnyGraphProxyProtocol { } extension ForceDirectedGraphModel: _AnyGraphProxyProtocol { + @inlinable - public func locateNode(at locationInViewportCoordinate: CGPoint) -> AnyHashable? { + public func node(of type: ID.Type, at locationInViewportCoordinate: CGPoint) -> ID? where ID: Hashable { + if type.self == NodeID.self { + return findNode(at: locationInViewportCoordinate) as! ID? + } else { + return nil + } + } + + @inlinable + public func node(at locationInViewportCoordinate: CGPoint) -> AnyHashable? { // Find from rich label first if let nodeIDFromRichLabel = findNodeFromRichLabel( @@ -52,7 +63,7 @@ extension ForceDirectedGraphModel: _AnyGraphProxyProtocol { } @inlinable - public func setNodeFixation(nodeID: some Hashable, fixation: CGPoint?, minimumAlpha: Double) { + public func setNodeFixation(nodeID: ID, fixation: CGPoint?, minimumAlpha: Double) { guard let nodeID = nodeID as? NodeID else { return } @@ -201,7 +212,7 @@ public final class ForceDirectedGraphModel { let ticksPerSecond: Double @usableFromInline - // @MainActor + @MainActor var scheduledTimer: Timer? = nil @usableFromInline @@ -329,6 +340,7 @@ public final class ForceDirectedGraphModel { @inlinable deinit { print("deinit") + let _ = MainActor.assumeIsolated { scheduledTimer?.invalidate() } @@ -604,11 +616,11 @@ extension ForceDirectedGraphModel { width: physicalWidth, height: physicalHeight) let rect = CGRect( - x: center.x + offset.x + textImageOffset.x, // - physicalWidth / 2, - y: -center.y - offset.y - textImageOffset.y, // - physicalHeight - width: physicalWidth, - height: physicalHeight - ) + x: center.x + offset.x + textImageOffset.x, // - physicalWidth / 2, + y: -center.y - offset.y - textImageOffset.y, // - physicalHeight + width: physicalWidth, + height: physicalHeight + ) cgContext.draw( rasterizedSymbol, in: rect @@ -658,11 +670,11 @@ extension ForceDirectedGraphModel { width: physicalWidth, height: physicalHeight) let rect = CGRect( - x: pos.x + offset.x + textImageOffset.x, // - physicalWidth / 2, - y: -pos.y - offset.y - textImageOffset.y, // - physicalHeight - width: physicalWidth, - height: physicalHeight - ) + x: pos.x + offset.x + textImageOffset.x, // - physicalWidth / 2, + y: -pos.y - offset.y - textImageOffset.y, // - physicalHeight + width: physicalWidth, + height: physicalHeight + ) cgContext.draw( rasterizedSymbol, @@ -692,7 +704,6 @@ extension ForceDirectedGraphModel { let textImageOffset = textOffsetParams.alignment.textImageOffsetInCGContext( width: physicalWidth, height: physicalHeight) - let rect = CGRect( x: center.x + offset.x + textImageOffset.x, // - physicalWidth / 2, y: -center.y - offset.y - textImageOffset.y, // - physicalHeight