jellyflow_runtime/runtime/utils/
bounds.rs1use 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
8pub 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
20pub 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
30pub 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
58pub 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}