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
12pub fn plan_node_resize(graph: &Graph, request: NodeResizeRequest) -> Option<NodeResizePlan> {
14 plan_node_resize_with_context(graph, NodeResizeContext::default(), request)
15}
16
17pub 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
43pub 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
51pub 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}