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)