From b08c89c0745ffc8389f4c7994884288ac1afb0e9 Mon Sep 17 00:00:00 2001 From: "Li, Zhen" Date: Tue, 20 Feb 2024 22:05:08 -0500 Subject: [PATCH 1/3] Fix magnification --- .../Views/ForceDirectedGraph+Gesture.swift | 28 +++++++++++++++---- .../Grape/Views/ForceDirectedGraphModel.swift | 5 ++++ 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/Sources/Grape/Views/ForceDirectedGraph+Gesture.swift b/Sources/Grape/Views/ForceDirectedGraph+Gesture.swift index ee42d7b..63f0909 100644 --- a/Sources/Grape/Views/ForceDirectedGraph+Gesture.swift +++ b/Sources/Grape/Views/ForceDirectedGraph+Gesture.swift @@ -119,14 +119,22 @@ extension ForceDirectedGraph { internal func onMagnifyChange( _ value: MagnifyGesture.Value ) { - // print(value.magnification) + var oldScale: Double + if let _scale = self.model.lastScaleRecord { + oldScale = _scale + } else { + self.model.lastScaleRecord = self.model.modelTransform.scale + oldScale = self.model.modelTransform.scale + } + let alpha = -self.model.finalTransform.invert(value.startLocation.simd) - let oldScale = self.model.modelTransform.scale + let oldTranslate = self.model.modelTransform.translate let newScale = clamp( - Darwin.cbrt(value.magnification) * oldScale, + value.magnification * oldScale, min: Self.minimumScale, max: Self.maximumScale) + let newTranslate = (oldScale - newScale) * alpha + oldTranslate let newModelTransform = ViewportTransform( @@ -143,11 +151,19 @@ extension ForceDirectedGraph { internal func onMagnifyEnd( _ value: MagnifyGesture.Value ) { + var oldScale: Double + if let _scale = self.model.lastScaleRecord { + oldScale = _scale + } else { + self.model.lastScaleRecord = self.model.modelTransform.scale + oldScale = self.model.modelTransform.scale + } + let alpha = -self.model.finalTransform.invert(value.startLocation.simd) - let oldScale = self.model.modelTransform.scale + let oldTranslate = self.model.modelTransform.translate let newScale = clamp( - Darwin.cbrt(value.magnification) * oldScale, + value.magnification * oldScale, min: Self.minimumScale, max: Self.maximumScale ) @@ -156,7 +172,7 @@ extension ForceDirectedGraph { translate: newTranslate, scale: newScale ) - // print("newModelTransform", newModelTransform) + self.model.lastScaleRecord = nil self.model.modelTransform = newModelTransform guard let action = self.model._onGraphMagnified else { return } action() diff --git a/Sources/Grape/Views/ForceDirectedGraphModel.swift b/Sources/Grape/Views/ForceDirectedGraphModel.swift index 4d55bb8..63fca79 100644 --- a/Sources/Grape/Views/ForceDirectedGraphModel.swift +++ b/Sources/Grape/Views/ForceDirectedGraphModel.swift @@ -55,6 +55,11 @@ public final class ForceDirectedGraphModel { return draggingNodeID != nil || backgroundDragStart != nil } + // records the scale right before a magnification gesture starts + @usableFromInline + var lastScaleRecord: Double? = nil + + @usableFromInline let velocityDecay: Double From ae3080e486115bd854a194211753c08164820f48 Mon Sep 17 00:00:00 2001 From: "Li, Zhen" Date: Tue, 20 Feb 2024 22:13:34 -0500 Subject: [PATCH 2/3] Fix Observation --- Sources/Grape/Views/ForceDirectedGraph+Gesture.swift | 2 +- Sources/Grape/Views/ForceDirectedGraph+View.swift | 2 +- Sources/Grape/Views/ForceDirectedGraphModel.swift | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/Grape/Views/ForceDirectedGraph+Gesture.swift b/Sources/Grape/Views/ForceDirectedGraph+Gesture.swift index 63f0909..90da0e5 100644 --- a/Sources/Grape/Views/ForceDirectedGraph+Gesture.swift +++ b/Sources/Grape/Views/ForceDirectedGraph+Gesture.swift @@ -183,7 +183,7 @@ extension ForceDirectedGraph { extension ForceDirectedGraph { @inlinable public func onTicked( - perform action: @escaping (KeyFrame) -> Void + perform action: @escaping (UInt) -> Void ) -> Self { self.model._onTicked = action return self diff --git a/Sources/Grape/Views/ForceDirectedGraph+View.swift b/Sources/Grape/Views/ForceDirectedGraph+View.swift index 69d935f..9fc30e3 100644 --- a/Sources/Grape/Views/ForceDirectedGraph+View.swift +++ b/Sources/Grape/Views/ForceDirectedGraph+View.swift @@ -46,7 +46,7 @@ extension ForceDirectedGraph: View { @inlinable var debugView: some View { VStack(alignment: .leading, spacing: 8.0) { - Text("Elapsed Time: \(model.currentFrame.rawValue)") + Text("Elapsed Time: \(model.currentFrame)") Divider() Text(self.model.changeMessage) Divider() diff --git a/Sources/Grape/Views/ForceDirectedGraphModel.swift b/Sources/Grape/Views/ForceDirectedGraphModel.swift index 63fca79..9e2be97 100644 --- a/Sources/Grape/Views/ForceDirectedGraphModel.swift +++ b/Sources/Grape/Views/ForceDirectedGraphModel.swift @@ -71,7 +71,7 @@ public final class ForceDirectedGraphModel { var _$changeMessage = "N/A" @usableFromInline - var _$currentFrame: KeyFrame = 0 + var _$currentFrame: UInt = 0 @inlinable var changeMessage: String { @@ -93,7 +93,7 @@ public final class ForceDirectedGraphModel { } @inlinable - var currentFrame: KeyFrame { + var currentFrame: UInt { @storageRestrictions(initializes: _$currentFrame) init(initialValue) { _$currentFrame = initialValue @@ -119,7 +119,7 @@ public final class ForceDirectedGraphModel { var scheduledTimer: Timer? = nil @usableFromInline - var _onTicked: ((KeyFrame) -> Void)? = nil + var _onTicked: ((UInt) -> Void)? = nil @usableFromInline var _onNodeDragChanged: ((NodeID, CGPoint) -> Void)? = nil @@ -245,7 +245,7 @@ extension ForceDirectedGraphModel { func tick() { withMutation(keyPath: \.currentFrame) { simulationContext.storage.tick() - currentFrame.advance() + currentFrame += 1 } _onTicked?(currentFrame) } From ca8271569565f4fee671f0b3201a74443b86b564 Mon Sep 17 00:00:00 2001 From: "Li, Zhen" Date: Tue, 20 Feb 2024 22:42:21 -0500 Subject: [PATCH 3/3] Fix magnify gesture --- .../Views/ForceDirectedGraph+Gesture.swift | 298 +++++++++--------- .../Grape/Views/ForceDirectedGraphModel.swift | 17 +- 2 files changed, 164 insertions(+), 151 deletions(-) diff --git a/Sources/Grape/Views/ForceDirectedGraph+Gesture.swift b/Sources/Grape/Views/ForceDirectedGraph+Gesture.swift index 90da0e5..c7bf4e5 100644 --- a/Sources/Grape/Views/ForceDirectedGraph+Gesture.swift +++ b/Sources/Grape/Views/ForceDirectedGraph+Gesture.swift @@ -2,182 +2,182 @@ import ForceSimulation import SwiftUI #if !os(tvOS) -extension ForceDirectedGraph { - @inlinable - static var minimumAlphaAfterDrag: CGFloat { 0.5 } - @inlinable - internal func onDragChange( - _ value: SwiftUI.DragGesture.Value - ) { - if !model.isDragStartStateRecorded { - if let nodeID = model.findNode(at: value.startLocation) { - model.draggingNodeID = nodeID - } else { - model.backgroundDragStart = value.location.simd + extension ForceDirectedGraph { + @inlinable + static var minimumAlphaAfterDrag: CGFloat { 0.5 } + @inlinable + internal func onDragChange( + _ value: SwiftUI.DragGesture.Value + ) { + if !model.isDragStartStateRecorded { + if let nodeID = model.findNode(at: value.startLocation) { + model.draggingNodeID = nodeID + } else { + model.backgroundDragStart = value.location.simd + } + assert(model.isDragStartStateRecorded == true) } - assert(model.isDragStartStateRecorded == true) - } - - guard let nodeID = model.draggingNodeID else { - if let dragStart = model.backgroundDragStart { - let delta = value.location.simd - dragStart - model.modelTransform.translate += delta - model.backgroundDragStart = value.location.simd + + guard let nodeID = model.draggingNodeID else { + if let dragStart = model.backgroundDragStart { + let delta = value.location.simd - dragStart + model.modelTransform.translate += delta + model.backgroundDragStart = value.location.simd + } + return } - return - } - if model.simulationContext.storage.kinetics.alpha < Self.minimumAlphaAfterDrag { - model.simulationContext.storage.kinetics.alpha = Self.minimumAlphaAfterDrag - } + if model.simulationContext.storage.kinetics.alpha < Self.minimumAlphaAfterDrag { + model.simulationContext.storage.kinetics.alpha = Self.minimumAlphaAfterDrag + } - let newLocationInSimulation = model.finalTransform.invert(value.location.simd) + let newLocationInSimulation = model.finalTransform.invert(value.location.simd) - if let nodeIndex = model.simulationContext.nodeIndexLookup[nodeID] { - model.simulationContext.storage.kinetics.fixation[ - nodeIndex - ] = newLocationInSimulation - } + if let nodeIndex = model.simulationContext.nodeIndexLookup[nodeID] { + model.simulationContext.storage.kinetics.fixation[ + nodeIndex + ] = newLocationInSimulation + } - guard let action = model._onNodeDragChanged else { return } - action(nodeID, value.location) + guard let action = model._onNodeDragChanged else { return } + action(nodeID, value.location) - } + } - @inlinable - internal func onDragEnd( - _ value: SwiftUI.DragGesture.Value - ) { - - guard let nodeID = model.draggingNodeID else { - if let dragStart = model.backgroundDragStart { - let delta = value.location.simd - dragStart - model.modelTransform.translate += delta - model.backgroundDragStart = nil + @inlinable + internal func onDragEnd( + _ value: SwiftUI.DragGesture.Value + ) { + + guard let nodeID = model.draggingNodeID else { + if let dragStart = model.backgroundDragStart { + let delta = value.location.simd - dragStart + model.modelTransform.translate += delta + model.backgroundDragStart = nil + } + return + } + if model.simulationContext.storage.kinetics.alpha < Self.minimumAlphaAfterDrag { + model.simulationContext.storage.kinetics.alpha = Self.minimumAlphaAfterDrag } - return - } - if model.simulationContext.storage.kinetics.alpha < Self.minimumAlphaAfterDrag { - model.simulationContext.storage.kinetics.alpha = Self.minimumAlphaAfterDrag - } - model.draggingNodeID = nil - - guard let nodeIndex = model.simulationContext.nodeIndexLookup[nodeID] else { return } - if model._onNodeDragEnded == nil { - model.simulationContext.storage.kinetics.fixation[ - nodeIndex - ] = nil - } else if let action = model._onNodeDragEnded, action(nodeID, value.location) { - model.simulationContext.storage.kinetics.fixation[ - nodeIndex - ] = nil + model.draggingNodeID = nil + + guard let nodeIndex = model.simulationContext.nodeIndexLookup[nodeID] else { return } + if model._onNodeDragEnded == nil { + model.simulationContext.storage.kinetics.fixation[ + nodeIndex + ] = nil + } else if let action = model._onNodeDragEnded, action(nodeID, value.location) { + model.simulationContext.storage.kinetics.fixation[ + nodeIndex + ] = nil + } } - } - @inlinable - static var minimumDragDistance: CGFloat { 3.0 } -} + @inlinable + static var minimumDragDistance: CGFloat { 3.0 } + } -extension ForceDirectedGraph { - @inlinable - internal func onTapGesture( - _ location: CGPoint - ) { - guard let action = self.model._onNodeTapped else { return } - let nodeID = self.model.findNode(at: location) - action(nodeID) + extension ForceDirectedGraph { + @inlinable + internal func onTapGesture( + _ location: CGPoint + ) { + guard let action = self.model._onNodeTapped else { return } + let nodeID = self.model.findNode(at: location) + action(nodeID) + } } -} #endif #if os(iOS) || os(macOS) -extension ForceDirectedGraph { + extension ForceDirectedGraph { - @inlinable - static var minimumScaleDelta: CGFloat { 0.001 } + @inlinable + static var minimumScaleDelta: CGFloat { 0.001 } - @inlinable - static var minimumScale: CGFloat { 0.25 } + @inlinable + static var minimumScale: CGFloat { 0.25 } - @inlinable - static var maximumScale: CGFloat { 4.0 } + @inlinable + static var maximumScale: CGFloat { 4.0 } - @inlinable - static var magnificationDecay: CGFloat { 0.1 } + @inlinable + static var magnificationDecay: CGFloat { 0.1 } - @inlinable - internal func clamp( - _ value: CGFloat, - min: CGFloat, - max: CGFloat - ) -> CGFloat { - Swift.min(Swift.max(value, min), max) - } + @inlinable + internal func clamp( + _ value: CGFloat, + min: CGFloat, + max: CGFloat + ) -> CGFloat { + Swift.min(Swift.max(value, min), max) + } - @inlinable - internal func onMagnifyChange( - _ value: MagnifyGesture.Value - ) { - var oldScale: Double - if let _scale = self.model.lastScaleRecord { - oldScale = _scale - } else { - self.model.lastScaleRecord = self.model.modelTransform.scale - oldScale = self.model.modelTransform.scale + @inlinable + internal func onMagnifyChange( + _ value: MagnifyGesture.Value + ) { + var startTransform: ViewportTransform + if let t = self.model.lastTransformRecord { + startTransform = t + } else { + self.model.lastTransformRecord = self.model.modelTransform + startTransform = self.model.modelTransform + } + + let alpha = (startTransform.translate(by: self.model.obsoleteState.cgSize.simd / 2)) + .invert(value.startLocation.simd) + + let newScale = clamp( + value.magnification * startTransform.scale, + min: Self.minimumScale, + max: Self.maximumScale) + + let newTranslate = (startTransform.scale - newScale) * alpha + startTransform.translate + + let newModelTransform = ViewportTransform( + translate: newTranslate, + scale: newScale + ) + self.model.modelTransform = newModelTransform + + guard let action = self.model._onGraphMagnified else { return } + action() } - - let alpha = -self.model.finalTransform.invert(value.startLocation.simd) - - let oldTranslate = self.model.modelTransform.translate - let newScale = clamp( - value.magnification * oldScale, - min: Self.minimumScale, - max: Self.maximumScale) - - let newTranslate = (oldScale - newScale) * alpha + oldTranslate - - let newModelTransform = ViewportTransform( - translate: newTranslate, - scale: newScale - ) - self.model.modelTransform = newModelTransform - - guard let action = self.model._onGraphMagnified else { return } - action() - } - @inlinable - internal func onMagnifyEnd( - _ value: MagnifyGesture.Value - ) { - var oldScale: Double - if let _scale = self.model.lastScaleRecord { - oldScale = _scale - } else { - self.model.lastScaleRecord = self.model.modelTransform.scale - oldScale = self.model.modelTransform.scale + @inlinable + internal func onMagnifyEnd( + _ value: MagnifyGesture.Value + ) { + var startTransform: ViewportTransform + if let t = self.model.lastTransformRecord { + startTransform = t + } else { + self.model.lastTransformRecord = self.model.modelTransform + startTransform = self.model.modelTransform + } + + let alpha = (startTransform.translate(by: self.model.obsoleteState.cgSize.simd / 2)) + .invert(value.startLocation.simd) + + let newScale = clamp( + value.magnification * startTransform.scale, + min: Self.minimumScale, + max: Self.maximumScale) + + let newTranslate = (startTransform.scale - newScale) * alpha + startTransform.translate + let newModelTransform = ViewportTransform( + translate: newTranslate, + scale: newScale + ) + self.model.lastTransformRecord = nil + self.model.modelTransform = newModelTransform + guard let action = self.model._onGraphMagnified else { return } + action() } - - let alpha = -self.model.finalTransform.invert(value.startLocation.simd) - - let oldTranslate = self.model.modelTransform.translate - let newScale = clamp( - value.magnification * oldScale, - min: Self.minimumScale, - max: Self.maximumScale - ) - let newTranslate = (oldScale - newScale) * alpha + oldTranslate - let newModelTransform = ViewportTransform( - translate: newTranslate, - scale: newScale - ) - self.model.lastScaleRecord = nil - self.model.modelTransform = newModelTransform - guard let action = self.model._onGraphMagnified else { return } - action() } -} #endif extension ForceDirectedGraph { diff --git a/Sources/Grape/Views/ForceDirectedGraphModel.swift b/Sources/Grape/Views/ForceDirectedGraphModel.swift index 9e2be97..70f16c2 100644 --- a/Sources/Grape/Views/ForceDirectedGraphModel.swift +++ b/Sources/Grape/Views/ForceDirectedGraphModel.swift @@ -6,6 +6,12 @@ import SwiftUI // @Observable public final class ForceDirectedGraphModel { + @usableFromInline + internal struct ObsoleteState { + @usableFromInline + var cgSize: CGSize + } + public typealias NodeID = Content.NodeID @usableFromInline @@ -55,9 +61,9 @@ public final class ForceDirectedGraphModel { return draggingNodeID != nil || backgroundDragStart != nil } - // records the scale right before a magnification gesture starts + // records the transform right before a magnification gesture starts @usableFromInline - var lastScaleRecord: Double? = nil + var lastTransformRecord: ViewportTransform? = nil @usableFromInline @@ -142,6 +148,11 @@ public final class ForceDirectedGraphModel { @usableFromInline var _onGraphMagnified: (() -> Void)? = nil + + // // records the transform right before a magnification gesture starts + @usableFromInline + var obsoleteState = ObsoleteState(cgSize: .zero) + @inlinable init( _ graphRenderingContext: _GraphRenderingContext, @@ -171,6 +182,7 @@ public final class ForceDirectedGraphModel { count: self.simulationContext.storage.kinetics.position.count ) self.currentFrame = 0 +// self.lastViewportSize = .zero self._modelTransformExtenalBinding = modelTransform self.modelTransform = modelTransform.wrappedValue } @@ -265,6 +277,7 @@ extension ForceDirectedGraphModel { ) { // should not invoke `access`, but actually does now ? // print("Rendering frame \(_$currentFrame.rawValue)") + obsoleteState.cgSize = size let transform = modelTransform.translate(by: size.simd / 2) // debugPrint(transform.scale)