1use crate::Result;
2use crate::math::MathRenderer;
3use crate::model::{LayoutCluster, SequenceDiagramLayout};
4use crate::text::{TextMeasurer, TextStyle};
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;
13mod 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::{config_f64, config_string};
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 seq_cfg = effective_config.get("sequence").unwrap_or(&Value::Null);
82 let diagram_margin_x = config_f64(seq_cfg, &["diagramMarginX"]).unwrap_or(50.0);
83 let diagram_margin_y = config_f64(seq_cfg, &["diagramMarginY"]).unwrap_or(10.0);
84 let bottom_margin_adj = config_f64(seq_cfg, &["bottomMarginAdj"]).unwrap_or(1.0);
85 let box_margin = config_f64(seq_cfg, &["boxMargin"]).unwrap_or(10.0);
86 let actor_margin = config_f64(seq_cfg, &["actorMargin"]).unwrap_or(50.0);
87 let sequence_default_width = config_f64(seq_cfg, &["width"]).unwrap_or(150.0);
88 let actor_width_min = sequence_default_width;
89 let actor_height = config_f64(seq_cfg, &["height"]).unwrap_or(65.0);
90 let message_margin = config_f64(seq_cfg, &["messageMargin"]).unwrap_or(35.0);
91 let wrap_padding = config_f64(seq_cfg, &["wrapPadding"]).unwrap_or(10.0);
92 let box_text_margin = config_f64(seq_cfg, &["boxTextMargin"]).unwrap_or(5.0);
93 let label_box_height = config_f64(seq_cfg, &["labelBoxHeight"]).unwrap_or(20.0);
94 let mirror_actors = seq_cfg
95 .get("mirrorActors")
96 .and_then(|v| v.as_bool())
97 .unwrap_or(true);
98
99 let global_font_family = config_string(effective_config, &["fontFamily"]);
102 let global_font_size = config_f64(effective_config, &["fontSize"]);
103 let global_font_weight = config_string(effective_config, &["fontWeight"]);
104
105 let message_font_family = global_font_family
106 .clone()
107 .or_else(|| config_string(seq_cfg, &["messageFontFamily"]));
108 let message_font_size = global_font_size
109 .or_else(|| config_f64(seq_cfg, &["messageFontSize"]))
110 .unwrap_or(16.0);
111 let message_font_weight = global_font_weight
112 .clone()
113 .or_else(|| config_string(seq_cfg, &["messageFontWeight"]));
114
115 let actor_font_family = global_font_family
116 .clone()
117 .or_else(|| config_string(seq_cfg, &["actorFontFamily"]));
118 let actor_font_size = global_font_size
119 .or_else(|| config_f64(seq_cfg, &["actorFontSize"]))
120 .unwrap_or(16.0);
121 let actor_font_weight = global_font_weight
122 .clone()
123 .or_else(|| config_string(seq_cfg, &["actorFontWeight"]));
124
125 let message_width_scale = 1.0;
129
130 let actor_text_style = TextStyle {
131 font_family: actor_font_family,
132 font_size: actor_font_size,
133 font_weight: actor_font_weight,
134 };
135 let note_font_family = global_font_family
136 .clone()
137 .or_else(|| config_string(seq_cfg, &["noteFontFamily"]));
138 let note_font_size = global_font_size
139 .or_else(|| config_f64(seq_cfg, &["noteFontSize"]))
140 .unwrap_or(16.0);
141 let note_font_weight = global_font_weight
142 .clone()
143 .or_else(|| config_string(seq_cfg, &["noteFontWeight"]));
144 let note_text_style = TextStyle {
145 font_family: note_font_family,
146 font_size: note_font_size,
147 font_weight: note_font_weight,
148 };
149 let msg_text_style = TextStyle {
150 font_family: message_font_family,
151 font_size: message_font_size,
152 font_weight: message_font_weight,
153 };
154
155 let SequenceActorLayoutPlan {
156 actor_index,
157 actor_widths,
158 actor_base_heights,
159 actor_box,
160 actor_left_x,
161 actor_centers_x,
162 box_margins,
163 actor_top_offset_y,
164 max_actor_layout_height,
165 has_boxes,
166 } = plan_sequence_actors(SequenceActorLayoutPlanContext {
167 model,
168 measurer,
169 actor_text_style: &actor_text_style,
170 note_text_style: ¬e_text_style,
171 msg_text_style: &msg_text_style,
172 math_config: &math_config,
173 math_renderer,
174 actor_width_min,
175 actor_height,
176 actor_margin,
177 actor_font_size,
178 box_margin,
179 box_text_margin,
180 wrap_padding,
181 message_width_scale,
182 message_font_size,
183 })?;
184
185 let message_text_line_height = sequence_text_dimensions_height_px(message_font_size);
186 let message_step = box_margin + 2.0 * message_text_line_height;
187 let msg_label_offset = (2.0 * message_text_line_height - wrap_padding / 2.0).max(0.0);
188
189 let clusters: Vec<LayoutCluster> = Vec::new();
190
191 let activation_width = config_f64(seq_cfg, &["activationWidth"])
192 .unwrap_or(10.0)
193 .max(1.0);
194 let SequenceLayoutGraph {
195 mut nodes,
196 edges,
197 bottom_box_top_y,
198 } = build_sequence_layout_graph(SequenceLayoutGraphContext {
199 model,
200 actor_index: &actor_index,
201 actor_centers_x: &actor_centers_x,
202 actor_widths: &actor_widths,
203 actor_base_heights: &actor_base_heights,
204 actor_top_offset_y,
205 max_actor_layout_height,
206 actor_width_min,
207 sequence_default_width,
208 actor_height,
209 message_margin,
210 box_margin,
211 box_text_margin,
212 bottom_margin_adj,
213 label_box_height,
214 message_step,
215 message_text_line_height,
216 msg_label_offset,
217 message_font_size,
218 message_width_scale,
219 wrap_padding,
220 mirror_actors,
221 activation_width,
222 measurer,
223 msg_text_style: &msg_text_style,
224 note_text_style: ¬e_text_style,
225 math_config: &math_config,
226 math_renderer,
227 });
228
229 let block_bounds = sequence_block_bounds(model, &nodes, &edges);
234
235 let rect_x_bounds = sequence_rect_stack_x_bounds(
236 model,
237 &actor_index,
238 &actor_centers_x,
239 &edges,
240 &nodes,
241 actor_width_min,
242 box_margin,
243 );
244 if !rect_x_bounds.is_empty() {
245 for n in &mut nodes {
246 let Some(start_id) = n.id.strip_prefix("rect-") else {
247 continue;
248 };
249 let Some((min_x, max_x)) = rect_x_bounds.get(start_id).copied() else {
250 continue;
251 };
252 n.x = (min_x + max_x) / 2.0;
253 n.width = (max_x - min_x).max(1.0);
254 }
255 }
256
257 let bounds = Some(sequence_root_bounds(SequenceRootBoundsContext {
258 model,
259 diagram_title,
260 nodes: &nodes,
261 edges: &edges,
262 block_bounds,
263 actor_index: &actor_index,
264 actor_centers_x: &actor_centers_x,
265 actor_left_x: &actor_left_x,
266 actor_widths: &actor_widths,
267 actor_box: &actor_box,
268 box_margins: &box_margins,
269 actor_width_min,
270 actor_height,
271 bottom_box_top_y,
272 diagram_margin_x,
273 diagram_margin_y,
274 bottom_margin_adj,
275 box_margin,
276 wrap_padding,
277 has_boxes,
278 mirror_actors,
279 measurer,
280 msg_text_style: &msg_text_style,
281 note_text_style: ¬e_text_style,
282 math_config: &math_config,
283 math_renderer,
284 }));
285
286 Ok(SequenceDiagramLayout {
287 nodes,
288 edges,
289 clusters,
290 bounds,
291 })
292}
293
294pub(crate) fn sequence_render_title<'a>(
295 model_title: Option<&'a str>,
296 diagram_title: Option<&'a str>,
297) -> Option<&'a str> {
298 if model_title.is_none_or(|t| t.trim().is_empty()) {
299 if let Some(title) = diagram_title.map(str::trim).filter(|t| !t.is_empty()) {
300 return Some(title);
301 }
302 }
303 model_title
304}