Skip to main content

jellyflow_runtime/runtime/viewport/
inertia.rs

1use serde::{Deserialize, Serialize};
2
3use jellyflow_core::core::CanvasPoint;
4
5use crate::io::NodeGraphPanInertiaTuning;
6
7use super::transform::ViewportTransform;
8
9/// Request to plan inertial panning from an adapter-provided release velocity.
10#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
11pub struct ViewportPanInertiaRequest {
12    pub current: ViewportTransform,
13    /// Initial pan velocity in logical screen pixels per second.
14    pub initial_velocity_screen: CanvasPoint,
15    pub tuning: NodeGraphPanInertiaTuning,
16}
17
18impl ViewportPanInertiaRequest {
19    pub fn new(
20        current: ViewportTransform,
21        initial_velocity_screen: CanvasPoint,
22        tuning: NodeGraphPanInertiaTuning,
23    ) -> Self {
24        Self {
25            current,
26            initial_velocity_screen,
27            tuning,
28        }
29    }
30}
31
32/// Deterministic pan inertia plan.
33#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
34pub struct ViewportPanInertiaPlan {
35    pub from: ViewportTransform,
36    pub initial_velocity_screen: CanvasPoint,
37    pub duration_seconds: f32,
38    pub decay_per_s: f32,
39    pub min_speed: f32,
40}
41
42impl ViewportPanInertiaPlan {
43    pub fn frame_at(self, elapsed_seconds: f32) -> Option<ViewportPanInertiaFrame> {
44        if !elapsed_seconds.is_finite() {
45            return None;
46        }
47
48        let elapsed_seconds = elapsed_seconds.max(0.0);
49        let sample_elapsed = elapsed_seconds.min(self.duration_seconds);
50        let decay_factor = (-self.decay_per_s * sample_elapsed).exp();
51        let velocity_screen = scale_point(self.initial_velocity_screen, decay_factor);
52        let speed_screen = point_speed(velocity_screen);
53        let displacement_screen = scale_point(
54            self.initial_velocity_screen,
55            (1.0 - decay_factor) / self.decay_per_s,
56        );
57        let transform = ViewportTransform::new(
58            CanvasPoint {
59                x: self.from.pan.x + displacement_screen.x / self.from.zoom,
60                y: self.from.pan.y + displacement_screen.y / self.from.zoom,
61            },
62            self.from.zoom,
63        )?;
64
65        Some(ViewportPanInertiaFrame {
66            elapsed_seconds,
67            progress: if self.duration_seconds <= 0.0 {
68                1.0
69            } else {
70                (sample_elapsed / self.duration_seconds).clamp(0.0, 1.0)
71            },
72            speed_screen,
73            velocity_screen,
74            transform,
75            done: elapsed_seconds >= self.duration_seconds || speed_screen <= self.min_speed,
76        })
77    }
78
79    pub fn terminal_frame(self) -> Option<ViewportPanInertiaFrame> {
80        self.frame_at(self.duration_seconds)
81    }
82}
83
84/// Sampled inertial pan frame.
85#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
86pub struct ViewportPanInertiaFrame {
87    pub elapsed_seconds: f32,
88    pub progress: f32,
89    pub speed_screen: f32,
90    pub velocity_screen: CanvasPoint,
91    pub transform: ViewportTransform,
92    pub done: bool,
93}
94
95pub fn plan_viewport_pan_inertia(
96    request: ViewportPanInertiaRequest,
97) -> Option<ViewportPanInertiaPlan> {
98    if !request.current.is_valid()
99        || !request.initial_velocity_screen.is_finite()
100        || !request.tuning.enabled
101        || !valid_positive(request.tuning.decay_per_s)
102        || !valid_positive(request.tuning.min_speed)
103        || !valid_positive(request.tuning.max_speed)
104        || request.tuning.max_speed <= request.tuning.min_speed
105    {
106        return None;
107    }
108
109    let initial_speed = point_speed(request.initial_velocity_screen);
110    if !initial_speed.is_finite() || initial_speed <= request.tuning.min_speed {
111        return None;
112    }
113
114    let initial_velocity_screen = clamp_velocity(
115        request.initial_velocity_screen,
116        initial_speed,
117        request.tuning.max_speed,
118    )?;
119    let clamped_speed = point_speed(initial_velocity_screen);
120    if clamped_speed <= request.tuning.min_speed {
121        return None;
122    }
123
124    let duration_seconds =
125        (clamped_speed / request.tuning.min_speed).ln() / request.tuning.decay_per_s;
126    if !duration_seconds.is_finite() || duration_seconds <= 0.0 {
127        return None;
128    }
129
130    Some(ViewportPanInertiaPlan {
131        from: request.current,
132        initial_velocity_screen,
133        duration_seconds,
134        decay_per_s: request.tuning.decay_per_s,
135        min_speed: request.tuning.min_speed,
136    })
137}
138
139fn clamp_velocity(velocity: CanvasPoint, speed: f32, max_speed: f32) -> Option<CanvasPoint> {
140    if speed <= max_speed {
141        return Some(velocity);
142    }
143
144    let scale = max_speed / speed;
145    let clamped = scale_point(velocity, scale);
146    clamped.is_finite().then_some(clamped)
147}
148
149fn scale_point(point: CanvasPoint, scale: f32) -> CanvasPoint {
150    CanvasPoint {
151        x: point.x * scale,
152        y: point.y * scale,
153    }
154}
155
156fn point_speed(point: CanvasPoint) -> f32 {
157    point.x.hypot(point.y)
158}
159
160fn valid_positive(value: f32) -> bool {
161    value.is_finite() && value > 0.0
162}