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