Skip to main content

merman_render/
sequence.rs

1use crate::Result;
2use crate::math::MathRenderer;
3use crate::model::{LayoutCluster, SequenceDiagramLayout};
4use crate::text::TextMeasurer;
5use merman_core::MermaidConfig;
6use merman_core::diagrams::sequence::SequenceDiagramRenderModel;
7use serde_json::Value;
8
9mod activation;
10mod actors;
11mod block_bounds;
12mod block_steps;
13pub(crate) mod config;
14mod constants;
15mod messages;
16mod metrics;
17mod notes;
18mod orchestration;
19mod rect;
20mod root_bounds;
21
22pub(crate) use activation::{sequence_activation_stack_bounds, sequence_activation_start_x};
23pub(crate) use constants::{
24    SEQUENCE_FRAME_GEOM_PAD_PX, SEQUENCE_FRAME_SIDE_PAD_PX, SEQUENCE_MESSAGE_WRAP_SLACK_FACTOR,
25    SEQUENCE_SELF_MESSAGE_FRAME_EXTRA_Y_PX, sequence_actor_popup_panel_height,
26    sequence_text_dimensions_height_px, sequence_text_line_step_px,
27};
28pub(crate) use metrics::{SequenceMathHeightMode, measure_sequence_math_label};
29pub(crate) use notes::sequence_note_final_wrapped_lines;
30
31use actors::{SequenceActorLayoutPlan, SequenceActorLayoutPlanContext, plan_sequence_actors};
32use block_bounds::sequence_block_bounds;
33use config::SequenceLayoutSettings;
34use orchestration::{SequenceLayoutGraph, SequenceLayoutGraphContext, build_sequence_layout_graph};
35use rect::sequence_rect_stack_x_bounds;
36use root_bounds::{SequenceRootBoundsContext, sequence_root_bounds};
37
38pub fn layout_sequence_diagram(
39    semantic: &Value,
40    effective_config: &Value,
41    measurer: &dyn TextMeasurer,
42    math_renderer: Option<&(dyn MathRenderer + Send + Sync)>,
43) -> Result<SequenceDiagramLayout> {
44    layout_sequence_diagram_with_title(semantic, None, effective_config, measurer, math_renderer)
45}
46
47pub fn layout_sequence_diagram_with_title(
48    semantic: &Value,
49    diagram_title: Option<&str>,
50    effective_config: &Value,
51    measurer: &dyn TextMeasurer,
52    math_renderer: Option<&(dyn MathRenderer + Send + Sync)>,
53) -> Result<SequenceDiagramLayout> {
54    let model: SequenceDiagramRenderModel = crate::json::from_value_ref(semantic)?;
55    layout_sequence_diagram_typed_with_title(
56        &model,
57        diagram_title,
58        effective_config,
59        measurer,
60        math_renderer,
61    )
62}
63
64pub fn layout_sequence_diagram_typed(
65    model: &SequenceDiagramRenderModel,
66    effective_config: &Value,
67    measurer: &dyn TextMeasurer,
68    math_renderer: Option<&(dyn MathRenderer + Send + Sync)>,
69) -> Result<SequenceDiagramLayout> {
70    layout_sequence_diagram_typed_with_title(model, None, effective_config, measurer, math_renderer)
71}
72
73pub fn layout_sequence_diagram_typed_with_title(
74    model: &SequenceDiagramRenderModel,
75    diagram_title: Option<&str>,
76    effective_config: &Value,
77    measurer: &dyn TextMeasurer,
78    math_renderer: Option<&(dyn MathRenderer + Send + Sync)>,
79) -> Result<SequenceDiagramLayout> {
80    let math_config = MermaidConfig::from_value(effective_config.clone());
81    let settings = SequenceLayoutSettings::from_effective_config(effective_config);
82
83    let SequenceActorLayoutPlan {
84        actor_index,
85        actor_widths,
86        actor_base_heights,
87        actor_box,
88        actor_left_x,
89        actor_centers_x,
90        box_margins,
91        actor_top_offset_y,
92        max_actor_layout_height,
93        has_boxes,
94    } = plan_sequence_actors(SequenceActorLayoutPlanContext {
95        model,
96        measurer,
97        actor_text_style: &settings.actor_text_style,
98        note_text_style: &settings.note_text_style,
99        msg_text_style: &settings.msg_text_style,
100        math_config: &math_config,
101        math_renderer,
102        actor_width_min: settings.sequence_default_width,
103        actor_height: settings.actor_height,
104        actor_margin: settings.actor_margin,
105        actor_font_size: settings.actor_text_style.font_size,
106        box_margin: settings.box_margin,
107        box_text_margin: settings.box_text_margin,
108        wrap_padding: settings.wrap_padding,
109        message_width_scale: settings.message_width_scale,
110        message_font_size: settings.msg_text_style.font_size,
111    })?;
112
113    let clusters: Vec<LayoutCluster> = Vec::new();
114
115    let SequenceLayoutGraph {
116        mut nodes,
117        edges,
118        bottom_box_top_y,
119    } = build_sequence_layout_graph(SequenceLayoutGraphContext {
120        model,
121        actor_index: &actor_index,
122        actor_centers_x: &actor_centers_x,
123        actor_widths: &actor_widths,
124        actor_base_heights: &actor_base_heights,
125        actor_top_offset_y,
126        max_actor_layout_height,
127        actor_width_min: settings.sequence_default_width,
128        sequence_default_width: settings.sequence_default_width,
129        actor_height: settings.actor_height,
130        message_margin: settings.message_margin,
131        box_margin: settings.box_margin,
132        box_text_margin: settings.box_text_margin,
133        bottom_margin_adj: settings.bottom_margin_adj,
134        label_box_height: settings.label_box_height,
135        message_step: settings.message_step,
136        message_text_line_height: settings.message_text_line_height,
137        msg_label_offset: settings.msg_label_offset,
138        message_font_size: settings.msg_text_style.font_size,
139        message_width_scale: settings.message_width_scale,
140        wrap_padding: settings.wrap_padding,
141        mirror_actors: settings.mirror_actors,
142        activation_width: settings.activation_width,
143        measurer,
144        msg_text_style: &settings.msg_text_style,
145        note_text_style: &settings.note_text_style,
146        math_config: &math_config,
147        math_renderer,
148    });
149
150    // Mermaid's SVG `viewBox` is derived from `svg.getBBox()` plus diagram margins. Block frames
151    // (`alt`, `par`, `loop`, `opt`, `break`, `critical`) can extend beyond the node/edge graph we
152    // model in headless layout. Capture their extents so we can expand bounds before emitting the
153    // final `viewBox`.
154    let block_bounds = sequence_block_bounds(model, &nodes, &edges);
155
156    let rect_x_bounds = sequence_rect_stack_x_bounds(
157        model,
158        &actor_index,
159        &actor_centers_x,
160        &edges,
161        &nodes,
162        settings.sequence_default_width,
163        settings.box_margin,
164    );
165    if !rect_x_bounds.is_empty() {
166        for n in &mut nodes {
167            let Some(start_id) = n.id.strip_prefix("rect-") else {
168                continue;
169            };
170            let Some((min_x, max_x)) = rect_x_bounds.get(start_id).copied() else {
171                continue;
172            };
173            n.x = (min_x + max_x) / 2.0;
174            n.width = (max_x - min_x).max(1.0);
175        }
176    }
177
178    let bounds = Some(sequence_root_bounds(SequenceRootBoundsContext {
179        model,
180        diagram_title,
181        nodes: &nodes,
182        edges: &edges,
183        block_bounds,
184        actor_index: &actor_index,
185        actor_centers_x: &actor_centers_x,
186        actor_left_x: &actor_left_x,
187        actor_widths: &actor_widths,
188        actor_box: &actor_box,
189        box_margins: &box_margins,
190        actor_width_min: settings.sequence_default_width,
191        actor_height: settings.actor_height,
192        bottom_box_top_y,
193        diagram_margin_x: settings.diagram_margin_x,
194        diagram_margin_y: settings.diagram_margin_y,
195        bottom_margin_adj: settings.bottom_margin_adj,
196        box_margin: settings.box_margin,
197        wrap_padding: settings.wrap_padding,
198        has_boxes,
199        mirror_actors: settings.mirror_actors,
200        measurer,
201        msg_text_style: &settings.msg_text_style,
202        note_text_style: &settings.note_text_style,
203        math_config: &math_config,
204        math_renderer,
205    }));
206
207    Ok(SequenceDiagramLayout {
208        nodes,
209        edges,
210        clusters,
211        bounds,
212    })
213}
214
215pub(crate) fn sequence_render_title<'a>(
216    model_title: Option<&'a str>,
217    diagram_title: Option<&'a str>,
218) -> Option<&'a str> {
219    if model_title.is_none_or(|t| t.trim().is_empty())
220        && let Some(title) = diagram_title.map(str::trim).filter(|t| !t.is_empty())
221    {
222        return Some(title);
223    }
224    model_title
225}