Skip to main content

jellyflow_runtime/runtime/
measurement.rs

1//! Renderer-neutral measurement facts reported by adapters.
2//!
3//! The graph document remains the persisted source of truth. Measurements live in runtime lookups
4//! so adapters can report layout facts once and reuse shared rendering, endpoint, and connection
5//! target behavior without copying geometry rules.
6
7use serde::{Deserialize, Serialize};
8
9use crate::runtime::connection::{
10    ConnectionHandleRef, ConnectionTargetCandidate, ResolvedConnectionTarget,
11};
12use crate::runtime::geometry::{EdgePosition, HandleBounds};
13use crate::runtime::rendering::RenderingQueryResult;
14use crate::runtime::store::NodeGraphStore;
15use jellyflow_core::core::{CanvasPoint, CanvasSize, EdgeId, NodeId};
16
17/// One measured handle attached to a node.
18#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
19pub struct MeasuredHandle {
20    pub handle: ConnectionHandleRef,
21    pub bounds: HandleBounds,
22}
23
24impl MeasuredHandle {
25    pub fn new(handle: ConnectionHandleRef, bounds: HandleBounds) -> Self {
26        Self { handle, bounds }
27    }
28}
29
30/// Renderer-neutral measurement facts for one node.
31#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
32pub struct NodeMeasurement {
33    pub node: NodeId,
34    #[serde(default, skip_serializing_if = "Option::is_none")]
35    pub size: Option<CanvasSize>,
36    #[serde(default, skip_serializing_if = "Vec::is_empty")]
37    pub handles: Vec<MeasuredHandle>,
38}
39
40impl NodeMeasurement {
41    pub fn new(node: NodeId) -> Self {
42        Self {
43            node,
44            size: None,
45            handles: Vec::new(),
46        }
47    }
48
49    pub fn with_size(mut self, size: Option<CanvasSize>) -> Self {
50        self.size = size;
51        self
52    }
53
54    pub fn with_handles(mut self, handles: impl IntoIterator<Item = MeasuredHandle>) -> Self {
55        self.handles = handles.into_iter().collect();
56        self
57    }
58}
59
60/// Result of applying measurement facts to runtime lookups.
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub enum NodeMeasurementOutcome {
63    Changed,
64    Unchanged,
65}
66
67impl NodeMeasurementOutcome {
68    pub fn changed(self) -> bool {
69        matches!(self, Self::Changed)
70    }
71}
72
73#[derive(Debug, thiserror::Error)]
74pub enum NodeMeasurementError {
75    #[error("measurement target node does not exist: {0:?}")]
76    MissingNode(NodeId),
77    #[error("measurement size is not positive and finite for node {node:?}: {size:?}")]
78    InvalidSize { node: NodeId, size: CanvasSize },
79    #[error("measurement handle does not belong to node {node:?}: {handle:?}")]
80    InvalidHandle {
81        node: NodeId,
82        handle: ConnectionHandleRef,
83    },
84    #[error("measurement handle bounds are not positive and finite for node {node:?}: {handle:?}")]
85    InvalidHandleBounds {
86        node: NodeId,
87        handle: ConnectionHandleRef,
88    },
89}
90
91/// Resolved endpoint geometry for one visible edge in a layout-facts query.
92#[derive(Debug, Clone, Copy, PartialEq)]
93pub struct LayoutEdgePosition {
94    pub edge: EdgeId,
95    pub position: EdgePosition,
96}
97
98impl LayoutEdgePosition {
99    pub fn new(edge: EdgeId, position: EdgePosition) -> Self {
100        Self { edge, position }
101    }
102}
103
104/// Store-level layout facts derived from the graph, view state, and reported measurements.
105#[derive(Debug, Clone, PartialEq)]
106pub struct LayoutFactsQueryResult {
107    pub revision: u64,
108    pub rendering: RenderingQueryResult,
109    pub visible_edge_positions: Vec<LayoutEdgePosition>,
110    pub connection_target_candidates: Vec<ConnectionTargetCandidate>,
111}
112
113impl LayoutFactsQueryResult {
114    pub fn new(
115        revision: u64,
116        rendering: RenderingQueryResult,
117        visible_edge_positions: Vec<LayoutEdgePosition>,
118        connection_target_candidates: Vec<ConnectionTargetCandidate>,
119    ) -> Self {
120        Self {
121            revision,
122            rendering,
123            visible_edge_positions,
124            connection_target_candidates,
125        }
126    }
127
128    pub fn visible_edge_position(&self, edge: EdgeId) -> Option<EdgePosition> {
129        self.visible_edge_positions
130            .iter()
131            .find(|position| position.edge == edge)
132            .map(|position| position.position)
133    }
134}
135
136impl NodeGraphStore {
137    /// Applies non-persisted renderer measurements for one node.
138    pub fn report_node_measurement(
139        &mut self,
140        measurement: NodeMeasurement,
141    ) -> Result<NodeMeasurementOutcome, NodeMeasurementError> {
142        let measurement = self.validate_node_measurement(measurement)?;
143        let Some(entry) = self.lookups_mut().node_lookup.get_mut(&measurement.node) else {
144            return Err(NodeMeasurementError::MissingNode(measurement.node));
145        };
146
147        if entry.apply_measurement(&measurement) {
148            self.publish_layout_facts_changed();
149            Ok(NodeMeasurementOutcome::Changed)
150        } else {
151            Ok(NodeMeasurementOutcome::Unchanged)
152        }
153    }
154
155    /// Clears non-persisted measurements for one node.
156    pub fn clear_node_measurement(&mut self, node: NodeId) -> NodeMeasurementOutcome {
157        let Some(entry) = self.lookups_mut().node_lookup.get_mut(&node) else {
158            return NodeMeasurementOutcome::Unchanged;
159        };
160
161        if entry.clear_measurement() {
162            self.publish_layout_facts_changed();
163            NodeMeasurementOutcome::Changed
164        } else {
165            NodeMeasurementOutcome::Unchanged
166        }
167    }
168
169    /// Reads the current non-persisted measurement facts for one node.
170    pub fn node_measurement(&self, node: NodeId) -> Option<NodeMeasurement> {
171        self.lookups()
172            .node_lookup
173            .get(&node)
174            .and_then(|entry| entry.measurement(node))
175    }
176
177    /// Reads the adapter-facing layout facts for the current store state.
178    pub fn layout_facts_query(&self, viewport_size: CanvasSize) -> LayoutFactsQueryResult {
179        crate::runtime::query::layout_facts_query(self, viewport_size)
180    }
181
182    /// Builds renderer-neutral connection target candidates from reported handle measurements.
183    pub fn connection_target_candidates_from_layout_facts(&self) -> Vec<ConnectionTargetCandidate> {
184        crate::runtime::query::connection_target_candidates_from_layout_facts(self)
185    }
186
187    /// Resolves a connection target using the handle inventory previously reported by adapters.
188    pub fn resolve_connection_target_from_layout_facts(
189        &self,
190        pointer: CanvasPoint,
191        from: ConnectionHandleRef,
192    ) -> ResolvedConnectionTarget {
193        crate::runtime::query::resolve_connection_target_from_layout_facts(self, pointer, from)
194    }
195
196    /// Resolves edge endpoint geometry from graph endpoints plus reported measurement facts.
197    pub fn edge_position_from_layout_facts(&self, edge: EdgeId) -> Option<EdgePosition> {
198        crate::runtime::query::edge_position_from_layout_facts(self, edge)
199    }
200
201    fn validate_node_measurement(
202        &self,
203        measurement: NodeMeasurement,
204    ) -> Result<NodeMeasurement, NodeMeasurementError> {
205        if !self.graph().nodes.contains_key(&measurement.node) {
206            return Err(NodeMeasurementError::MissingNode(measurement.node));
207        }
208        if let Some(size) = measurement.size
209            && !size.is_positive_finite()
210        {
211            return Err(NodeMeasurementError::InvalidSize {
212                node: measurement.node,
213                size,
214            });
215        }
216
217        for measured in &measurement.handles {
218            if measured.handle.node != measurement.node {
219                return Err(NodeMeasurementError::InvalidHandle {
220                    node: measurement.node,
221                    handle: measured.handle,
222                });
223            }
224            if !measured.bounds.rect.is_positive_finite() {
225                return Err(NodeMeasurementError::InvalidHandleBounds {
226                    node: measurement.node,
227                    handle: measured.handle,
228                });
229            }
230            let Some(port) = self.graph().ports.get(&measured.handle.port) else {
231                return Err(NodeMeasurementError::InvalidHandle {
232                    node: measurement.node,
233                    handle: measured.handle,
234                });
235            };
236            if port.node != measurement.node || port.dir != measured.handle.direction {
237                return Err(NodeMeasurementError::InvalidHandle {
238                    node: measurement.node,
239                    handle: measured.handle,
240                });
241            }
242        }
243
244        Ok(measurement)
245    }
246}