Skip to main content

jellyflow_runtime/runtime/resize/
planner.rs

1use crate::node_origin::resolve_node_origin;
2use crate::runtime::geometry::CanvasBounds;
3use jellyflow_core::core::{CanvasPoint, CanvasRect, CanvasSize, Graph, Node, NodeExtent, NodeId};
4use jellyflow_core::ops::{GraphOp, GraphTransaction};
5
6use super::parent_expansion::parent_expansion_op;
7use super::types::{
8    NODE_RESIZE_TRANSACTION_LABEL, NodePointerResizeRequest, NodeResizeContext,
9    NodeResizeDirection, NodeResizePlan, NodeResizeRequest,
10};
11
12/// Plans a node resize update without mutating the graph.
13pub fn plan_node_resize(graph: &Graph, request: NodeResizeRequest) -> Option<NodeResizePlan> {
14    plan_node_resize_with_context(graph, NodeResizeContext::default(), request)
15}
16
17/// Plans a node resize update using a runtime geometry context.
18pub fn plan_node_resize_with_context(
19    graph: &Graph,
20    context: NodeResizeContext,
21    request: NodeResizeRequest,
22) -> Option<NodeResizePlan> {
23    let node = graph.nodes.get(&request.node)?;
24    if node.hidden || !node.pos.is_finite() {
25        return None;
26    }
27
28    let mut to = request
29        .constraints
30        .clamp(direction_target_size(node.size, request)?)?;
31    let from_pos = node.pos;
32    let node_origin = resolve_node_origin(node.origin, context.node_origin);
33    let mut to_pos = resized_position(from_pos, node.size, to, request.direction, node_origin)?;
34    if let Some(extent) = resolved_resize_extent(graph, node, None) {
35        let clamped = clamp_geometry_to_extent(to_pos, to, request.direction, node_origin, extent)?;
36        to_pos = clamped.0;
37        to = clamped.1;
38    }
39
40    resize_plan_for_geometry(graph, request.node, node, to, to_pos, node_origin)
41}
42
43/// Plans a node resize update from canvas-space pointer movement without mutating the graph.
44pub fn plan_node_pointer_resize(
45    graph: &Graph,
46    request: NodePointerResizeRequest,
47) -> Option<NodeResizePlan> {
48    plan_node_pointer_resize_with_context(graph, NodeResizeContext::default(), request)
49}
50
51/// Plans a pointer-driven node resize update using a runtime geometry context.
52pub fn plan_node_pointer_resize_with_context(
53    graph: &Graph,
54    context: NodeResizeContext,
55    request: NodePointerResizeRequest,
56) -> Option<NodeResizePlan> {
57    plan_node_pointer_resize_with_policy_extent(graph, context, None, request)
58}
59
60pub(super) fn plan_node_pointer_resize_with_policy_extent(
61    graph: &Graph,
62    context: NodeResizeContext,
63    node_extent: Option<CanvasRect>,
64    request: NodePointerResizeRequest,
65) -> Option<NodeResizePlan> {
66    let node = graph.nodes.get(&request.node)?;
67    if node.hidden
68        || !node.pos.is_finite()
69        || !request.start.is_finite()
70        || !request.current.is_finite()
71    {
72        return None;
73    }
74
75    let from_size = positive_size(node.size)?;
76    let node_origin = resolve_node_origin(node.origin, context.node_origin);
77    let (to_pos, to) =
78        pointer_resize_geometry(graph, node, node_extent, request, from_size, node_origin)?;
79
80    resize_plan_for_geometry(graph, request.node, node, to, to_pos, node_origin)
81}
82
83fn resize_plan_for_geometry(
84    graph: &Graph,
85    node_id: NodeId,
86    node: &Node,
87    to: CanvasSize,
88    to_pos: CanvasPoint,
89    node_origin: (f32, f32),
90) -> Option<NodeResizePlan> {
91    if !to.is_positive_finite() || !to_pos.is_finite() {
92        return None;
93    }
94    if node.size == Some(to) && node.pos == to_pos {
95        return None;
96    }
97
98    let mut ops = Vec::new();
99    if node.pos != to_pos {
100        ops.push(GraphOp::SetNodePos {
101            id: node_id,
102            from: node.pos,
103            to: to_pos,
104        });
105    }
106    if node.size != Some(to) {
107        ops.push(GraphOp::SetNodeSize {
108            id: node_id,
109            from: node.size,
110            to: Some(to),
111        });
112    }
113    if let Some(op) = parent_expansion_op(graph, node, to_pos, to, node_origin) {
114        ops.push(op);
115    }
116    let transaction = GraphTransaction::from_ops(ops).with_label(NODE_RESIZE_TRANSACTION_LABEL);
117
118    Some(NodeResizePlan::new(
119        node_id,
120        node.size,
121        to,
122        node.pos,
123        to_pos,
124        transaction,
125    ))
126}
127
128fn pointer_resize_geometry(
129    graph: &Graph,
130    node: &Node,
131    node_extent: Option<CanvasRect>,
132    request: NodePointerResizeRequest,
133    from_size: CanvasSize,
134    origin: (f32, f32),
135) -> Option<(CanvasPoint, CanvasSize)> {
136    let dist_x = (request.current.x - request.start.x).floor();
137    let dist_y = (request.current.y - request.start.y).floor();
138    let dir_x = resize_direction_x(request.direction);
139    let dir_y = resize_direction_y(request.direction);
140
141    let mut to = CanvasSize {
142        width: pointer_axis_size(from_size.width, dist_x, dir_x, origin.0),
143        height: pointer_axis_size(from_size.height, dist_y, dir_y, origin.1),
144    };
145    if request.keep_aspect_ratio {
146        to = keep_aspect_ratio_size(to, from_size, request.direction)?;
147    }
148    to = request.constraints.clamp(to)?;
149    if !request.axis.includes_width() {
150        to.width = from_size.width;
151    }
152    if !request.axis.includes_height() {
153        to.height = from_size.height;
154    }
155    if !to.is_positive_finite() {
156        return None;
157    }
158
159    let mut to_pos = resized_position(node.pos, Some(from_size), to, request.direction, origin)?;
160    if let Some(extent) = resolved_resize_extent(graph, node, node_extent) {
161        let clamped = clamp_geometry_to_extent(to_pos, to, request.direction, origin, extent)?;
162        to_pos = clamped.0;
163        to = clamped.1;
164    }
165
166    Some((to_pos, to))
167}
168
169fn direction_target_size(
170    current: Option<CanvasSize>,
171    request: NodeResizeRequest,
172) -> Option<CanvasSize> {
173    if !request.to.is_positive_finite() {
174        return None;
175    }
176
177    let mut to = request.to;
178    if !request.direction.is_horizontal() || !request.direction.is_vertical() {
179        let current = positive_size(current)?;
180        if !request.direction.is_horizontal() {
181            to.width = current.width;
182        }
183        if !request.direction.is_vertical() {
184            to.height = current.height;
185        }
186    }
187    Some(to)
188}
189
190fn resized_position(
191    from_pos: CanvasPoint,
192    from_size: Option<CanvasSize>,
193    to: CanvasSize,
194    direction: NodeResizeDirection,
195    origin: (f32, f32),
196) -> Option<CanvasPoint> {
197    let Some(from_size) = from_size else {
198        return position_without_current_size(from_pos, direction, origin);
199    };
200    let from_size = positive_size(Some(from_size))?;
201    let delta_width = to.width - from_size.width;
202    let delta_height = to.height - from_size.height;
203
204    Some(CanvasPoint {
205        x: resized_axis(
206            from_pos.x,
207            delta_width,
208            origin.0,
209            direction.is_horizontal(),
210            direction.affects_x(),
211        ),
212        y: resized_axis(
213            from_pos.y,
214            delta_height,
215            origin.1,
216            direction.is_vertical(),
217            direction.affects_y(),
218        ),
219    })
220}
221
222fn position_without_current_size(
223    from_pos: CanvasPoint,
224    direction: NodeResizeDirection,
225    origin: (f32, f32),
226) -> Option<CanvasPoint> {
227    (!position_may_change(direction, origin)).then_some(from_pos)
228}
229
230fn position_may_change(direction: NodeResizeDirection, origin: (f32, f32)) -> bool {
231    axis_position_may_change(direction.is_horizontal(), direction.affects_x(), origin.0)
232        || axis_position_may_change(direction.is_vertical(), direction.affects_y(), origin.1)
233}
234
235fn axis_position_may_change(is_axis: bool, affects_axis: bool, origin: f32) -> bool {
236    is_axis
237        && if affects_axis {
238            origin < 1.0
239        } else {
240            origin > 0.0
241        }
242}
243
244fn resized_axis(from: f32, delta: f32, origin: f32, is_axis: bool, affects_axis: bool) -> f32 {
245    if !is_axis {
246        return from;
247    }
248    if affects_axis {
249        from - delta * (1.0 - origin)
250    } else {
251        from + delta * origin
252    }
253}
254
255fn resize_direction_x(direction: NodeResizeDirection) -> f32 {
256    if direction.affects_x() {
257        -1.0
258    } else if direction.is_horizontal() {
259        1.0
260    } else {
261        0.0
262    }
263}
264
265fn resize_direction_y(direction: NodeResizeDirection) -> f32 {
266    if direction.affects_y() {
267        -1.0
268    } else if direction.is_vertical() {
269        1.0
270    } else {
271        0.0
272    }
273}
274
275fn pointer_axis_size(from: f32, delta: f32, direction: f32, origin: f32) -> f32 {
276    if direction == 0.0 {
277        return from;
278    }
279    from + direction * delta / (origin * 2.0 + 1.0)
280}
281
282fn keep_aspect_ratio_size(
283    mut to: CanvasSize,
284    from: CanvasSize,
285    direction: NodeResizeDirection,
286) -> Option<CanvasSize> {
287    let aspect_ratio = from.width / from.height;
288    if !aspect_ratio.is_finite() || aspect_ratio <= 0.0 {
289        return None;
290    }
291
292    if direction.is_horizontal() && !direction.is_vertical() {
293        to.height = to.width / aspect_ratio;
294    } else if direction.is_vertical() && !direction.is_horizontal() {
295        to.width = to.height * aspect_ratio;
296    } else if to.width / to.height > aspect_ratio {
297        to.height = to.width / aspect_ratio;
298    } else {
299        to.width = to.height * aspect_ratio;
300    }
301
302    to.is_positive_finite().then_some(to)
303}
304
305fn resolved_resize_extent(
306    graph: &Graph,
307    node: &Node,
308    node_extent: Option<CanvasRect>,
309) -> Option<CanvasRect> {
310    let extent = node
311        .extent
312        .or_else(|| node_extent.map(|rect| NodeExtent::Rect { rect }))?;
313    match extent {
314        NodeExtent::Rect { rect } => normalized_rect(rect),
315        NodeExtent::Parent if !node.expand_parent.unwrap_or(false) => node
316            .parent
317            .and_then(|parent| graph.groups.get(&parent))
318            .and_then(|group| normalized_rect(group.rect)),
319        NodeExtent::Parent => None,
320    }
321}
322
323fn normalized_rect(rect: CanvasRect) -> Option<CanvasRect> {
324    CanvasBounds::from_rect(rect).map(CanvasBounds::to_rect)
325}
326
327fn clamp_geometry_to_extent(
328    position: CanvasPoint,
329    size: CanvasSize,
330    direction: NodeResizeDirection,
331    origin: (f32, f32),
332    extent: CanvasRect,
333) -> Option<(CanvasPoint, CanvasSize)> {
334    if !extent.is_positive_finite() {
335        return None;
336    }
337
338    let mut top_left = CanvasPoint {
339        x: position.x - origin.0 * size.width,
340        y: position.y - origin.1 * size.height,
341    };
342    let mut size = size;
343    let extent_max_x = extent.origin.x + extent.size.width;
344    let extent_max_y = extent.origin.y + extent.size.height;
345
346    if direction.is_horizontal() {
347        if direction.affects_x() {
348            if top_left.x < extent.origin.x {
349                let overflow = extent.origin.x - top_left.x;
350                top_left.x = extent.origin.x;
351                size.width -= overflow;
352            }
353            let right = top_left.x + size.width;
354            if right > extent_max_x {
355                size.width -= right - extent_max_x;
356            }
357        } else {
358            let right = top_left.x + size.width;
359            if right > extent_max_x {
360                size.width -= right - extent_max_x;
361            }
362        }
363    }
364
365    if direction.is_vertical() {
366        if direction.affects_y() {
367            if top_left.y < extent.origin.y {
368                let overflow = extent.origin.y - top_left.y;
369                top_left.y = extent.origin.y;
370                size.height -= overflow;
371            }
372            let bottom = top_left.y + size.height;
373            if bottom > extent_max_y {
374                size.height -= bottom - extent_max_y;
375            }
376        } else {
377            let bottom = top_left.y + size.height;
378            if bottom > extent_max_y {
379                size.height -= bottom - extent_max_y;
380            }
381        }
382    }
383
384    if !size.is_positive_finite() {
385        return None;
386    }
387
388    Some((
389        CanvasPoint {
390            x: top_left.x + origin.0 * size.width,
391            y: top_left.y + origin.1 * size.height,
392        },
393        size,
394    ))
395}
396
397fn positive_size(size: Option<CanvasSize>) -> Option<CanvasSize> {
398    size.filter(|size| size.is_positive_finite())
399}