jellyflow_runtime/runtime/viewport/
transform.rs1use serde::{Deserialize, Serialize};
2
3use crate::io::NodeGraphViewState;
4use jellyflow_core::core::{CanvasPoint, CanvasRect, CanvasSize};
5
6#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
11pub struct ViewportTransform {
12 pub pan: CanvasPoint,
14 pub zoom: f32,
16}
17
18impl ViewportTransform {
19 pub fn new(pan: CanvasPoint, zoom: f32) -> Option<Self> {
21 let transform = Self { pan, zoom };
22 if !transform.is_valid() {
23 return None;
24 }
25
26 Some(transform)
27 }
28
29 pub fn is_valid(self) -> bool {
31 self.pan.is_finite() && valid_zoom(self.zoom)
32 }
33
34 pub fn from_view_state(view_state: &NodeGraphViewState) -> Option<Self> {
36 Self::new(view_state.pan, view_state.zoom)
37 }
38
39 pub fn screen_point_for_canvas(self, canvas: CanvasPoint) -> Option<CanvasPoint> {
41 if !self.is_valid() || !canvas.is_finite() {
42 return None;
43 }
44
45 let screen = CanvasPoint {
46 x: (canvas.x + self.pan.x) * self.zoom,
47 y: (canvas.y + self.pan.y) * self.zoom,
48 };
49 screen.is_finite().then_some(screen)
50 }
51
52 pub fn canvas_point_at_screen(self, screen: CanvasPoint) -> CanvasPoint {
54 CanvasPoint {
55 x: screen.x / self.zoom - self.pan.x,
56 y: screen.y / self.zoom - self.pan.y,
57 }
58 }
59}
60
61#[derive(Debug, Default, Clone, Copy, PartialEq)]
63pub struct ViewportConstraints {
64 pub viewport_size: Option<CanvasSize>,
65 pub translate_extent: Option<CanvasRect>,
66}
67
68impl ViewportConstraints {
69 pub fn unconstrained() -> Self {
70 Self::default()
71 }
72
73 pub fn with_translate_extent(viewport_size: CanvasSize, translate_extent: CanvasRect) -> Self {
74 Self {
75 viewport_size: Some(viewport_size),
76 translate_extent: Some(translate_extent),
77 }
78 }
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
83pub struct ViewportPanRequest {
84 pub screen_delta: CanvasPoint,
86}
87
88impl ViewportPanRequest {
89 pub fn new(screen_delta: CanvasPoint) -> Self {
90 Self { screen_delta }
91 }
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
96pub struct ViewportZoomRequest {
97 pub anchor_screen: CanvasPoint,
99 pub target_zoom: f32,
101 pub min_zoom: f32,
103 pub max_zoom: f32,
105}
106
107impl ViewportZoomRequest {
108 pub fn new(anchor_screen: CanvasPoint, target_zoom: f32, min_zoom: f32, max_zoom: f32) -> Self {
109 Self {
110 anchor_screen,
111 target_zoom,
112 min_zoom,
113 max_zoom,
114 }
115 }
116}
117
118pub fn pan_viewport(
120 current: ViewportTransform,
121 request: ViewportPanRequest,
122) -> Option<ViewportTransform> {
123 if !current.is_valid() {
124 return None;
125 }
126 if !request.screen_delta.is_finite() {
127 return None;
128 }
129
130 ViewportTransform::new(
131 CanvasPoint {
132 x: current.pan.x + request.screen_delta.x / current.zoom,
133 y: current.pan.y + request.screen_delta.y / current.zoom,
134 },
135 current.zoom,
136 )
137 .and_then(|next| constrain_viewport(next, ViewportConstraints::unconstrained()))
138}
139
140pub fn zoom_viewport(
142 current: ViewportTransform,
143 request: ViewportZoomRequest,
144) -> Option<ViewportTransform> {
145 if !current.is_valid() {
146 return None;
147 }
148 let target_zoom = clamped_target_zoom(request)?;
149 let anchor = request.anchor_screen;
150 if !anchor.is_finite() {
151 return None;
152 }
153
154 ViewportTransform::new(
155 CanvasPoint {
156 x: current.pan.x + anchor.x / target_zoom - anchor.x / current.zoom,
157 y: current.pan.y + anchor.y / target_zoom - anchor.y / current.zoom,
158 },
159 target_zoom,
160 )
161 .and_then(|next| constrain_viewport(next, ViewportConstraints::unconstrained()))
162}
163
164pub fn constrain_viewport(
166 transform: ViewportTransform,
167 constraints: ViewportConstraints,
168) -> Option<ViewportTransform> {
169 if !transform.is_valid() {
170 return None;
171 }
172 let Some(translate_extent) = constraints.translate_extent else {
173 return Some(transform);
174 };
175 let viewport_size = constraints.viewport_size?;
176 if !viewport_size.is_positive_finite() || !translate_extent.is_positive_finite() {
177 return None;
178 }
179
180 let visible_width = viewport_size.width / transform.zoom;
181 let visible_height = viewport_size.height / transform.zoom;
182 let extent_min_x = translate_extent.origin.x;
183 let extent_min_y = translate_extent.origin.y;
184 let extent_max_x = translate_extent.origin.x + translate_extent.size.width;
185 let extent_max_y = translate_extent.origin.y + translate_extent.size.height;
186
187 ViewportTransform::new(
188 CanvasPoint {
189 x: constrain_pan_axis(transform.pan.x, visible_width, extent_min_x, extent_max_x),
190 y: constrain_pan_axis(transform.pan.y, visible_height, extent_min_y, extent_max_y),
191 },
192 transform.zoom,
193 )
194}
195
196fn clamped_target_zoom(request: ViewportZoomRequest) -> Option<f32> {
197 if !valid_zoom(request.target_zoom)
198 || !valid_zoom(request.min_zoom)
199 || !valid_zoom(request.max_zoom)
200 {
201 return None;
202 }
203
204 let (min_zoom, max_zoom) = if request.min_zoom <= request.max_zoom {
205 (request.min_zoom, request.max_zoom)
206 } else {
207 (request.max_zoom, request.min_zoom)
208 };
209
210 Some(request.target_zoom.clamp(min_zoom, max_zoom))
211}
212
213fn constrain_pan_axis(pan: f32, visible_size: f32, extent_min: f32, extent_max: f32) -> f32 {
214 let lower = visible_size - extent_max;
215 let upper = -extent_min;
216 if lower <= upper {
217 pan.clamp(lower, upper)
218 } else {
219 (lower + upper) * 0.5
220 }
221}
222
223pub(super) fn valid_zoom(zoom: f32) -> bool {
224 zoom.is_finite() && zoom > 0.0
225}