diff --git a/Sources/Grape/Views/ForceDirectedGraph+Gesture.swift b/Sources/Grape/Views/ForceDirectedGraph+Gesture.swift index ee42d7b..c7bf4e5 100644 --- a/Sources/Grape/Views/ForceDirectedGraph+Gesture.swift +++ b/Sources/Grape/Views/ForceDirectedGraph+Gesture.swift @@ -2,172 +2,188 @@ 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 - ) { - // print(value.magnification) - 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, - 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 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 + } - @inlinable - internal func onMagnifyEnd( - _ value: MagnifyGesture.Value - ) { - 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, - min: Self.minimumScale, - max: Self.maximumScale - ) - let newTranslate = (oldScale - newScale) * alpha + oldTranslate - let newModelTransform = ViewportTransform( - translate: newTranslate, - scale: newScale - ) - // print("newModelTransform", newModelTransform) - self.model.modelTransform = newModelTransform - guard let action = self.model._onGraphMagnified else { return } - action() + 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() + } + + @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() + } } -} #endif 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 4d55bb8..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,6 +61,11 @@ public final class ForceDirectedGraphModel { return draggingNodeID != nil || backgroundDragStart != nil } + // records the transform right before a magnification gesture starts + @usableFromInline + var lastTransformRecord: ViewportTransform? = nil + + @usableFromInline let velocityDecay: Double @@ -66,7 +77,7 @@ public final class ForceDirectedGraphModel { var _$changeMessage = "N/A" @usableFromInline - var _$currentFrame: KeyFrame = 0 + var _$currentFrame: UInt = 0 @inlinable var changeMessage: String { @@ -88,7 +99,7 @@ public final class ForceDirectedGraphModel { } @inlinable - var currentFrame: KeyFrame { + var currentFrame: UInt { @storageRestrictions(initializes: _$currentFrame) init(initialValue) { _$currentFrame = initialValue @@ -114,7 +125,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 @@ -137,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, @@ -166,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 } @@ -240,7 +257,7 @@ extension ForceDirectedGraphModel { func tick() { withMutation(keyPath: \.currentFrame) { simulationContext.storage.tick() - currentFrame.advance() + currentFrame += 1 } _onTicked?(currentFrame) } @@ -260,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)