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