Skip to main content

jellyflow_runtime/runtime/
layout.rs

1//! Headless automatic layout integration.
2//!
3//! This module is a thin runtime facade over `jellyflow-layout`: it turns a layout request into a
4//! normal graph transaction, then lets the store's dispatch/profile pipeline apply it.
5
6use jellyflow_core::core::Graph;
7use jellyflow_core::ops::GraphTransaction;
8pub use jellyflow_layout::{
9    DUGONG_LAYOUT_ENGINE_ID, DugongLayoutEngine, LAYERED_DAG_LAYOUT_FAMILY_ID, LayoutContext,
10    LayoutDirection, LayoutEdgeRoute, LayoutEngine, LayoutEngineCapability, LayoutEngineId,
11    LayoutEngineMetadata, LayoutEngineRegistry, LayoutEngineRequest, LayoutError, LayoutFamilyId,
12    LayoutFamilyMetadata, LayoutNodePosition, LayoutOptions, LayoutRequest, LayoutResult,
13    LayoutScope, LayoutSpacing, MIND_MAP_FREEFORM_LAYOUT_ENGINE_ID, MIND_MAP_LAYOUT_FAMILY_ID,
14    MIND_MAP_RADIAL_LAYOUT_ENGINE_ID, MindMapFreeformLayoutEngine, MindMapRadialLayoutEngine,
15    builtin_layout_engine_registry, layout_graph_to_transaction_with_dugong,
16    layout_graph_to_transaction_with_engine, layout_graph_to_transaction_with_mind_map_freeform,
17    layout_graph_to_transaction_with_mind_map_radial, layout_graph_with_dugong,
18    layout_graph_with_engine, layout_graph_with_mind_map_freeform,
19    layout_graph_with_mind_map_radial,
20};
21
22use crate::runtime::store::{DispatchError, DispatchOutcome, NodeGraphStore};
23
24/// Result of applying a layout engine through the store.
25#[derive(Debug, Clone)]
26pub struct LayoutApplyOutcome {
27    pub layout: LayoutResult,
28    pub dispatch: Option<DispatchOutcome>,
29}
30
31impl LayoutApplyOutcome {
32    pub fn committed(&self) -> Option<&GraphTransaction> {
33        self.dispatch.as_ref().map(DispatchOutcome::committed)
34    }
35}
36
37/// Errors from planning or dispatching a layout engine.
38#[derive(Debug, thiserror::Error)]
39pub enum LayoutApplyError {
40    #[error(transparent)]
41    Layout(#[from] LayoutError),
42    #[error(transparent)]
43    Dispatch(#[from] DispatchError),
44}
45
46/// Result of applying a dugong layout through the store.
47#[derive(Debug, Clone)]
48pub struct DugongLayoutApplyOutcome {
49    pub layout: LayoutResult,
50    pub dispatch: Option<DispatchOutcome>,
51}
52
53impl DugongLayoutApplyOutcome {
54    pub fn committed(&self) -> Option<&GraphTransaction> {
55        self.dispatch.as_ref().map(DispatchOutcome::committed)
56    }
57}
58
59/// Errors from planning or dispatching a dugong layout.
60#[derive(Debug, thiserror::Error)]
61pub enum DugongLayoutApplyError {
62    #[error(transparent)]
63    Layout(#[from] LayoutError),
64    #[error(transparent)]
65    Dispatch(#[from] DispatchError),
66}
67
68impl From<LayoutApplyOutcome> for DugongLayoutApplyOutcome {
69    fn from(outcome: LayoutApplyOutcome) -> Self {
70        Self {
71            layout: outcome.layout,
72            dispatch: outcome.dispatch,
73        }
74    }
75}
76
77impl From<LayoutApplyError> for DugongLayoutApplyError {
78    fn from(error: LayoutApplyError) -> Self {
79        match error {
80            LayoutApplyError::Layout(error) => Self::Layout(error),
81            LayoutApplyError::Dispatch(error) => Self::Dispatch(error),
82        }
83    }
84}
85
86/// Builds a layout context from non-persisted runtime facts already known by the store.
87pub fn layout_context_from_store(store: &NodeGraphStore) -> LayoutContext {
88    let measured_node_sizes = store.graph().nodes.keys().filter_map(|node| {
89        store
90            .node_measurement(*node)
91            .and_then(|measurement| measurement.size.map(|size| (*node, size)))
92    });
93    let node_origin = store.resolved_interaction_state().node_origin.normalized();
94
95    LayoutContext::new()
96        .with_measured_node_sizes(measured_node_sizes)
97        .with_node_origin((node_origin.x, node_origin.y))
98}
99
100/// Runs a selected layout engine for a graph without mutating runtime state.
101pub fn plan_layout(
102    graph: &Graph,
103    request: &LayoutEngineRequest,
104    registry: &LayoutEngineRegistry,
105    context: &LayoutContext,
106) -> Result<LayoutResult, LayoutError> {
107    layout_graph_with_engine(graph, request, registry, context)
108}
109
110/// Runs a selected layout engine and returns the transaction that would move changed nodes.
111pub fn layout_transaction(
112    graph: &Graph,
113    request: &LayoutEngineRequest,
114    registry: &LayoutEngineRegistry,
115    context: &LayoutContext,
116) -> Result<GraphTransaction, LayoutError> {
117    layout_graph_to_transaction_with_engine(graph, request, registry, context)
118}
119
120/// Runs a selected layout engine and commits the resulting transaction through normal store dispatch.
121pub fn apply_layout(
122    store: &mut NodeGraphStore,
123    request: &LayoutEngineRequest,
124    registry: &LayoutEngineRegistry,
125) -> Result<LayoutApplyOutcome, LayoutApplyError> {
126    let context = layout_context_from_store(store);
127    let layout = plan_layout(store.graph(), request, registry, &context)?;
128    let tx = layout.to_transaction(store.graph())?;
129    let dispatch = if tx.is_empty() {
130        None
131    } else {
132        Some(store.dispatch_transaction(&tx)?)
133    };
134
135    Ok(LayoutApplyOutcome { layout, dispatch })
136}
137
138/// Runs dugong layout for a graph without mutating runtime state.
139pub fn plan_dugong_layout(
140    graph: &Graph,
141    request: &LayoutRequest,
142) -> Result<LayoutResult, LayoutError> {
143    layout_graph_with_dugong(graph, request)
144}
145
146/// Runs dugong layout and returns the transaction that would move changed nodes.
147pub fn dugong_layout_transaction(
148    graph: &Graph,
149    request: &LayoutRequest,
150) -> Result<GraphTransaction, LayoutError> {
151    plan_dugong_layout(graph, request)?.to_transaction(graph)
152}
153
154/// Runs dugong layout and commits the resulting transaction through normal store dispatch.
155pub fn apply_dugong_layout(
156    store: &mut NodeGraphStore,
157    request: &LayoutRequest,
158) -> Result<DugongLayoutApplyOutcome, DugongLayoutApplyError> {
159    let registry = builtin_layout_engine_registry();
160    let request = LayoutEngineRequest::dugong(request.clone());
161    apply_layout(store, &request, &registry)
162        .map(Into::into)
163        .map_err(Into::into)
164}
165
166impl NodeGraphStore {
167    /// Builds a layout context from non-persisted runtime facts already known by this store.
168    pub fn layout_context(&self) -> LayoutContext {
169        layout_context_from_store(self)
170    }
171
172    /// Runs a selected layout engine for the current graph without mutating the store.
173    pub fn plan_layout(
174        &self,
175        request: &LayoutEngineRequest,
176        registry: &LayoutEngineRegistry,
177    ) -> Result<LayoutResult, LayoutError> {
178        let context = self.layout_context();
179        plan_layout(self.graph(), request, registry, &context)
180    }
181
182    /// Runs a selected layout engine and returns the transaction that would move changed nodes.
183    pub fn layout_transaction(
184        &self,
185        request: &LayoutEngineRequest,
186        registry: &LayoutEngineRegistry,
187    ) -> Result<GraphTransaction, LayoutError> {
188        let context = self.layout_context();
189        layout_transaction(self.graph(), request, registry, &context)
190    }
191
192    /// Runs a selected layout engine and commits the resulting transaction through normal dispatch.
193    pub fn apply_layout(
194        &mut self,
195        request: &LayoutEngineRequest,
196        registry: &LayoutEngineRegistry,
197    ) -> Result<LayoutApplyOutcome, LayoutApplyError> {
198        apply_layout(self, request, registry)
199    }
200
201    /// Runs dugong layout for the current graph without mutating the store.
202    pub fn plan_dugong_layout(&self, request: &LayoutRequest) -> Result<LayoutResult, LayoutError> {
203        let registry = builtin_layout_engine_registry();
204        let request = LayoutEngineRequest::dugong(request.clone());
205        self.plan_layout(&request, &registry)
206    }
207
208    /// Runs dugong layout and returns the transaction that would move changed nodes.
209    pub fn dugong_layout_transaction(
210        &self,
211        request: &LayoutRequest,
212    ) -> Result<GraphTransaction, LayoutError> {
213        let registry = builtin_layout_engine_registry();
214        let request = LayoutEngineRequest::dugong(request.clone());
215        self.layout_transaction(&request, &registry)
216    }
217
218    /// Runs dugong layout and commits the resulting transaction through normal store dispatch.
219    pub fn apply_dugong_layout(
220        &mut self,
221        request: &LayoutRequest,
222    ) -> Result<DugongLayoutApplyOutcome, DugongLayoutApplyError> {
223        apply_dugong_layout(self, request)
224    }
225}