1#![allow(clippy::too_many_arguments)]
2
3use crate::model::{
4 Bounds, LayoutCluster, LayoutEdge, LayoutLabel, LayoutNode, LayoutPoint, SequenceDiagramLayout,
5};
6use crate::text::{
7 TextMeasurer, TextStyle, WrapMode, split_html_br_lines, wrap_label_like_mermaid_lines,
8 wrap_label_like_mermaid_lines_floored_bbox,
9};
10use crate::{Error, Result};
11use serde::Deserialize;
12use serde_json::Value;
13
14#[derive(Debug, Clone, Deserialize)]
15struct SequenceActor {
16 #[allow(dead_code)]
17 name: String,
18 description: String,
19 #[serde(rename = "type")]
20 actor_type: String,
21 #[allow(dead_code)]
22 wrap: bool,
23}
24
25#[derive(Debug, Clone, Deserialize)]
26struct SequenceMessage {
27 id: String,
28 #[serde(default)]
29 from: Option<String>,
30 #[serde(default)]
31 to: Option<String>,
32 #[serde(rename = "type")]
33 message_type: i32,
34 message: Value,
35 #[allow(dead_code)]
36 wrap: bool,
37 activate: bool,
38 #[serde(default)]
39 placement: Option<i32>,
40}
41
42#[derive(Debug, Clone, Deserialize)]
43struct SequenceBox {
44 #[serde(rename = "actorKeys")]
45 actor_keys: Vec<String>,
46 #[allow(dead_code)]
47 fill: String,
48 name: Option<String>,
49 #[allow(dead_code)]
50 wrap: bool,
51}
52
53#[derive(Debug, Clone, Deserialize)]
54struct SequenceModel {
55 #[serde(rename = "actorOrder")]
56 actor_order: Vec<String>,
57 actors: std::collections::BTreeMap<String, SequenceActor>,
58 #[serde(default)]
59 boxes: Vec<SequenceBox>,
60 messages: Vec<SequenceMessage>,
61 title: Option<String>,
62 #[serde(rename = "createdActors", default)]
63 created_actors: std::collections::BTreeMap<String, usize>,
64 #[serde(rename = "destroyedActors", default)]
65 destroyed_actors: std::collections::BTreeMap<String, usize>,
66}
67
68fn config_f64(cfg: &Value, path: &[&str]) -> Option<f64> {
69 let mut cur = cfg;
70 for key in path {
71 cur = cur.get(*key)?;
72 }
73 cur.as_f64()
74 .or_else(|| cur.as_i64().map(|n| n as f64))
75 .or_else(|| cur.as_u64().map(|n| n as f64))
76}
77
78fn config_string(cfg: &Value, path: &[&str]) -> Option<String> {
79 let mut cur = cfg;
80 for key in path {
81 cur = cur.get(*key)?;
82 }
83 cur.as_str().map(|s| s.to_string())
84}
85
86fn measure_svg_like_with_html_br(
87 measurer: &dyn TextMeasurer,
88 text: &str,
89 style: &TextStyle,
90) -> (f64, f64) {
91 let lines = split_html_br_lines(text);
92 let default_line_height = (style.font_size.max(1.0) * 1.1).max(1.0);
93 if lines.len() <= 1 {
94 let metrics = measurer.measure_wrapped(text, style, None, WrapMode::SvgLikeSingleRun);
95 let h = if metrics.height > 0.0 {
96 metrics.height
97 } else {
98 default_line_height
99 };
100 return (metrics.width.max(0.0), h.max(0.0));
101 }
102 let mut max_w: f64 = 0.0;
103 let mut line_h: f64 = 0.0;
104 for line in &lines {
105 let metrics = measurer.measure_wrapped(line, style, None, WrapMode::SvgLikeSingleRun);
106 max_w = max_w.max(metrics.width.max(0.0));
107 let h = if metrics.height > 0.0 {
108 metrics.height
109 } else {
110 default_line_height
111 };
112 line_h = line_h.max(h.max(0.0));
113 }
114 (
115 max_w,
116 (line_h * lines.len() as f64).max(default_line_height),
117 )
118}
119
120fn sequence_actor_visual_height(
121 actor_type: &str,
122 base_width: f64,
123 base_height: f64,
124 label_box_height: f64,
125) -> f64 {
126 match actor_type {
127 "boundary" => (60.0 + label_box_height).max(1.0),
130 "database" => ((base_width / 4.0) + label_box_height).max(1.0),
133 "entity" => (36.0 + label_box_height).max(1.0),
134 "control" => (36.0 + 2.0 * label_box_height).max(1.0),
136 _ => base_height.max(1.0),
137 }
138}
139
140fn sequence_actor_lifeline_start_y(
141 actor_type: &str,
142 base_height: f64,
143 box_text_margin: f64,
144) -> f64 {
145 match actor_type {
146 "actor" | "boundary" => 80.0,
148 "control" | "entity" => 75.0,
149 "database" => base_height + 2.0 * box_text_margin,
151 _ => base_height,
152 }
153}
154
155pub fn layout_sequence_diagram(
156 semantic: &Value,
157 effective_config: &Value,
158 measurer: &dyn TextMeasurer,
159) -> Result<SequenceDiagramLayout> {
160 let model: SequenceModel = crate::json::from_value_ref(semantic)?;
161
162 let seq_cfg = effective_config.get("sequence").unwrap_or(&Value::Null);
163 let diagram_margin_x = config_f64(seq_cfg, &["diagramMarginX"]).unwrap_or(50.0);
164 let diagram_margin_y = config_f64(seq_cfg, &["diagramMarginY"]).unwrap_or(10.0);
165 let bottom_margin_adj = config_f64(seq_cfg, &["bottomMarginAdj"]).unwrap_or(1.0);
166 let box_margin = config_f64(seq_cfg, &["boxMargin"]).unwrap_or(10.0);
167 let actor_margin = config_f64(seq_cfg, &["actorMargin"]).unwrap_or(50.0);
168 let actor_width_min = config_f64(seq_cfg, &["width"]).unwrap_or(150.0);
169 let actor_height = config_f64(seq_cfg, &["height"]).unwrap_or(65.0);
170 let message_margin = config_f64(seq_cfg, &["messageMargin"]).unwrap_or(35.0);
171 let wrap_padding = config_f64(seq_cfg, &["wrapPadding"]).unwrap_or(10.0);
172 let box_text_margin = config_f64(seq_cfg, &["boxTextMargin"]).unwrap_or(5.0);
173 let label_box_height = config_f64(seq_cfg, &["labelBoxHeight"]).unwrap_or(20.0);
174 let mirror_actors = seq_cfg
175 .get("mirrorActors")
176 .and_then(|v| v.as_bool())
177 .unwrap_or(true);
178
179 let global_font_family = config_string(effective_config, &["fontFamily"]);
182 let global_font_size = config_f64(effective_config, &["fontSize"]);
183 let global_font_weight = config_string(effective_config, &["fontWeight"]);
184
185 let message_font_family = global_font_family
186 .clone()
187 .or_else(|| config_string(seq_cfg, &["messageFontFamily"]));
188 let message_font_size = global_font_size
189 .or_else(|| config_f64(seq_cfg, &["messageFontSize"]))
190 .unwrap_or(16.0);
191 let message_font_weight = global_font_weight
192 .clone()
193 .or_else(|| config_string(seq_cfg, &["messageFontWeight"]));
194
195 let actor_font_family = global_font_family
196 .clone()
197 .or_else(|| config_string(seq_cfg, &["actorFontFamily"]));
198 let actor_font_size = global_font_size
199 .or_else(|| config_f64(seq_cfg, &["actorFontSize"]))
200 .unwrap_or(16.0);
201 let actor_font_weight = global_font_weight
202 .clone()
203 .or_else(|| config_string(seq_cfg, &["actorFontWeight"]));
204
205 let message_width_scale = 1.0;
209
210 let actor_text_style = TextStyle {
211 font_family: actor_font_family,
212 font_size: actor_font_size,
213 font_weight: actor_font_weight,
214 };
215 let note_font_family = global_font_family
216 .clone()
217 .or_else(|| config_string(seq_cfg, &["noteFontFamily"]));
218 let note_font_size = global_font_size
219 .or_else(|| config_f64(seq_cfg, &["noteFontSize"]))
220 .unwrap_or(16.0);
221 let note_font_weight = global_font_weight
222 .clone()
223 .or_else(|| config_string(seq_cfg, &["noteFontWeight"]));
224 let note_text_style = TextStyle {
225 font_family: note_font_family,
226 font_size: note_font_size,
227 font_weight: note_font_weight,
228 };
229 let msg_text_style = TextStyle {
230 font_family: message_font_family,
231 font_size: message_font_size,
232 font_weight: message_font_weight,
233 };
234
235 let has_boxes = !model.boxes.is_empty();
236 let has_box_titles = model
237 .boxes
238 .iter()
239 .any(|b| b.name.as_deref().is_some_and(|s| !s.trim().is_empty()));
240
241 fn mermaid_text_dimensions_height_px(font_size: f64) -> f64 {
247 (font_size.max(1.0) * (17.0 / 16.0)).max(1.0)
249 }
250
251 let max_box_title_height = if has_box_titles {
252 let line_h = mermaid_text_dimensions_height_px(message_font_size);
253 model
254 .boxes
255 .iter()
256 .filter_map(|b| b.name.as_deref())
257 .map(|s| split_html_br_lines(s).len().max(1) as f64 * line_h)
258 .fold(0.0, f64::max)
259 } else {
260 0.0
261 };
262
263 if model.actor_order.is_empty() {
264 return Err(Error::InvalidModel {
265 message: "sequence model has no actorOrder".to_string(),
266 });
267 }
268
269 let mut actor_widths: Vec<f64> = Vec::with_capacity(model.actor_order.len());
271 let mut actor_base_heights: Vec<f64> = Vec::with_capacity(model.actor_order.len());
272 for id in &model.actor_order {
273 let a = model.actors.get(id).ok_or_else(|| Error::InvalidModel {
274 message: format!("missing actor {id}"),
275 })?;
276 if a.wrap {
277 let wrap_w = (actor_width_min - 2.0 * wrap_padding).max(1.0);
280 let wrapped_lines =
281 wrap_label_like_mermaid_lines(&a.description, measurer, &actor_text_style, wrap_w);
282 let line_count = wrapped_lines.len().max(1) as f64;
283 let text_h = mermaid_text_dimensions_height_px(actor_font_size) * line_count;
284 actor_base_heights.push(actor_height.max(text_h).max(1.0));
285 actor_widths.push(actor_width_min.max(1.0));
286 } else {
287 let (w0, _h0) =
288 measure_svg_like_with_html_br(measurer, &a.description, &actor_text_style);
289 let w = (w0 + 2.0 * wrap_padding).max(actor_width_min);
290 actor_base_heights.push(actor_height.max(1.0));
291 actor_widths.push(w.max(1.0));
292 }
293 }
294
295 let mut actor_index: std::collections::HashMap<&str, usize> = std::collections::HashMap::new();
298 for (i, id) in model.actor_order.iter().enumerate() {
299 actor_index.insert(id.as_str(), i);
300 }
301
302 let mut actor_to_message_width: Vec<f64> = vec![0.0; model.actor_order.len()];
303 for msg in &model.messages {
304 let (Some(from), Some(to)) = (msg.from.as_deref(), msg.to.as_deref()) else {
305 continue;
306 };
307 let Some(&from_idx) = actor_index.get(from) else {
308 continue;
309 };
310 let Some(&to_idx) = actor_index.get(to) else {
311 continue;
312 };
313
314 let placement = msg.placement;
315 if placement == Some(0) && to_idx == 0 {
317 continue;
318 }
319 if placement == Some(1) && to_idx + 1 == model.actor_order.len() {
321 continue;
322 }
323
324 let is_note = placement.is_some();
325 let is_message = !is_note;
326 let style = if is_note {
327 ¬e_text_style
328 } else {
329 &msg_text_style
330 };
331 let text = msg.message.as_str().unwrap_or_default();
332 if text.is_empty() {
333 continue;
334 }
335
336 let measured_text = if msg.wrap {
337 let wrap_w = (actor_width_min - 2.0 * wrap_padding).max(1.0);
340 let lines = wrap_label_like_mermaid_lines(text, measurer, style, wrap_w);
341 lines.join("<br>")
342 } else {
343 text.to_string()
344 };
345 let (w0, _h0) = measure_svg_like_with_html_br(measurer, &measured_text, style);
346 let w0 = w0 * message_width_scale;
347 let message_w = (w0 + 2.0 * wrap_padding).max(0.0);
348
349 let prev_idx = if to_idx > 0 { Some(to_idx - 1) } else { None };
350 let next_idx = if to_idx + 1 < model.actor_order.len() {
351 Some(to_idx + 1)
352 } else {
353 None
354 };
355
356 if is_message && next_idx.is_some_and(|n| n == from_idx) {
357 actor_to_message_width[to_idx] = actor_to_message_width[to_idx].max(message_w);
358 } else if is_message && prev_idx.is_some_and(|p| p == from_idx) {
359 actor_to_message_width[from_idx] = actor_to_message_width[from_idx].max(message_w);
360 } else if is_message && from_idx == to_idx {
361 let half = message_w / 2.0;
362 actor_to_message_width[from_idx] = actor_to_message_width[from_idx].max(half);
363 actor_to_message_width[to_idx] = actor_to_message_width[to_idx].max(half);
364 } else if placement == Some(1) {
365 actor_to_message_width[from_idx] = actor_to_message_width[from_idx].max(message_w);
367 } else if placement == Some(0) {
368 if let Some(p) = prev_idx {
370 actor_to_message_width[p] = actor_to_message_width[p].max(message_w);
371 }
372 } else if placement == Some(2) {
373 if let Some(p) = prev_idx {
375 actor_to_message_width[p] = actor_to_message_width[p].max(message_w / 2.0);
376 }
377 if next_idx.is_some() {
378 actor_to_message_width[from_idx] =
379 actor_to_message_width[from_idx].max(message_w / 2.0);
380 }
381 }
382 }
383
384 let mut actor_margins: Vec<f64> = vec![actor_margin; model.actor_order.len()];
385 for i in 0..model.actor_order.len() {
386 let msg_w = actor_to_message_width[i];
387 if msg_w <= 0.0 {
388 continue;
389 }
390 let w0 = actor_widths[i];
391 let actor_w = if i + 1 < model.actor_order.len() {
392 let w1 = actor_widths[i + 1];
393 msg_w + actor_margin - (w0 / 2.0) - (w1 / 2.0)
394 } else {
395 msg_w + actor_margin - (w0 / 2.0)
396 };
397 actor_margins[i] = actor_w.max(actor_margin);
398 }
399
400 let mut box_margins: Vec<f64> = vec![box_text_margin; model.boxes.len()];
404 for (box_idx, b) in model.boxes.iter().enumerate() {
405 let mut total_width = 0.0;
406 for actor_key in &b.actor_keys {
407 let Some(&i) = actor_index.get(actor_key.as_str()) else {
408 continue;
409 };
410 let actor_margin_for_box = if actor_to_message_width[i] > 0.0 {
411 actor_margins[i]
412 } else {
413 0.0
414 };
415 total_width += actor_widths[i] + actor_margin_for_box;
416 }
417
418 total_width += box_margin * 8.0;
419 total_width -= 2.0 * box_text_margin;
420
421 let Some(name) = b.name.as_deref().filter(|s| !s.trim().is_empty()) else {
422 continue;
423 };
424
425 let (text_w, _text_h) = measure_svg_like_with_html_br(measurer, name, &msg_text_style);
426 let min_width = total_width.max(text_w + 2.0 * wrap_padding);
427 if total_width < min_width {
428 box_margins[box_idx] += (min_width - total_width) / 2.0;
429 }
430 }
431
432 let mut actor_top_offset_y = 0.0;
434 if has_boxes {
435 actor_top_offset_y += box_margin;
436 if has_box_titles {
437 actor_top_offset_y += max_box_title_height;
438 }
439 }
440
441 let mut actor_box: Vec<Option<usize>> = vec![None; model.actor_order.len()];
443 for (box_idx, b) in model.boxes.iter().enumerate() {
444 for actor_key in &b.actor_keys {
445 let Some(&i) = actor_index.get(actor_key.as_str()) else {
446 continue;
447 };
448 actor_box[i] = Some(box_idx);
449 }
450 }
451
452 let mut actor_left_x: Vec<f64> = Vec::with_capacity(model.actor_order.len());
453 let mut prev_width = 0.0;
454 let mut prev_margin = 0.0;
455 let mut prev_box: Option<usize> = None;
456 for i in 0..model.actor_order.len() {
457 let w = actor_widths[i];
458 let cur_box = actor_box[i];
459
460 if prev_box.is_some() && prev_box != cur_box {
462 if let Some(prev) = prev_box {
463 prev_margin += box_margin + box_margins[prev];
464 }
465 }
466
467 if cur_box.is_some() && cur_box != prev_box {
469 if let Some(bi) = cur_box {
470 prev_margin += box_margins[bi];
471 }
472 }
473
474 if model.created_actors.contains_key(&model.actor_order[i]) {
476 prev_margin += w / 2.0;
477 }
478 let x = prev_width + prev_margin;
479 actor_left_x.push(x);
480 prev_width += w + prev_margin;
481 prev_margin = actor_margins[i];
482 prev_box = cur_box;
483 }
484
485 let mut actor_centers_x: Vec<f64> = Vec::with_capacity(model.actor_order.len());
486 for i in 0..model.actor_order.len() {
487 actor_centers_x.push(actor_left_x[i] + actor_widths[i] / 2.0);
488 }
489
490 let message_step = message_margin + (message_font_size / 2.0) + bottom_margin_adj;
491 let msg_label_offset = (message_step - message_font_size) + bottom_margin_adj;
492
493 let mut edges: Vec<LayoutEdge> = Vec::new();
494 let mut nodes: Vec<LayoutNode> = Vec::new();
495 let clusters: Vec<LayoutCluster> = Vec::new();
496
497 let mut max_actor_visual_height: f64 = 0.0;
501 for (idx, id) in model.actor_order.iter().enumerate() {
502 let w = actor_widths[idx];
503 let cx = actor_centers_x[idx];
504 let base_h = actor_base_heights[idx];
505 let actor_type = model
506 .actors
507 .get(id)
508 .map(|a| a.actor_type.as_str())
509 .unwrap_or("participant");
510 let visual_h = sequence_actor_visual_height(actor_type, w, base_h, label_box_height);
511 max_actor_visual_height = max_actor_visual_height.max(visual_h.max(1.0));
512 let top_y = actor_top_offset_y + visual_h / 2.0;
513 nodes.push(LayoutNode {
514 id: format!("actor-top-{id}"),
515 x: cx,
516 y: top_y,
517 width: w,
518 height: visual_h,
519 is_cluster: false,
520 label_width: None,
521 label_height: None,
522 });
523 }
524
525 fn bracketize(s: &str) -> String {
528 let t = s.trim();
529 if t.is_empty() {
530 return "\u{200B}".to_string();
531 }
532 if t.starts_with('[') && t.ends_with(']') {
533 return t.to_string();
534 }
535 format!("[{t}]")
536 }
537
538 fn block_label_text(raw_label: &str) -> String {
539 bracketize(raw_label)
540 }
541
542 let block_base_step = (2.0 * box_margin + box_text_margin + label_box_height).max(0.0);
556 let block_base_step_empty = (block_base_step - label_box_height).max(0.0);
557 let line_step = message_font_size * 1.1875;
558 let block_extra_per_line = (line_step - box_text_margin).max(0.0);
559 let block_end_step = 10.0;
560
561 let mut msg_by_id: std::collections::HashMap<&str, &SequenceMessage> =
562 std::collections::HashMap::new();
563 for msg in &model.messages {
564 msg_by_id.insert(msg.id.as_str(), msg);
565 }
566
567 fn is_self_message_id(
568 msg_id: &str,
569 msg_by_id: &std::collections::HashMap<&str, &SequenceMessage>,
570 ) -> bool {
571 let Some(msg) = msg_by_id.get(msg_id).copied() else {
572 return false;
573 };
574 if msg.message_type == 2 {
576 return false;
577 }
578 msg.from
579 .as_deref()
580 .is_some_and(|from| Some(from) == msg.to.as_deref())
581 }
582
583 fn message_span_x(
584 msg: &SequenceMessage,
585 actor_index: &std::collections::HashMap<&str, usize>,
586 actor_centers_x: &[f64],
587 measurer: &dyn TextMeasurer,
588 msg_text_style: &TextStyle,
589 message_width_scale: f64,
590 ) -> Option<(f64, f64)> {
591 let (Some(from), Some(to)) = (msg.from.as_deref(), msg.to.as_deref()) else {
592 return None;
593 };
594 let (Some(fi), Some(ti)) = (actor_index.get(from).copied(), actor_index.get(to).copied())
595 else {
596 return None;
597 };
598 let from_x = actor_centers_x[fi];
599 let to_x = actor_centers_x[ti];
600 let sign = if to_x >= from_x { 1.0 } else { -1.0 };
601 let x1 = from_x + sign * 1.0;
602 let x2 = if from == to { x1 } else { to_x - sign * 4.0 };
603 let cx = (x1 + x2) / 2.0;
604
605 let text = msg.message.as_str().unwrap_or_default();
606 let w = if text.is_empty() {
607 1.0
608 } else {
609 let (w, _h) = measure_svg_like_with_html_br(measurer, text, msg_text_style);
610 (w * message_width_scale).max(1.0)
611 };
612 Some((cx - w / 2.0, cx + w / 2.0))
613 }
614
615 fn block_frame_width(
616 message_ids: &[String],
617 msg_by_id: &std::collections::HashMap<&str, &SequenceMessage>,
618 actor_index: &std::collections::HashMap<&str, usize>,
619 actor_centers_x: &[f64],
620 actor_widths: &[f64],
621 message_margin: f64,
622 box_text_margin: f64,
623 bottom_margin_adj: f64,
624 measurer: &dyn TextMeasurer,
625 msg_text_style: &TextStyle,
626 message_width_scale: f64,
627 ) -> Option<f64> {
628 let mut actor_idxs: Vec<usize> = Vec::new();
629 for msg_id in message_ids {
630 let Some(msg) = msg_by_id.get(msg_id.as_str()).copied() else {
631 continue;
632 };
633 let (Some(from), Some(to)) = (msg.from.as_deref(), msg.to.as_deref()) else {
634 continue;
635 };
636 if let Some(i) = actor_index.get(from).copied() {
637 actor_idxs.push(i);
638 }
639 if let Some(i) = actor_index.get(to).copied() {
640 actor_idxs.push(i);
641 }
642 }
643 actor_idxs.sort();
644 actor_idxs.dedup();
645 if actor_idxs.is_empty() {
646 return None;
647 }
648
649 if actor_idxs.len() == 1 {
650 let i = actor_idxs[0];
651 let actor_w = actor_widths.get(i).copied().unwrap_or(150.0);
652 let half_width =
653 actor_w / 2.0 + (message_margin / 2.0) + box_text_margin + bottom_margin_adj;
654 let w = (2.0 * half_width).max(1.0);
655 return Some(w);
656 }
657
658 let min_i = actor_idxs.first().copied()?;
659 let max_i = actor_idxs.last().copied()?;
660 let mut x1 = actor_centers_x[min_i] - 11.0;
661 let mut x2 = actor_centers_x[max_i] + 11.0;
662
663 for msg_id in message_ids {
665 let Some(msg) = msg_by_id.get(msg_id.as_str()).copied() else {
666 continue;
667 };
668 let Some((l, r)) = message_span_x(
669 msg,
670 actor_index,
671 actor_centers_x,
672 measurer,
673 msg_text_style,
674 message_width_scale,
675 ) else {
676 continue;
677 };
678 if l < x1 {
679 x1 = l.floor();
680 }
681 if r > x2 {
682 x2 = r.ceil();
683 }
684 }
685
686 Some((x2 - x1).max(1.0))
687 }
688
689 #[derive(Debug, Clone)]
690 enum BlockStackEntry {
691 Loop {
692 start_id: String,
693 raw_label: String,
694 messages: Vec<String>,
695 },
696 Opt {
697 start_id: String,
698 raw_label: String,
699 messages: Vec<String>,
700 },
701 Break {
702 start_id: String,
703 raw_label: String,
704 messages: Vec<String>,
705 },
706 Alt {
707 section_directives: Vec<(String, String)>,
708 sections: Vec<Vec<String>>,
709 },
710 Par {
711 section_directives: Vec<(String, String)>,
712 sections: Vec<Vec<String>>,
713 },
714 Critical {
715 section_directives: Vec<(String, String)>,
716 sections: Vec<Vec<String>>,
717 },
718 }
719
720 let mut directive_steps: std::collections::HashMap<String, f64> =
721 std::collections::HashMap::new();
722 let mut stack: Vec<BlockStackEntry> = Vec::new();
723 for msg in &model.messages {
724 let raw_label = msg.message.as_str().unwrap_or_default();
725 match msg.message_type {
726 10 => stack.push(BlockStackEntry::Loop {
728 start_id: msg.id.clone(),
729 raw_label: raw_label.to_string(),
730 messages: Vec::new(),
731 }),
732 11 => {
733 if let Some(BlockStackEntry::Loop {
734 start_id,
735 raw_label,
736 messages,
737 }) = stack.pop()
738 {
739 let loop_has_self_message = messages
740 .iter()
741 .any(|msg_id| is_self_message_id(msg_id.as_str(), &msg_by_id));
742 let loop_end_step = if loop_has_self_message {
743 40.0
744 } else {
745 block_end_step
746 };
747
748 if raw_label.trim().is_empty() {
749 directive_steps.insert(start_id, block_base_step_empty);
750 } else if let Some(w) = block_frame_width(
751 &messages,
752 &msg_by_id,
753 &actor_index,
754 &actor_centers_x,
755 &actor_widths,
756 message_margin,
757 box_text_margin,
758 bottom_margin_adj,
759 measurer,
760 &msg_text_style,
761 message_width_scale,
762 ) {
763 let label = block_label_text(&raw_label);
764 let metrics = measurer.measure_wrapped(
765 &label,
766 &msg_text_style,
767 Some(w),
768 WrapMode::SvgLikeSingleRun,
769 );
770 let extra =
771 (metrics.line_count.saturating_sub(1) as f64) * block_extra_per_line;
772 directive_steps.insert(start_id, block_base_step + extra);
773 } else {
774 directive_steps.insert(start_id, block_base_step);
775 }
776
777 directive_steps.insert(msg.id.clone(), loop_end_step);
778 }
779 }
780 15 => stack.push(BlockStackEntry::Opt {
782 start_id: msg.id.clone(),
783 raw_label: raw_label.to_string(),
784 messages: Vec::new(),
785 }),
786 16 => {
787 let mut end_step = block_end_step;
788 if let Some(BlockStackEntry::Opt {
789 start_id,
790 raw_label,
791 messages,
792 }) = stack.pop()
793 {
794 let has_self = messages
795 .iter()
796 .any(|msg_id| is_self_message_id(msg_id.as_str(), &msg_by_id));
797 end_step = if has_self { 40.0 } else { block_end_step };
798 if raw_label.trim().is_empty() {
799 directive_steps.insert(start_id, block_base_step_empty);
800 } else if let Some(w) = block_frame_width(
801 &messages,
802 &msg_by_id,
803 &actor_index,
804 &actor_centers_x,
805 &actor_widths,
806 message_margin,
807 box_text_margin,
808 bottom_margin_adj,
809 measurer,
810 &msg_text_style,
811 message_width_scale,
812 ) {
813 let label = block_label_text(&raw_label);
814 let metrics = measurer.measure_wrapped(
815 &label,
816 &msg_text_style,
817 Some(w),
818 WrapMode::SvgLikeSingleRun,
819 );
820 let extra =
821 (metrics.line_count.saturating_sub(1) as f64) * block_extra_per_line;
822 directive_steps.insert(start_id, block_base_step + extra);
823 } else {
824 directive_steps.insert(start_id, block_base_step);
825 }
826 }
827 directive_steps.insert(msg.id.clone(), end_step);
828 }
829 30 => stack.push(BlockStackEntry::Break {
831 start_id: msg.id.clone(),
832 raw_label: raw_label.to_string(),
833 messages: Vec::new(),
834 }),
835 31 => {
836 let mut end_step = block_end_step;
837 if let Some(BlockStackEntry::Break {
838 start_id,
839 raw_label,
840 messages,
841 }) = stack.pop()
842 {
843 let has_self = messages
844 .iter()
845 .any(|msg_id| is_self_message_id(msg_id.as_str(), &msg_by_id));
846 end_step = if has_self { 40.0 } else { block_end_step };
847 if raw_label.trim().is_empty() {
848 directive_steps.insert(start_id, block_base_step_empty);
849 } else if let Some(w) = block_frame_width(
850 &messages,
851 &msg_by_id,
852 &actor_index,
853 &actor_centers_x,
854 &actor_widths,
855 message_margin,
856 box_text_margin,
857 bottom_margin_adj,
858 measurer,
859 &msg_text_style,
860 message_width_scale,
861 ) {
862 let label = block_label_text(&raw_label);
863 let metrics = measurer.measure_wrapped(
864 &label,
865 &msg_text_style,
866 Some(w),
867 WrapMode::SvgLikeSingleRun,
868 );
869 let extra =
870 (metrics.line_count.saturating_sub(1) as f64) * block_extra_per_line;
871 directive_steps.insert(start_id, block_base_step + extra);
872 } else {
873 directive_steps.insert(start_id, block_base_step);
874 }
875 }
876 directive_steps.insert(msg.id.clone(), end_step);
877 }
878 12 => stack.push(BlockStackEntry::Alt {
880 section_directives: vec![(msg.id.clone(), raw_label.to_string())],
881 sections: vec![Vec::new()],
882 }),
883 13 => {
884 if let Some(BlockStackEntry::Alt {
885 section_directives,
886 sections,
887 }) = stack.last_mut()
888 {
889 section_directives.push((msg.id.clone(), raw_label.to_string()));
890 sections.push(Vec::new());
891 }
892 }
893 14 => {
894 let mut end_step = block_end_step;
895 if let Some(BlockStackEntry::Alt {
896 section_directives,
897 sections,
898 }) = stack.pop()
899 {
900 let has_self = sections
901 .iter()
902 .flatten()
903 .any(|msg_id| is_self_message_id(msg_id.as_str(), &msg_by_id));
904 end_step = if has_self { 40.0 } else { block_end_step };
905 let mut message_ids: Vec<String> = Vec::new();
906 for sec in §ions {
907 message_ids.extend(sec.iter().cloned());
908 }
909 if let Some(w) = block_frame_width(
910 &message_ids,
911 &msg_by_id,
912 &actor_index,
913 &actor_centers_x,
914 &actor_widths,
915 message_margin,
916 box_text_margin,
917 bottom_margin_adj,
918 measurer,
919 &msg_text_style,
920 message_width_scale,
921 ) {
922 for (idx, (id, raw)) in section_directives.into_iter().enumerate() {
923 let is_empty = raw.trim().is_empty();
924 if is_empty {
925 directive_steps.insert(id, block_base_step_empty);
926 continue;
927 }
928 let _ = idx;
929 let label = block_label_text(&raw);
930 let metrics = measurer.measure_wrapped(
931 &label,
932 &msg_text_style,
933 Some(w),
934 WrapMode::SvgLikeSingleRun,
935 );
936 let extra = (metrics.line_count.saturating_sub(1) as f64)
937 * block_extra_per_line;
938 directive_steps.insert(id, block_base_step + extra);
939 }
940 } else {
941 for (id, raw) in section_directives {
942 let step = if raw.trim().is_empty() {
943 block_base_step_empty
944 } else {
945 block_base_step
946 };
947 directive_steps.insert(id, step);
948 }
949 }
950 }
951 directive_steps.insert(msg.id.clone(), end_step);
952 }
953 19 | 32 => stack.push(BlockStackEntry::Par {
955 section_directives: vec![(msg.id.clone(), raw_label.to_string())],
956 sections: vec![Vec::new()],
957 }),
958 20 => {
959 if let Some(BlockStackEntry::Par {
960 section_directives,
961 sections,
962 }) = stack.last_mut()
963 {
964 section_directives.push((msg.id.clone(), raw_label.to_string()));
965 sections.push(Vec::new());
966 }
967 }
968 21 => {
969 let mut end_step = block_end_step;
970 if let Some(BlockStackEntry::Par {
971 section_directives,
972 sections,
973 }) = stack.pop()
974 {
975 let has_self = sections
976 .iter()
977 .flatten()
978 .any(|msg_id| is_self_message_id(msg_id.as_str(), &msg_by_id));
979 end_step = if has_self { 40.0 } else { block_end_step };
980 let mut message_ids: Vec<String> = Vec::new();
981 for sec in §ions {
982 message_ids.extend(sec.iter().cloned());
983 }
984 if let Some(w) = block_frame_width(
985 &message_ids,
986 &msg_by_id,
987 &actor_index,
988 &actor_centers_x,
989 &actor_widths,
990 message_margin,
991 box_text_margin,
992 bottom_margin_adj,
993 measurer,
994 &msg_text_style,
995 message_width_scale,
996 ) {
997 for (idx, (id, raw)) in section_directives.into_iter().enumerate() {
998 let is_empty = raw.trim().is_empty();
999 if is_empty {
1000 directive_steps.insert(id, block_base_step_empty);
1001 continue;
1002 }
1003 let _ = idx;
1004 let label = block_label_text(&raw);
1005 let metrics = measurer.measure_wrapped(
1006 &label,
1007 &msg_text_style,
1008 Some(w),
1009 WrapMode::SvgLikeSingleRun,
1010 );
1011 let extra = (metrics.line_count.saturating_sub(1) as f64)
1012 * block_extra_per_line;
1013 directive_steps.insert(id, block_base_step + extra);
1014 }
1015 } else {
1016 for (id, raw) in section_directives {
1017 let step = if raw.trim().is_empty() {
1018 block_base_step_empty
1019 } else {
1020 block_base_step
1021 };
1022 directive_steps.insert(id, step);
1023 }
1024 }
1025 }
1026 directive_steps.insert(msg.id.clone(), end_step);
1027 }
1028 27 => stack.push(BlockStackEntry::Critical {
1030 section_directives: vec![(msg.id.clone(), raw_label.to_string())],
1031 sections: vec![Vec::new()],
1032 }),
1033 28 => {
1034 if let Some(BlockStackEntry::Critical {
1035 section_directives,
1036 sections,
1037 }) = stack.last_mut()
1038 {
1039 section_directives.push((msg.id.clone(), raw_label.to_string()));
1040 sections.push(Vec::new());
1041 }
1042 }
1043 29 => {
1044 let mut end_step = block_end_step;
1045 if let Some(BlockStackEntry::Critical {
1046 section_directives,
1047 sections,
1048 }) = stack.pop()
1049 {
1050 let has_self = sections
1051 .iter()
1052 .flatten()
1053 .any(|msg_id| is_self_message_id(msg_id.as_str(), &msg_by_id));
1054 end_step = if has_self { 40.0 } else { block_end_step };
1055 let mut message_ids: Vec<String> = Vec::new();
1056 for sec in §ions {
1057 message_ids.extend(sec.iter().cloned());
1058 }
1059 if let Some(w) = block_frame_width(
1060 &message_ids,
1061 &msg_by_id,
1062 &actor_index,
1063 &actor_centers_x,
1064 &actor_widths,
1065 message_margin,
1066 box_text_margin,
1067 bottom_margin_adj,
1068 measurer,
1069 &msg_text_style,
1070 message_width_scale,
1071 ) {
1072 for (idx, (id, raw)) in section_directives.into_iter().enumerate() {
1073 let is_empty = raw.trim().is_empty();
1074 if is_empty {
1075 directive_steps.insert(id, block_base_step_empty);
1076 continue;
1077 }
1078 let _ = idx;
1079 let label = block_label_text(&raw);
1080 let metrics = measurer.measure_wrapped(
1081 &label,
1082 &msg_text_style,
1083 Some(w),
1084 WrapMode::SvgLikeSingleRun,
1085 );
1086 let extra = (metrics.line_count.saturating_sub(1) as f64)
1087 * block_extra_per_line;
1088 directive_steps.insert(id, block_base_step + extra);
1089 }
1090 } else {
1091 for (id, raw) in section_directives {
1092 let step = if raw.trim().is_empty() {
1093 block_base_step_empty
1094 } else {
1095 block_base_step
1096 };
1097 directive_steps.insert(id, step);
1098 }
1099 }
1100 }
1101 directive_steps.insert(msg.id.clone(), end_step);
1102 }
1103 _ => {
1104 if msg.from.is_some() && msg.to.is_some() {
1107 for entry in stack.iter_mut() {
1108 match entry {
1109 BlockStackEntry::Alt { sections, .. }
1110 | BlockStackEntry::Par { sections, .. }
1111 | BlockStackEntry::Critical { sections, .. } => {
1112 if let Some(cur) = sections.last_mut() {
1113 cur.push(msg.id.clone());
1114 }
1115 }
1116 BlockStackEntry::Loop { messages, .. }
1117 | BlockStackEntry::Opt { messages, .. }
1118 | BlockStackEntry::Break { messages, .. } => {
1119 messages.push(msg.id.clone());
1120 }
1121 }
1122 }
1123 }
1124 }
1125 }
1126 }
1127
1128 #[derive(Debug, Clone)]
1129 struct RectOpen {
1130 start_id: String,
1131 top_y: f64,
1132 bounds: Option<merman_core::geom::Box2>,
1133 }
1134
1135 impl RectOpen {
1136 fn include_min_max(&mut self, min_x: f64, max_x: f64, max_y: f64) {
1137 let r = merman_core::geom::Box2::from_min_max(min_x, self.top_y, max_x, max_y);
1138 if let Some(ref mut cur) = self.bounds {
1139 cur.union(r);
1140 } else {
1141 self.bounds = Some(r);
1142 }
1143 }
1144 }
1145
1146 let note_width_single = actor_width_min;
1150 let rect_step_start = 20.0;
1151 let rect_step_end = 10.0;
1152 let note_gap = 10.0;
1153 let note_text_pad_total = 20.0;
1156 let note_top_offset = message_step - note_gap;
1157
1158 let mut cursor_y = actor_top_offset_y + max_actor_visual_height + message_step;
1159 let mut rect_stack: Vec<RectOpen> = Vec::new();
1160 let activation_width = config_f64(seq_cfg, &["activationWidth"])
1161 .unwrap_or(10.0)
1162 .max(1.0);
1163 let mut activation_stacks: std::collections::BTreeMap<&str, Vec<f64>> =
1164 std::collections::BTreeMap::new();
1165
1166 let mut created_actor_top_center_y: std::collections::BTreeMap<String, f64> =
1171 std::collections::BTreeMap::new();
1172 let mut destroyed_actor_bottom_top_y: std::collections::BTreeMap<String, f64> =
1173 std::collections::BTreeMap::new();
1174
1175 let actor_visual_height_for_id = |actor_id: &str| -> f64 {
1176 let Some(idx) = actor_index.get(actor_id).copied() else {
1177 return actor_height.max(1.0);
1178 };
1179 let w = actor_widths.get(idx).copied().unwrap_or(actor_width_min);
1180 let base_h = actor_base_heights.get(idx).copied().unwrap_or(actor_height);
1181 model
1182 .actors
1183 .get(actor_id)
1184 .map(|a| a.actor_type.as_str())
1185 .map(|t| sequence_actor_visual_height(t, w, base_h, label_box_height))
1186 .unwrap_or(base_h.max(1.0))
1187 };
1188 let actor_is_type_width_limited = |actor_id: &str| -> bool {
1189 model
1190 .actors
1191 .get(actor_id)
1192 .map(|a| {
1193 matches!(
1194 a.actor_type.as_str(),
1195 "actor" | "control" | "entity" | "database"
1196 )
1197 })
1198 .unwrap_or(false)
1199 };
1200
1201 for (msg_idx, msg) in model.messages.iter().enumerate() {
1202 match msg.message_type {
1203 17 => {
1205 let Some(actor_id) = msg.from.as_deref() else {
1206 continue;
1207 };
1208 let Some(&idx) = actor_index.get(actor_id) else {
1209 continue;
1210 };
1211 let cx = actor_centers_x[idx];
1212 let stack = activation_stacks.entry(actor_id).or_default();
1213 let stacked_size = stack.len();
1214 let startx = cx + (((stacked_size as f64) - 1.0) * activation_width) / 2.0;
1215 stack.push(startx);
1216 continue;
1217 }
1218 18 => {
1220 let Some(actor_id) = msg.from.as_deref() else {
1221 continue;
1222 };
1223 if let Some(stack) = activation_stacks.get_mut(actor_id) {
1224 let _ = stack.pop();
1225 }
1226 continue;
1227 }
1228 _ => {}
1229 }
1230
1231 if let Some(step) = directive_steps.get(msg.id.as_str()).copied() {
1232 cursor_y += step;
1233 continue;
1234 }
1235 match msg.message_type {
1236 22 => {
1238 rect_stack.push(RectOpen {
1239 start_id: msg.id.clone(),
1240 top_y: cursor_y - note_top_offset,
1241 bounds: None,
1242 });
1243 cursor_y += rect_step_start;
1244 continue;
1245 }
1246 23 => {
1248 if let Some(open) = rect_stack.pop() {
1249 let rect_left = open.bounds.map(|b| b.min_x()).unwrap_or_else(|| {
1250 actor_centers_x
1251 .iter()
1252 .copied()
1253 .fold(f64::INFINITY, f64::min)
1254 - 11.0
1255 });
1256 let rect_right = open.bounds.map(|b| b.max_x()).unwrap_or_else(|| {
1257 actor_centers_x
1258 .iter()
1259 .copied()
1260 .fold(f64::NEG_INFINITY, f64::max)
1261 + 11.0
1262 });
1263 let rect_bottom = open
1264 .bounds
1265 .map(|b| b.max_y() + 10.0)
1266 .unwrap_or(open.top_y + 10.0);
1267 let rect_w = (rect_right - rect_left).max(1.0);
1268 let rect_h = (rect_bottom - open.top_y).max(1.0);
1269
1270 nodes.push(LayoutNode {
1271 id: format!("rect-{}", open.start_id),
1272 x: rect_left + rect_w / 2.0,
1273 y: open.top_y + rect_h / 2.0,
1274 width: rect_w,
1275 height: rect_h,
1276 is_cluster: false,
1277 label_width: None,
1278 label_height: None,
1279 });
1280
1281 if let Some(parent) = rect_stack.last_mut() {
1282 parent.include_min_max(rect_left - 10.0, rect_right + 10.0, rect_bottom);
1283 }
1284 }
1285 cursor_y += rect_step_end;
1286 continue;
1287 }
1288 _ => {}
1289 }
1290
1291 if msg.message_type == 2 {
1293 let (Some(from), Some(to)) = (msg.from.as_deref(), msg.to.as_deref()) else {
1294 continue;
1295 };
1296 let (Some(fi), Some(ti)) =
1297 (actor_index.get(from).copied(), actor_index.get(to).copied())
1298 else {
1299 continue;
1300 };
1301 let fx = actor_centers_x[fi];
1302 let tx = actor_centers_x[ti];
1303
1304 let placement = msg.placement.unwrap_or(2);
1305 let (mut note_x, mut note_w) = match placement {
1306 0 => (fx - 25.0 - note_width_single, note_width_single),
1308 1 => (fx + 25.0, note_width_single),
1310 _ => {
1312 if (fx - tx).abs() < 0.0001 {
1313 let mut w = note_width_single;
1320 if msg.wrap {
1321 w = w.max(actor_widths.get(fi).copied().unwrap_or(note_width_single));
1322 }
1323 (fx - (w / 2.0), w)
1324 } else {
1325 let left = fx.min(tx) - 25.0;
1326 let right = fx.max(tx) + 25.0;
1327 let w = (right - left).max(note_width_single);
1328 (left, w)
1329 }
1330 }
1331 };
1332
1333 let text = msg.message.as_str().unwrap_or_default();
1334 let (text_w, h) = if msg.wrap {
1335 let w0 = {
1343 let init_lines = wrap_label_like_mermaid_lines_floored_bbox(
1344 text,
1345 measurer,
1346 ¬e_text_style,
1347 note_width_single.max(1.0),
1348 );
1349 let init_wrapped = init_lines.join("<br/>");
1350 let (w, _h) =
1351 measure_svg_like_with_html_br(measurer, &init_wrapped, ¬e_text_style);
1352 w.max(0.0)
1353 };
1354
1355 if placement == 0 {
1356 note_w = note_w.max((w0 + note_text_pad_total).round().max(1.0));
1359 note_x = fx - 25.0 - note_w;
1360 }
1361
1362 let wrap_w = (note_w - note_text_pad_total).max(1.0);
1363 let lines = wrap_label_like_mermaid_lines_floored_bbox(
1364 text,
1365 measurer,
1366 ¬e_text_style,
1367 wrap_w,
1368 );
1369 let wrapped = lines.join("<br/>");
1370 let (w, h) = measure_svg_like_with_html_br(measurer, &wrapped, ¬e_text_style);
1371 (w.max(0.0), h.max(0.0))
1372 } else {
1373 measure_svg_like_with_html_br(measurer, text, ¬e_text_style)
1374 };
1375
1376 let padded_w = (text_w + note_text_pad_total).round().max(1.0);
1380 if !msg.wrap {
1381 match placement {
1382 0 | 1 => {
1384 note_w = note_w.max(padded_w);
1385 }
1386 _ => {
1388 if (fx - tx).abs() < 0.0001 {
1389 note_w = note_w.max(padded_w);
1390 }
1391 }
1392 }
1393 }
1394 let note_h = (h + note_text_pad_total).round().max(1.0);
1395 let note_y = (cursor_y - note_top_offset).round();
1396
1397 nodes.push(LayoutNode {
1398 id: format!("note-{}", msg.id),
1399 x: note_x + note_w / 2.0,
1400 y: note_y + note_h / 2.0,
1401 width: note_w.max(1.0),
1402 height: note_h,
1403 is_cluster: false,
1404 label_width: None,
1405 label_height: None,
1406 });
1407
1408 for open in rect_stack.iter_mut() {
1409 open.include_min_max(note_x - 10.0, note_x + note_w + 10.0, note_y + note_h);
1410 }
1411
1412 cursor_y += note_h + note_gap;
1413 continue;
1414 }
1415
1416 let (Some(from), Some(to)) = (msg.from.as_deref(), msg.to.as_deref()) else {
1418 continue;
1419 };
1420 let (Some(fi), Some(ti)) = (actor_index.get(from).copied(), actor_index.get(to).copied())
1421 else {
1422 continue;
1423 };
1424 let from_x = actor_centers_x[fi];
1425 let to_x = actor_centers_x[ti];
1426
1427 let (from_left, from_right) = activation_stacks
1428 .get(from)
1429 .and_then(|s| s.last().copied())
1430 .map(|startx| (startx, startx + activation_width))
1431 .unwrap_or((from_x - 1.0, from_x + 1.0));
1432
1433 let (to_left, to_right) = activation_stacks
1434 .get(to)
1435 .and_then(|s| s.last().copied())
1436 .map(|startx| (startx, startx + activation_width))
1437 .unwrap_or((to_x - 1.0, to_x + 1.0));
1438
1439 let is_arrow_to_right = from_left <= to_left;
1440 let mut startx = if is_arrow_to_right {
1441 from_right
1442 } else {
1443 from_left
1444 };
1445 let mut stopx = if is_arrow_to_right { to_left } else { to_right };
1446
1447 let adjust_value = |v: f64| if is_arrow_to_right { -v } else { v };
1448 let is_arrow_to_activation = (to_left - to_right).abs() > 2.0;
1449
1450 let is_self = from == to;
1451 if is_self {
1452 stopx = startx;
1453 } else {
1454 if msg.activate && !is_arrow_to_activation {
1455 stopx += adjust_value(activation_width / 2.0 - 1.0);
1456 }
1457
1458 if !matches!(msg.message_type, 5 | 6) {
1459 stopx += adjust_value(3.0);
1460 }
1461
1462 if matches!(msg.message_type, 33 | 34) {
1463 startx -= adjust_value(3.0);
1464 }
1465 }
1466
1467 if !is_self {
1468 const ACTOR_TYPE_WIDTH_HALF: f64 = 18.0;
1470 if model
1471 .created_actors
1472 .get(to)
1473 .is_some_and(|&idx| idx == msg_idx)
1474 {
1475 let adjustment = if actor_is_type_width_limited(to) {
1476 ACTOR_TYPE_WIDTH_HALF + 3.0
1477 } else {
1478 actor_widths[ti] / 2.0 + 3.0
1479 };
1480 if to_x < from_x {
1481 stopx += adjustment;
1482 } else {
1483 stopx -= adjustment;
1484 }
1485 } else if model
1486 .destroyed_actors
1487 .get(from)
1488 .is_some_and(|&idx| idx == msg_idx)
1489 {
1490 let adjustment = if actor_is_type_width_limited(from) {
1491 ACTOR_TYPE_WIDTH_HALF
1492 } else {
1493 actor_widths[fi] / 2.0
1494 };
1495 if from_x < to_x {
1496 startx += adjustment;
1497 } else {
1498 startx -= adjustment;
1499 }
1500 } else if model
1501 .destroyed_actors
1502 .get(to)
1503 .is_some_and(|&idx| idx == msg_idx)
1504 {
1505 let adjustment = if actor_is_type_width_limited(to) {
1506 ACTOR_TYPE_WIDTH_HALF + 3.0
1507 } else {
1508 actor_widths[ti] / 2.0 + 3.0
1509 };
1510 if to_x < from_x {
1511 stopx += adjustment;
1512 } else {
1513 stopx -= adjustment;
1514 }
1515 }
1516 }
1517
1518 let text = msg.message.as_str().unwrap_or_default();
1519 let bounded_width = (startx - stopx).abs().max(0.0);
1520 let wrapped_text = if !text.is_empty() && msg.wrap {
1521 let wrap_w = (bounded_width + 3.0 * wrap_padding)
1525 .max(actor_width_min)
1526 .max(1.0);
1527 let lines =
1528 wrap_label_like_mermaid_lines_floored_bbox(text, measurer, &msg_text_style, wrap_w);
1529 Some(lines.join("<br>"))
1530 } else {
1531 None
1532 };
1533 let effective_text = wrapped_text.as_deref().unwrap_or(text);
1534
1535 let (line_y, label_base_y, cursor_step) = if effective_text.is_empty() {
1536 let line_y = cursor_y - (message_step - box_margin);
1544 (line_y, cursor_y, box_margin)
1545 } else {
1546 let lines = split_html_br_lines(effective_text).len().max(1);
1551 let bbox_line_h = (message_font_size + bottom_margin_adj).max(0.0);
1555 let extra = (lines.saturating_sub(1) as f64) * bbox_line_h;
1556 (cursor_y + extra, cursor_y, message_step + extra)
1557 };
1558
1559 let x1 = startx;
1560 let x2 = stopx;
1561
1562 let label = if effective_text.is_empty() {
1563 Some(LayoutLabel {
1566 x: ((x1 + x2) / 2.0).round(),
1567 y: (label_base_y - msg_label_offset).round(),
1568 width: 1.0,
1569 height: message_font_size.max(1.0),
1570 })
1571 } else {
1572 let (w, h) = measure_svg_like_with_html_br(measurer, effective_text, &msg_text_style);
1573 Some(LayoutLabel {
1574 x: ((x1 + x2) / 2.0).round(),
1575 y: (label_base_y - msg_label_offset).round(),
1576 width: (w * message_width_scale).max(1.0),
1577 height: h.max(1.0),
1578 })
1579 };
1580
1581 edges.push(LayoutEdge {
1582 id: format!("msg-{}", msg.id),
1583 from: from.to_string(),
1584 to: to.to_string(),
1585 from_cluster: None,
1586 to_cluster: None,
1587 points: vec![
1588 LayoutPoint { x: x1, y: line_y },
1589 LayoutPoint { x: x2, y: line_y },
1590 ],
1591 label,
1592 start_label_left: None,
1593 start_label_right: None,
1594 end_label_left: None,
1595 end_label_right: None,
1596 start_marker: None,
1597 end_marker: None,
1598 stroke_dasharray: None,
1599 });
1600
1601 for open in rect_stack.iter_mut() {
1602 let lx = from_x.min(to_x) - 11.0;
1603 let rx = from_x.max(to_x) + 11.0;
1604 open.include_min_max(lx, rx, line_y);
1605 }
1606
1607 cursor_y += cursor_step;
1608 if is_self {
1609 cursor_y += 30.0;
1611 }
1612
1613 if model
1615 .created_actors
1616 .get(to)
1617 .is_some_and(|&idx| idx == msg_idx)
1618 {
1619 let h = actor_visual_height_for_id(to);
1620 created_actor_top_center_y.insert(to.to_string(), line_y);
1621 cursor_y += h / 2.0;
1622 } else if model
1623 .destroyed_actors
1624 .get(from)
1625 .is_some_and(|&idx| idx == msg_idx)
1626 {
1627 let h = actor_visual_height_for_id(from);
1628 destroyed_actor_bottom_top_y.insert(from.to_string(), line_y - h / 2.0);
1629 cursor_y += h / 2.0;
1630 } else if model
1631 .destroyed_actors
1632 .get(to)
1633 .is_some_and(|&idx| idx == msg_idx)
1634 {
1635 let h = actor_visual_height_for_id(to);
1636 destroyed_actor_bottom_top_y.insert(to.to_string(), line_y - h / 2.0);
1637 cursor_y += h / 2.0;
1638 }
1639 }
1640
1641 let bottom_margin = message_margin - message_font_size + bottom_margin_adj;
1642 let bottom_box_top_y = (cursor_y - message_step) + bottom_margin;
1643
1644 for n in nodes.iter_mut() {
1646 let Some(actor_id) = n.id.strip_prefix("actor-top-") else {
1647 continue;
1648 };
1649 if let Some(y) = created_actor_top_center_y.get(actor_id).copied() {
1650 n.y = y;
1651 }
1652 }
1653
1654 for (idx, id) in model.actor_order.iter().enumerate() {
1655 let w = actor_widths[idx];
1656 let cx = actor_centers_x[idx];
1657 let base_h = actor_base_heights[idx];
1658 let actor_type = model
1659 .actors
1660 .get(id)
1661 .map(|a| a.actor_type.as_str())
1662 .unwrap_or("participant");
1663 let visual_h = sequence_actor_visual_height(actor_type, w, base_h, label_box_height);
1664 let bottom_top_y = destroyed_actor_bottom_top_y
1665 .get(id)
1666 .copied()
1667 .unwrap_or(bottom_box_top_y);
1668 let bottom_visual_h = if mirror_actors { visual_h } else { 0.0 };
1669 nodes.push(LayoutNode {
1670 id: format!("actor-bottom-{id}"),
1671 x: cx,
1672 y: bottom_top_y + bottom_visual_h / 2.0,
1673 width: w,
1674 height: bottom_visual_h,
1675 is_cluster: false,
1676 label_width: None,
1677 label_height: None,
1678 });
1679
1680 let top_center_y = created_actor_top_center_y
1681 .get(id)
1682 .copied()
1683 .unwrap_or(actor_top_offset_y + visual_h / 2.0);
1684 let top_left_y = top_center_y - visual_h / 2.0;
1685 let lifeline_start_y =
1686 top_left_y + sequence_actor_lifeline_start_y(actor_type, base_h, box_text_margin);
1687
1688 edges.push(LayoutEdge {
1689 id: format!("lifeline-{id}"),
1690 from: format!("actor-top-{id}"),
1691 to: format!("actor-bottom-{id}"),
1692 from_cluster: None,
1693 to_cluster: None,
1694 points: vec![
1695 LayoutPoint {
1696 x: cx,
1697 y: lifeline_start_y,
1698 },
1699 LayoutPoint {
1700 x: cx,
1701 y: bottom_top_y,
1702 },
1703 ],
1704 label: None,
1705 start_label_left: None,
1706 start_label_right: None,
1707 end_label_left: None,
1708 end_label_right: None,
1709 start_marker: None,
1710 end_marker: None,
1711 stroke_dasharray: None,
1712 });
1713 }
1714
1715 let block_bounds = {
1720 use std::collections::HashMap;
1721
1722 let nodes_by_id: HashMap<&str, &LayoutNode> = nodes
1723 .iter()
1724 .map(|n| (n.id.as_str(), n))
1725 .collect::<HashMap<_, _>>();
1726 let edges_by_id: HashMap<&str, &LayoutEdge> = edges
1727 .iter()
1728 .map(|e| (e.id.as_str(), e))
1729 .collect::<HashMap<_, _>>();
1730
1731 let mut msg_endpoints: HashMap<&str, (&str, &str)> = HashMap::new();
1732 for msg in &model.messages {
1733 let (Some(from), Some(to)) = (msg.from.as_deref(), msg.to.as_deref()) else {
1734 continue;
1735 };
1736 msg_endpoints.insert(msg.id.as_str(), (from, to));
1737 }
1738
1739 fn item_y_range(
1740 item_id: &str,
1741 nodes_by_id: &HashMap<&str, &LayoutNode>,
1742 edges_by_id: &HashMap<&str, &LayoutEdge>,
1743 msg_endpoints: &HashMap<&str, (&str, &str)>,
1744 ) -> Option<(f64, f64)> {
1745 const SELF_MESSAGE_EXTRA_Y: f64 = 60.0;
1749 let edge_id = format!("msg-{item_id}");
1750 if let Some(e) = edges_by_id.get(edge_id.as_str()).copied() {
1751 let y = e.points.first()?.y;
1752 let extra = msg_endpoints
1753 .get(item_id)
1754 .copied()
1755 .filter(|(from, to)| from == to)
1756 .map(|_| SELF_MESSAGE_EXTRA_Y)
1757 .unwrap_or(0.0);
1758 return Some((y, y + extra));
1759 }
1760
1761 let node_id = format!("note-{item_id}");
1762 let n = nodes_by_id.get(node_id.as_str()).copied()?;
1763 let top = n.y - n.height / 2.0;
1764 let bottom = n.y + n.height / 2.0;
1765 Some((top, bottom))
1766 }
1767
1768 fn frame_x_from_item_ids<'a>(
1769 item_ids: impl IntoIterator<Item = &'a String>,
1770 nodes_by_id: &HashMap<&str, &LayoutNode>,
1771 edges_by_id: &HashMap<&str, &LayoutEdge>,
1772 msg_endpoints: &HashMap<&str, (&str, &str)>,
1773 ) -> Option<(f64, f64, f64)> {
1774 const SIDE_PAD: f64 = 11.0;
1775 const GEOM_PAD: f64 = 10.0;
1776 let mut min_cx = f64::INFINITY;
1777 let mut max_cx = f64::NEG_INFINITY;
1778 let mut min_left = f64::INFINITY;
1779 let mut geom_min_x = f64::INFINITY;
1780 let mut geom_max_x = f64::NEG_INFINITY;
1781
1782 for id in item_ids {
1783 let note_id = format!("note-{id}");
1785 if let Some(n) = nodes_by_id.get(note_id.as_str()).copied() {
1786 geom_min_x = geom_min_x.min(n.x - n.width / 2.0 - GEOM_PAD);
1787 geom_max_x = geom_max_x.max(n.x + n.width / 2.0 + GEOM_PAD);
1788 }
1789
1790 let Some((from, to)) = msg_endpoints.get(id.as_str()).copied() else {
1791 continue;
1792 };
1793 for actor_id in [from, to] {
1794 let actor_node_id = format!("actor-top-{actor_id}");
1795 let Some(n) = nodes_by_id.get(actor_node_id.as_str()).copied() else {
1796 continue;
1797 };
1798 min_cx = min_cx.min(n.x);
1799 max_cx = max_cx.max(n.x);
1800 min_left = min_left.min(n.x - n.width / 2.0);
1801 }
1802
1803 let edge_id = format!("msg-{id}");
1805 if let Some(e) = edges_by_id.get(edge_id.as_str()).copied() {
1806 for p in &e.points {
1807 geom_min_x = geom_min_x.min(p.x);
1808 geom_max_x = geom_max_x.max(p.x);
1809 }
1810 if let Some(label) = e.label.as_ref() {
1811 geom_min_x = geom_min_x.min(label.x - (label.width / 2.0) - GEOM_PAD);
1812 geom_max_x = geom_max_x.max(label.x + (label.width / 2.0) + GEOM_PAD);
1813 }
1814 }
1815 }
1816
1817 if !min_cx.is_finite() || !max_cx.is_finite() {
1818 return None;
1819 }
1820 let mut x1 = min_cx - SIDE_PAD;
1821 let mut x2 = max_cx + SIDE_PAD;
1822 if geom_min_x.is_finite() {
1823 x1 = x1.min(geom_min_x);
1824 }
1825 if geom_max_x.is_finite() {
1826 x2 = x2.max(geom_max_x);
1827 }
1828 Some((x1, x2, min_left))
1829 }
1830
1831 #[derive(Debug)]
1832 enum BlockStackEntry {
1833 Loop { items: Vec<String> },
1834 Opt { items: Vec<String> },
1835 Break { items: Vec<String> },
1836 Alt { sections: Vec<Vec<String>> },
1837 Par { sections: Vec<Vec<String>> },
1838 Critical { sections: Vec<Vec<String>> },
1839 }
1840
1841 let mut block_min_x = f64::INFINITY;
1842 let mut block_min_y = f64::INFINITY;
1843 let mut block_max_x = f64::NEG_INFINITY;
1844 let mut block_max_y = f64::NEG_INFINITY;
1845
1846 let mut stack: Vec<BlockStackEntry> = Vec::new();
1847 for msg in &model.messages {
1848 let msg_id = msg.id.clone();
1849 match msg.message_type {
1850 10 => stack.push(BlockStackEntry::Loop { items: Vec::new() }),
1851 11 => {
1852 if let Some(BlockStackEntry::Loop { items }) = stack.pop() {
1853 if let (Some((x1, x2, _min_left)), Some((y0, y1))) = (
1854 frame_x_from_item_ids(
1855 &items,
1856 &nodes_by_id,
1857 &edges_by_id,
1858 &msg_endpoints,
1859 ),
1860 items
1861 .iter()
1862 .filter_map(|id| {
1863 item_y_range(id, &nodes_by_id, &edges_by_id, &msg_endpoints)
1864 })
1865 .reduce(|a, b| (a.0.min(b.0), a.1.max(b.1))),
1866 ) {
1867 let frame_y1 = y0 - 79.0;
1868 let frame_y2 = y1 + 10.0;
1869 block_min_x = block_min_x.min(x1);
1870 block_max_x = block_max_x.max(x2);
1871 block_min_y = block_min_y.min(frame_y1);
1872 block_max_y = block_max_y.max(frame_y2);
1873 }
1874 }
1875 }
1876 15 => stack.push(BlockStackEntry::Opt { items: Vec::new() }),
1877 16 => {
1878 if let Some(BlockStackEntry::Opt { items }) = stack.pop() {
1879 if let (Some((x1, x2, _min_left)), Some((y0, y1))) = (
1880 frame_x_from_item_ids(
1881 &items,
1882 &nodes_by_id,
1883 &edges_by_id,
1884 &msg_endpoints,
1885 ),
1886 items
1887 .iter()
1888 .filter_map(|id| {
1889 item_y_range(id, &nodes_by_id, &edges_by_id, &msg_endpoints)
1890 })
1891 .reduce(|a, b| (a.0.min(b.0), a.1.max(b.1))),
1892 ) {
1893 let frame_y1 = y0 - 79.0;
1894 let frame_y2 = y1 + 10.0;
1895 block_min_x = block_min_x.min(x1);
1896 block_max_x = block_max_x.max(x2);
1897 block_min_y = block_min_y.min(frame_y1);
1898 block_max_y = block_max_y.max(frame_y2);
1899 }
1900 }
1901 }
1902 30 => stack.push(BlockStackEntry::Break { items: Vec::new() }),
1903 31 => {
1904 if let Some(BlockStackEntry::Break { items }) = stack.pop() {
1905 if let (Some((x1, x2, _min_left)), Some((y0, y1))) = (
1906 frame_x_from_item_ids(
1907 &items,
1908 &nodes_by_id,
1909 &edges_by_id,
1910 &msg_endpoints,
1911 ),
1912 items
1913 .iter()
1914 .filter_map(|id| {
1915 item_y_range(id, &nodes_by_id, &edges_by_id, &msg_endpoints)
1916 })
1917 .reduce(|a, b| (a.0.min(b.0), a.1.max(b.1))),
1918 ) {
1919 let frame_y1 = y0 - 93.0;
1920 let frame_y2 = y1 + 10.0;
1921 block_min_x = block_min_x.min(x1);
1922 block_max_x = block_max_x.max(x2);
1923 block_min_y = block_min_y.min(frame_y1);
1924 block_max_y = block_max_y.max(frame_y2);
1925 }
1926 }
1927 }
1928 12 => stack.push(BlockStackEntry::Alt {
1929 sections: vec![Vec::new()],
1930 }),
1931 13 => {
1932 if let Some(BlockStackEntry::Alt { sections }) = stack.last_mut() {
1933 sections.push(Vec::new());
1934 }
1935 }
1936 14 => {
1937 if let Some(BlockStackEntry::Alt { sections }) = stack.pop() {
1938 let items: Vec<String> = sections.into_iter().flatten().collect();
1939 if let (Some((x1, x2, _min_left)), Some((y0, y1))) = (
1940 frame_x_from_item_ids(
1941 &items,
1942 &nodes_by_id,
1943 &edges_by_id,
1944 &msg_endpoints,
1945 ),
1946 items
1947 .iter()
1948 .filter_map(|id| {
1949 item_y_range(id, &nodes_by_id, &edges_by_id, &msg_endpoints)
1950 })
1951 .reduce(|a, b| (a.0.min(b.0), a.1.max(b.1))),
1952 ) {
1953 let frame_y1 = y0 - 79.0;
1954 let frame_y2 = y1 + 10.0;
1955 block_min_x = block_min_x.min(x1);
1956 block_max_x = block_max_x.max(x2);
1957 block_min_y = block_min_y.min(frame_y1);
1958 block_max_y = block_max_y.max(frame_y2);
1959 }
1960 }
1961 }
1962 19 | 32 => stack.push(BlockStackEntry::Par {
1963 sections: vec![Vec::new()],
1964 }),
1965 20 => {
1966 if let Some(BlockStackEntry::Par { sections }) = stack.last_mut() {
1967 sections.push(Vec::new());
1968 }
1969 }
1970 21 => {
1971 if let Some(BlockStackEntry::Par { sections }) = stack.pop() {
1972 let items: Vec<String> = sections.into_iter().flatten().collect();
1973 if let (Some((x1, x2, _min_left)), Some((y0, y1))) = (
1974 frame_x_from_item_ids(
1975 &items,
1976 &nodes_by_id,
1977 &edges_by_id,
1978 &msg_endpoints,
1979 ),
1980 items
1981 .iter()
1982 .filter_map(|id| {
1983 item_y_range(id, &nodes_by_id, &edges_by_id, &msg_endpoints)
1984 })
1985 .reduce(|a, b| (a.0.min(b.0), a.1.max(b.1))),
1986 ) {
1987 let frame_y1 = y0 - 79.0;
1988 let frame_y2 = y1 + 10.0;
1989 block_min_x = block_min_x.min(x1);
1990 block_max_x = block_max_x.max(x2);
1991 block_min_y = block_min_y.min(frame_y1);
1992 block_max_y = block_max_y.max(frame_y2);
1993 }
1994 }
1995 }
1996 27 => stack.push(BlockStackEntry::Critical {
1997 sections: vec![Vec::new()],
1998 }),
1999 28 => {
2000 if let Some(BlockStackEntry::Critical { sections }) = stack.last_mut() {
2001 sections.push(Vec::new());
2002 }
2003 }
2004 29 => {
2005 if let Some(BlockStackEntry::Critical { sections }) = stack.pop() {
2006 let section_count = sections.len();
2007 let items: Vec<String> = sections.into_iter().flatten().collect();
2008 if let (Some((mut x1, x2, min_left)), Some((y0, y1))) = (
2009 frame_x_from_item_ids(
2010 &items,
2011 &nodes_by_id,
2012 &edges_by_id,
2013 &msg_endpoints,
2014 ),
2015 items
2016 .iter()
2017 .filter_map(|id| {
2018 item_y_range(id, &nodes_by_id, &edges_by_id, &msg_endpoints)
2019 })
2020 .reduce(|a, b| (a.0.min(b.0), a.1.max(b.1))),
2021 ) {
2022 if min_left.is_finite() && !items.is_empty() && section_count > 1 {
2023 x1 = x1.min(min_left - 9.0);
2024 }
2025 let frame_y1 = y0 - 79.0;
2026 let frame_y2 = y1 + 10.0;
2027 block_min_x = block_min_x.min(x1);
2028 block_max_x = block_max_x.max(x2);
2029 block_min_y = block_min_y.min(frame_y1);
2030 block_max_y = block_max_y.max(frame_y2);
2031 }
2032 }
2033 }
2034 2 => {
2035 for entry in stack.iter_mut() {
2036 match entry {
2037 BlockStackEntry::Alt { sections }
2038 | BlockStackEntry::Par { sections }
2039 | BlockStackEntry::Critical { sections } => {
2040 if let Some(cur) = sections.last_mut() {
2041 cur.push(msg_id.clone());
2042 }
2043 }
2044 BlockStackEntry::Loop { items }
2045 | BlockStackEntry::Opt { items }
2046 | BlockStackEntry::Break { items } => {
2047 items.push(msg_id.clone());
2048 }
2049 }
2050 }
2051 }
2052 _ => {
2053 if msg.from.is_some() && msg.to.is_some() {
2054 for entry in stack.iter_mut() {
2055 match entry {
2056 BlockStackEntry::Alt { sections }
2057 | BlockStackEntry::Par { sections }
2058 | BlockStackEntry::Critical { sections } => {
2059 if let Some(cur) = sections.last_mut() {
2060 cur.push(msg_id.clone());
2061 }
2062 }
2063 BlockStackEntry::Loop { items }
2064 | BlockStackEntry::Opt { items }
2065 | BlockStackEntry::Break { items } => {
2066 items.push(msg_id.clone());
2067 }
2068 }
2069 }
2070 }
2071 }
2072 }
2073 }
2074
2075 if block_min_x.is_finite() && block_min_y.is_finite() {
2076 Some((block_min_x, block_min_y, block_max_x, block_max_y))
2077 } else {
2078 None
2079 }
2080 };
2081
2082 let mut content_min_x = f64::INFINITY;
2083 let mut content_max_x = f64::NEG_INFINITY;
2084 let mut content_max_y = f64::NEG_INFINITY;
2085 for n in &nodes {
2086 let left = n.x - n.width / 2.0;
2087 let right = n.x + n.width / 2.0;
2088 let bottom = n.y + n.height / 2.0;
2089 content_min_x = content_min_x.min(left);
2090 content_max_x = content_max_x.max(right);
2091 content_max_y = content_max_y.max(bottom);
2092 }
2093 if !content_min_x.is_finite() {
2094 content_min_x = 0.0;
2095 content_max_x = actor_width_min.max(1.0);
2096 content_max_y = (bottom_box_top_y + actor_height).max(1.0);
2097 }
2098
2099 if let Some((min_x, _min_y, max_x, max_y)) = block_bounds {
2100 content_min_x = content_min_x.min(min_x);
2101 content_max_x = content_max_x.max(max_x);
2102 content_max_y = content_max_y.max(max_y);
2103 }
2104
2105 let extra_vert_for_title = if model.title.is_some() { 40.0 } else { 0.0 };
2108
2109 let vb_min_y = -(diagram_margin_y + extra_vert_for_title);
2112
2113 let mut bounds_box_stopy = (content_max_y + bottom_margin_adj).max(0.0);
2120 if has_boxes {
2121 bounds_box_stopy += box_margin;
2122 }
2123
2124 let mut bounds_box_startx = content_min_x;
2127 let mut bounds_box_stopx = content_max_x;
2128 for i in 0..model.actor_order.len() {
2129 let left = actor_left_x[i];
2130 let right = left + actor_widths[i];
2131 if let Some(bi) = actor_box[i] {
2132 let m = box_margins[bi];
2133 bounds_box_startx = bounds_box_startx.min(left - m);
2134 bounds_box_stopx = bounds_box_stopx.max(right + m);
2135 } else {
2136 bounds_box_startx = bounds_box_startx.min(left);
2137 bounds_box_stopx = bounds_box_stopx.max(right);
2138 }
2139 }
2140
2141 for msg in &model.messages {
2145 let (Some(from), Some(to)) = (msg.from.as_deref(), msg.to.as_deref()) else {
2146 continue;
2147 };
2148 if from != to {
2149 continue;
2150 }
2151 if msg.message_type == 2 {
2153 continue;
2154 }
2155 let Some(&i) = actor_index.get(from) else {
2156 continue;
2157 };
2158 let center_x = actor_centers_x[i] + 1.0;
2159 let text = msg.message.as_str().unwrap_or_default();
2160 let (text_w, _text_h) = if text.is_empty() {
2161 (1.0, 1.0)
2162 } else {
2163 measure_svg_like_with_html_br(measurer, text, &msg_text_style)
2164 };
2165 let dx = (text_w.max(1.0) / 2.0).max(actor_width_min / 2.0);
2166 bounds_box_startx = bounds_box_startx.min(center_x - dx);
2167 bounds_box_stopx = bounds_box_stopx.max(center_x + dx);
2168 }
2169
2170 let bounds = Some(Bounds {
2171 min_x: bounds_box_startx - diagram_margin_x,
2172 min_y: vb_min_y,
2173 max_x: bounds_box_stopx + diagram_margin_x,
2174 max_y: bounds_box_stopy + diagram_margin_y,
2175 });
2176
2177 Ok(SequenceDiagramLayout {
2178 nodes,
2179 edges,
2180 clusters,
2181 bounds,
2182 })
2183}