Skip to main content

jellyflow_runtime/runtime/drag/
planner.rs

1use crate::io::NodeGraphNudgeStepMode;
2use crate::io::{NodeGraphInteractionState, NodeGraphViewState};
3use jellyflow_core::core::{CanvasPoint, Graph, NodeId};
4use jellyflow_core::ops::{GraphOp, GraphTransaction};
5
6use super::candidates::drag_candidates;
7use super::constraints::{drag_items, snapped_delta};
8use super::parent_expansion::parent_expansion_ops;
9use super::types::{
10    NODE_DRAG_TRANSACTION_LABEL, NODE_NUDGE_TRANSACTION_LABEL, NodeDragItem, NodeDragPlan,
11    NodeDragRequest, NodeNudgePlan, NodeNudgeRequest,
12};
13
14/// Plans a node drag update without mutating the graph.
15pub fn plan_node_drag(
16    graph: &Graph,
17    view_state: &NodeGraphViewState,
18    interaction: &NodeGraphInteractionState,
19    request: NodeDragRequest,
20) -> Option<NodeDragPlan> {
21    if !request.to.is_finite() {
22        return None;
23    }
24
25    let primary = graph.nodes.get(&request.node)?;
26    let delta = CanvasPoint {
27        x: request.to.x - primary.pos.x,
28        y: request.to.y - primary.pos.y,
29    };
30    let (_delta, items, transaction) = plan_node_move_delta(
31        graph,
32        view_state,
33        interaction,
34        request.node,
35        delta,
36        NODE_DRAG_TRANSACTION_LABEL,
37    )?;
38    let primary_to = items
39        .iter()
40        .find(|item| item.node == request.node)
41        .map(|item| item.to)?;
42
43    Some(NodeDragPlan::new(
44        request.node,
45        primary.pos,
46        primary_to,
47        items,
48        transaction,
49    ))
50}
51
52/// Plans a keyboard nudge update for the currently selected nodes without mutating the graph.
53pub fn plan_node_nudge(
54    graph: &Graph,
55    view_state: &NodeGraphViewState,
56    interaction: &NodeGraphInteractionState,
57    request: NodeNudgeRequest,
58) -> Option<NodeNudgePlan> {
59    if interaction.keyboard_interaction().disable_keyboard_a11y {
60        return None;
61    }
62
63    let primary = nudge_primary(graph, view_state, interaction)?;
64    let delta = nudge_delta(view_state, interaction, request)?;
65    let (delta, items, transaction) = plan_node_move_delta(
66        graph,
67        view_state,
68        interaction,
69        primary,
70        delta,
71        NODE_NUDGE_TRANSACTION_LABEL,
72    )?;
73
74    Some(NodeNudgePlan::new(
75        request.direction,
76        delta,
77        items,
78        transaction,
79    ))
80}
81
82fn plan_node_move_delta(
83    graph: &Graph,
84    view_state: &NodeGraphViewState,
85    interaction: &NodeGraphInteractionState,
86    primary: NodeId,
87    delta: CanvasPoint,
88    label: &'static str,
89) -> Option<(CanvasPoint, Vec<NodeDragItem>, GraphTransaction)> {
90    if !delta.is_finite() || delta == CanvasPoint::default() {
91        return None;
92    }
93
94    let candidates = drag_candidates(graph, view_state, interaction, primary);
95    if !candidates.iter().any(|item| item.node == primary) {
96        return None;
97    }
98    let delta = snapped_delta(interaction, &candidates, delta);
99    if !delta.is_finite() || delta == CanvasPoint::default() {
100        return None;
101    }
102    let items = drag_items(interaction, &candidates, delta);
103    if items.is_empty() || items.iter().all(|item| item.from == item.to) {
104        return None;
105    }
106    let mut ops = items
107        .iter()
108        .map(|item| GraphOp::SetNodePos {
109            id: item.node,
110            from: item.from,
111            to: item.to,
112        })
113        .collect::<Vec<_>>();
114    ops.extend(parent_expansion_ops(graph, &candidates, &items));
115    let transaction = GraphTransaction::from_ops(ops).with_label(label);
116
117    Some((delta, items, transaction))
118}
119
120fn nudge_primary(
121    graph: &Graph,
122    view_state: &NodeGraphViewState,
123    interaction: &NodeGraphInteractionState,
124) -> Option<NodeId> {
125    let mut nodes = view_state.selected_nodes.clone();
126    nodes.sort();
127    nodes.dedup();
128    nodes.into_iter().find(|node| {
129        drag_candidates(graph, view_state, interaction, *node)
130            .iter()
131            .any(|candidate| candidate.node == *node)
132    })
133}
134
135fn nudge_delta(
136    view_state: &NodeGraphViewState,
137    interaction: &NodeGraphInteractionState,
138    request: NodeNudgeRequest,
139) -> Option<CanvasPoint> {
140    let direction = request.direction.unit_delta();
141    let keyboard = interaction.keyboard_interaction();
142    let node_drag = interaction.node_drag_interaction();
143    let (step_x, step_y) = match keyboard.nudge_step_mode {
144        NodeGraphNudgeStepMode::ScreenPx => {
145            let step_px = if request.fast {
146                keyboard.nudge_fast_step_px
147            } else {
148                keyboard.nudge_step_px
149            };
150            let zoom = if view_state.zoom.is_finite() && view_state.zoom > 0.0 {
151                view_state.zoom
152            } else {
153                1.0
154            };
155            let step = step_px / zoom;
156            (step, step)
157        }
158        NodeGraphNudgeStepMode::Grid => {
159            let grid = node_drag.snap_grid;
160            if !grid.is_positive_finite() {
161                return None;
162            }
163            let factor = if request.fast { 4.0 } else { 1.0 };
164            (grid.width * factor, grid.height * factor)
165        }
166    };
167
168    finite_positive_step(step_x, step_y)?;
169    Some(CanvasPoint {
170        x: direction.x * step_x,
171        y: direction.y * step_y,
172    })
173}
174
175fn finite_positive_step(step_x: f32, step_y: f32) -> Option<()> {
176    (step_x.is_finite() && step_x > 0.0 && step_y.is_finite() && step_y > 0.0).then_some(())
177}