Skip to main content

jellyflow_runtime/runtime/utils/
bounds.rs

1use crate::node_origin::{normalize_node_origin, resolve_node_origin};
2use crate::runtime::geometry::CanvasBounds;
3use crate::runtime::lookups::{NodeGraphLookups, NodeLookupEntry};
4use jellyflow_core::core::{CanvasPoint, CanvasRect, CanvasSize, NodeId};
5
6use super::options::{GetNodesBoundsOptions, GetNodesInsideOptions, NodeInclusion};
7
8/// Returns the top-left position for a node, taking node origin into account.
9///
10/// This mirrors XyFlow's `getNodePositionWithOrigin` utility.
11pub fn get_node_position_with_origin(
12    lookups: &NodeGraphLookups,
13    node: NodeId,
14    node_origin: (f32, f32),
15    fallback_size: Option<CanvasSize>,
16) -> Option<CanvasPoint> {
17    node_bounds(lookups, node, node_origin, fallback_size).map(CanvasBounds::top_left)
18}
19
20/// Returns the node's canvas-space bounding rect.
21pub fn get_node_rect(
22    lookups: &NodeGraphLookups,
23    node: NodeId,
24    node_origin: (f32, f32),
25    fallback_size: Option<CanvasSize>,
26) -> Option<CanvasRect> {
27    node_bounds(lookups, node, node_origin, fallback_size).map(CanvasBounds::to_rect)
28}
29
30/// Computes the bounding rect enclosing the given nodes.
31///
32/// Returns `None` when no nodes contribute a valid rect (e.g. all nodes are missing sizes and
33/// no `fallback_size` is provided).
34pub fn get_nodes_bounds(
35    lookups: &NodeGraphLookups,
36    nodes: impl IntoIterator<Item = NodeId>,
37    options: GetNodesBoundsOptions,
38) -> Option<CanvasRect> {
39    let resolver = NodeBoundsResolver::from_bounds_options(options);
40    let mut bounds: Option<CanvasBounds> = None;
41
42    for node in nodes {
43        let Some(entry) = lookups.node_lookup.get(&node) else {
44            continue;
45        };
46        let Some(node_bounds) = resolver.bounds_for_entry(entry) else {
47            continue;
48        };
49        bounds = Some(match bounds {
50            Some(current) => current.union(node_bounds),
51            None => node_bounds,
52        });
53    }
54
55    bounds.map(CanvasBounds::to_rect)
56}
57
58/// Returns the nodes that are inside the given query rect.
59pub fn get_nodes_inside(
60    lookups: &NodeGraphLookups,
61    rect: CanvasRect,
62    options: GetNodesInsideOptions,
63) -> Vec<NodeId> {
64    let resolver = NodeBoundsResolver::from_inside_options(options);
65    if !rect.is_positive_finite() {
66        return Vec::new();
67    }
68
69    let Some(query) = CanvasBounds::from_rect(rect) else {
70        return Vec::new();
71    };
72
73    let mut out: Vec<NodeId> = Vec::new();
74    for (node, entry) in &lookups.node_lookup {
75        let Some(node_bounds) = resolver.bounds_for_entry(entry) else {
76            continue;
77        };
78
79        let keep = match options.inclusion {
80            NodeInclusion::Partial => query.intersects(node_bounds),
81            NodeInclusion::Full => query.contains(node_bounds),
82        };
83        if keep {
84            out.push(*node);
85        }
86    }
87
88    out.sort();
89    out
90}
91
92fn node_bounds(
93    lookups: &NodeGraphLookups,
94    node: NodeId,
95    node_origin: (f32, f32),
96    fallback_size: Option<CanvasSize>,
97) -> Option<CanvasBounds> {
98    let entry = lookups.node_lookup.get(&node)?;
99    NodeBoundsResolver::include_hidden(node_origin, fallback_size).bounds_for_entry(entry)
100}
101
102struct NodeBoundsResolver {
103    node_origin: (f32, f32),
104    fallback_size: Option<CanvasSize>,
105    include_hidden: bool,
106}
107
108impl NodeBoundsResolver {
109    fn include_hidden(node_origin: (f32, f32), fallback_size: Option<CanvasSize>) -> Self {
110        Self {
111            node_origin: normalize_node_origin(node_origin),
112            fallback_size,
113            include_hidden: true,
114        }
115    }
116
117    fn from_bounds_options(options: GetNodesBoundsOptions) -> Self {
118        Self {
119            node_origin: normalize_node_origin(options.node_origin),
120            fallback_size: options.fallback_size,
121            include_hidden: options.include_hidden,
122        }
123    }
124
125    fn from_inside_options(options: GetNodesInsideOptions) -> Self {
126        Self {
127            node_origin: normalize_node_origin(options.node_origin),
128            fallback_size: options.fallback_size,
129            include_hidden: options.include_hidden,
130        }
131    }
132
133    fn bounds_for_entry(&self, entry: &NodeLookupEntry) -> Option<CanvasBounds> {
134        if !entry.is_visible_with_hidden_policy(self.include_hidden) {
135            return None;
136        }
137        let node_origin = resolve_node_origin(entry.origin, self.node_origin);
138        CanvasBounds::from_node(
139            entry.pos,
140            entry.resolved_size(self.fallback_size)?,
141            node_origin,
142        )
143    }
144}