1#![allow(clippy::too_many_arguments)]
2
3use crate::generated::sequence_text_overrides_11_12_2 as sequence_text_overrides;
4use crate::model::{
5 Bounds, LayoutCluster, LayoutEdge, LayoutLabel, LayoutNode, LayoutPoint, SequenceDiagramLayout,
6};
7use crate::text::{
8 TextMeasurer, TextStyle, WrapMode, split_html_br_lines, wrap_label_like_mermaid_lines,
9 wrap_label_like_mermaid_lines_floored_bbox,
10};
11use crate::{Error, Result};
12use serde::Deserialize;
13use serde_json::Value;
14
15#[derive(Debug, Clone, Deserialize)]
16struct SequenceActor {
17 #[allow(dead_code)]
18 name: String,
19 description: String,
20 #[serde(rename = "type")]
21 actor_type: String,
22 #[allow(dead_code)]
23 wrap: bool,
24}
25
26#[derive(Debug, Clone, Deserialize)]
27struct SequenceMessage {
28 id: String,
29 #[serde(default)]
30 from: Option<String>,
31 #[serde(default)]
32 to: Option<String>,
33 #[serde(rename = "type")]
34 message_type: i32,
35 message: Value,
36 #[allow(dead_code)]
37 wrap: bool,
38 activate: bool,
39 #[serde(default)]
40 placement: Option<i32>,
41}
42
43#[derive(Debug, Clone, Deserialize)]
44struct SequenceBox {
45 #[serde(rename = "actorKeys")]
46 actor_keys: Vec<String>,
47 #[allow(dead_code)]
48 fill: String,
49 name: Option<String>,
50 #[allow(dead_code)]
51 wrap: bool,
52}
53
54#[derive(Debug, Clone, Deserialize)]
55struct SequenceModel {
56 #[serde(rename = "actorOrder")]
57 actor_order: Vec<String>,
58 actors: std::collections::BTreeMap<String, SequenceActor>,
59 #[serde(default)]
60 boxes: Vec<SequenceBox>,
61 messages: Vec<SequenceMessage>,
62 title: Option<String>,
63 #[serde(rename = "createdActors", default)]
64 created_actors: std::collections::BTreeMap<String, usize>,
65 #[serde(rename = "destroyedActors", default)]
66 destroyed_actors: std::collections::BTreeMap<String, usize>,
67}
68
69fn config_f64(cfg: &Value, path: &[&str]) -> Option<f64> {
70 let mut cur = cfg;
71 for key in path {
72 cur = cur.get(*key)?;
73 }
74 cur.as_f64()
75 .or_else(|| cur.as_i64().map(|n| n as f64))
76 .or_else(|| cur.as_u64().map(|n| n as f64))
77}
78
79fn config_string(cfg: &Value, path: &[&str]) -> Option<String> {
80 let mut cur = cfg;
81 for key in path {
82 cur = cur.get(*key)?;
83 }
84 cur.as_str().map(|s| s.to_string())
85}
86
87fn measure_svg_like_with_html_br(
88 measurer: &dyn TextMeasurer,
89 text: &str,
90 style: &TextStyle,
91) -> (f64, f64) {
92 let lines = split_html_br_lines(text);
93 let default_line_height = (style.font_size.max(1.0) * 1.1).max(1.0);
94 if lines.len() <= 1 {
95 let metrics = measurer.measure_wrapped(text, style, None, WrapMode::SvgLikeSingleRun);
96 let h = if metrics.height > 0.0 {
97 metrics.height
98 } else {
99 default_line_height
100 };
101 return (metrics.width.max(0.0), h.max(0.0));
102 }
103 let mut max_w: f64 = 0.0;
104 let mut line_h: f64 = 0.0;
105 for line in &lines {
106 let metrics = measurer.measure_wrapped(line, style, None, WrapMode::SvgLikeSingleRun);
107 max_w = max_w.max(metrics.width.max(0.0));
108 let h = if metrics.height > 0.0 {
109 metrics.height
110 } else {
111 default_line_height
112 };
113 line_h = line_h.max(h.max(0.0));
114 }
115 (
116 max_w,
117 (line_h * lines.len() as f64).max(default_line_height),
118 )
119}
120
121fn sequence_actor_visual_height(
122 actor_type: &str,
123 base_width: f64,
124 base_height: f64,
125 label_box_height: f64,
126) -> f64 {
127 match actor_type {
128 "boundary" => (60.0 + label_box_height).max(1.0),
131 "database" => ((base_width / 4.0) + label_box_height).max(1.0),
134 "entity" => (36.0 + label_box_height).max(1.0),
135 "control" => (36.0 + 2.0 * label_box_height).max(1.0),
137 _ => base_height.max(1.0),
138 }
139}
140
141fn sequence_actor_lifeline_start_y(
142 actor_type: &str,
143 base_height: f64,
144 box_text_margin: f64,
145) -> f64 {
146 match actor_type {
147 "actor" | "boundary" => 80.0,
149 "control" | "entity" => 75.0,
150 "database" => base_height + 2.0 * box_text_margin,
152 _ => base_height,
153 }
154}
155
156pub fn layout_sequence_diagram(
157 semantic: &Value,
158 effective_config: &Value,
159 measurer: &dyn TextMeasurer,
160) -> Result<SequenceDiagramLayout> {
161 let model: SequenceModel = crate::json::from_value_ref(semantic)?;
162
163 let seq_cfg = effective_config.get("sequence").unwrap_or(&Value::Null);
164 let diagram_margin_x = config_f64(seq_cfg, &["diagramMarginX"]).unwrap_or(50.0);
165 let diagram_margin_y = config_f64(seq_cfg, &["diagramMarginY"]).unwrap_or(10.0);
166 let bottom_margin_adj = config_f64(seq_cfg, &["bottomMarginAdj"]).unwrap_or(1.0);
167 let box_margin = config_f64(seq_cfg, &["boxMargin"]).unwrap_or(10.0);
168 let actor_margin = config_f64(seq_cfg, &["actorMargin"]).unwrap_or(50.0);
169 let actor_width_min = config_f64(seq_cfg, &["width"]).unwrap_or(150.0);
170 let actor_height = config_f64(seq_cfg, &["height"]).unwrap_or(65.0);
171 let message_margin = config_f64(seq_cfg, &["messageMargin"]).unwrap_or(35.0);
172 let wrap_padding = config_f64(seq_cfg, &["wrapPadding"]).unwrap_or(10.0);
173 let box_text_margin = config_f64(seq_cfg, &["boxTextMargin"]).unwrap_or(5.0);
174 let label_box_height = config_f64(seq_cfg, &["labelBoxHeight"]).unwrap_or(20.0);
175 let mirror_actors = seq_cfg
176 .get("mirrorActors")
177 .and_then(|v| v.as_bool())
178 .unwrap_or(true);
179
180 let global_font_family = config_string(effective_config, &["fontFamily"]);
183 let global_font_size = config_f64(effective_config, &["fontSize"]);
184 let global_font_weight = config_string(effective_config, &["fontWeight"]);
185
186 let message_font_family = global_font_family
187 .clone()
188 .or_else(|| config_string(seq_cfg, &["messageFontFamily"]));
189 let message_font_size = global_font_size
190 .or_else(|| config_f64(seq_cfg, &["messageFontSize"]))
191 .unwrap_or(16.0);
192 let message_font_weight = global_font_weight
193 .clone()
194 .or_else(|| config_string(seq_cfg, &["messageFontWeight"]));
195
196 let actor_font_family = global_font_family
197 .clone()
198 .or_else(|| config_string(seq_cfg, &["actorFontFamily"]));
199 let actor_font_size = global_font_size
200 .or_else(|| config_f64(seq_cfg, &["actorFontSize"]))
201 .unwrap_or(16.0);
202 let actor_font_weight = global_font_weight
203 .clone()
204 .or_else(|| config_string(seq_cfg, &["actorFontWeight"]));
205
206 let message_width_scale = 1.0;
210
211 let actor_text_style = TextStyle {
212 font_family: actor_font_family,
213 font_size: actor_font_size,
214 font_weight: actor_font_weight,
215 };
216 let note_font_family = global_font_family
217 .clone()
218 .or_else(|| config_string(seq_cfg, &["noteFontFamily"]));
219 let note_font_size = global_font_size
220 .or_else(|| config_f64(seq_cfg, &["noteFontSize"]))
221 .unwrap_or(16.0);
222 let note_font_weight = global_font_weight
223 .clone()
224 .or_else(|| config_string(seq_cfg, &["noteFontWeight"]));
225 let note_text_style = TextStyle {
226 font_family: note_font_family,
227 font_size: note_font_size,
228 font_weight: note_font_weight,
229 };
230 let msg_text_style = TextStyle {
231 font_family: message_font_family,
232 font_size: message_font_size,
233 font_weight: message_font_weight,
234 };
235
236 let has_boxes = !model.boxes.is_empty();
237 let has_box_titles = model
238 .boxes
239 .iter()
240 .any(|b| b.name.as_deref().is_some_and(|s| !s.trim().is_empty()));
241
242 let max_box_title_height = if has_box_titles {
248 let line_h = sequence_text_overrides::sequence_text_dimensions_height_px(message_font_size);
249 model
250 .boxes
251 .iter()
252 .filter_map(|b| b.name.as_deref())
253 .map(|s| split_html_br_lines(s).len().max(1) as f64 * line_h)
254 .fold(0.0, f64::max)
255 } else {
256 0.0
257 };
258
259 if model.actor_order.is_empty() {
260 return Err(Error::InvalidModel {
261 message: "sequence model has no actorOrder".to_string(),
262 });
263 }
264
265 let mut actor_widths: Vec<f64> = Vec::with_capacity(model.actor_order.len());
267 let mut actor_base_heights: Vec<f64> = Vec::with_capacity(model.actor_order.len());
268 for id in &model.actor_order {
269 let a = model.actors.get(id).ok_or_else(|| Error::InvalidModel {
270 message: format!("missing actor {id}"),
271 })?;
272 if a.wrap {
273 let wrap_w = (actor_width_min - 2.0 * wrap_padding).max(1.0);
276 let wrapped_lines =
277 wrap_label_like_mermaid_lines(&a.description, measurer, &actor_text_style, wrap_w);
278 let line_count = wrapped_lines.len().max(1) as f64;
279 let text_h =
280 sequence_text_overrides::sequence_text_dimensions_height_px(actor_font_size)
281 * line_count;
282 actor_base_heights.push(actor_height.max(text_h).max(1.0));
283 actor_widths.push(actor_width_min.max(1.0));
284 } else {
285 let (w0, _h0) =
286 measure_svg_like_with_html_br(measurer, &a.description, &actor_text_style);
287 let w = (w0 + 2.0 * wrap_padding).max(actor_width_min);
288 actor_base_heights.push(actor_height.max(1.0));
289 actor_widths.push(w.max(1.0));
290 }
291 }
292
293 let mut actor_index: std::collections::HashMap<&str, usize> = std::collections::HashMap::new();
296 for (i, id) in model.actor_order.iter().enumerate() {
297 actor_index.insert(id.as_str(), i);
298 }
299
300 let mut actor_to_message_width: Vec<f64> = vec![0.0; model.actor_order.len()];
301 for msg in &model.messages {
302 let (Some(from), Some(to)) = (msg.from.as_deref(), msg.to.as_deref()) else {
303 continue;
304 };
305 let Some(&from_idx) = actor_index.get(from) else {
306 continue;
307 };
308 let Some(&to_idx) = actor_index.get(to) else {
309 continue;
310 };
311
312 let placement = msg.placement;
313 if placement == Some(0) && to_idx == 0 {
315 continue;
316 }
317 if placement == Some(1) && to_idx + 1 == model.actor_order.len() {
319 continue;
320 }
321
322 let is_note = placement.is_some();
323 let is_message = !is_note;
324 let style = if is_note {
325 ¬e_text_style
326 } else {
327 &msg_text_style
328 };
329 let text = msg.message.as_str().unwrap_or_default();
330 if text.is_empty() {
331 continue;
332 }
333
334 let measured_text = if msg.wrap {
335 let wrap_w = (actor_width_min - 2.0 * wrap_padding).max(1.0);
338 let lines = wrap_label_like_mermaid_lines(text, measurer, style, wrap_w);
339 lines.join("<br>")
340 } else {
341 text.to_string()
342 };
343 let (w0, _h0) = measure_svg_like_with_html_br(measurer, &measured_text, style);
344 let w0 = w0 * message_width_scale;
345 let message_w = (w0 + 2.0 * wrap_padding).max(0.0);
346
347 let prev_idx = if to_idx > 0 { Some(to_idx - 1) } else { None };
348 let next_idx = if to_idx + 1 < model.actor_order.len() {
349 Some(to_idx + 1)
350 } else {
351 None
352 };
353
354 if is_message && next_idx.is_some_and(|n| n == from_idx) {
355 actor_to_message_width[to_idx] = actor_to_message_width[to_idx].max(message_w);
356 } else if is_message && prev_idx.is_some_and(|p| p == from_idx) {
357 actor_to_message_width[from_idx] = actor_to_message_width[from_idx].max(message_w);
358 } else if is_message && from_idx == to_idx {
359 let half = message_w / 2.0;
360 actor_to_message_width[from_idx] = actor_to_message_width[from_idx].max(half);
361 actor_to_message_width[to_idx] = actor_to_message_width[to_idx].max(half);
362 } else if placement == Some(1) {
363 actor_to_message_width[from_idx] = actor_to_message_width[from_idx].max(message_w);
365 } else if placement == Some(0) {
366 if let Some(p) = prev_idx {
368 actor_to_message_width[p] = actor_to_message_width[p].max(message_w);
369 }
370 } else if placement == Some(2) {
371 if let Some(p) = prev_idx {
373 actor_to_message_width[p] = actor_to_message_width[p].max(message_w / 2.0);
374 }
375 if next_idx.is_some() {
376 actor_to_message_width[from_idx] =
377 actor_to_message_width[from_idx].max(message_w / 2.0);
378 }
379 }
380 }
381
382 let mut actor_margins: Vec<f64> = vec![actor_margin; model.actor_order.len()];
383 for i in 0..model.actor_order.len() {
384 let msg_w = actor_to_message_width[i];
385 if msg_w <= 0.0 {
386 continue;
387 }
388 let w0 = actor_widths[i];
389 let actor_w = if i + 1 < model.actor_order.len() {
390 let w1 = actor_widths[i + 1];
391 msg_w + actor_margin - (w0 / 2.0) - (w1 / 2.0)
392 } else {
393 msg_w + actor_margin - (w0 / 2.0)
394 };
395 actor_margins[i] = actor_w.max(actor_margin);
396 }
397
398 let mut box_margins: Vec<f64> = vec![box_text_margin; model.boxes.len()];
402 for (box_idx, b) in model.boxes.iter().enumerate() {
403 let mut total_width = 0.0;
404 for actor_key in &b.actor_keys {
405 let Some(&i) = actor_index.get(actor_key.as_str()) else {
406 continue;
407 };
408 let actor_margin_for_box = if actor_to_message_width[i] > 0.0 {
409 actor_margins[i]
410 } else {
411 0.0
412 };
413 total_width += actor_widths[i] + actor_margin_for_box;
414 }
415
416 total_width += box_margin * 8.0;
417 total_width -= 2.0 * box_text_margin;
418
419 let Some(name) = b.name.as_deref().filter(|s| !s.trim().is_empty()) else {
420 continue;
421 };
422
423 let (text_w, _text_h) = measure_svg_like_with_html_br(measurer, name, &msg_text_style);
424 let min_width = total_width.max(text_w + 2.0 * wrap_padding);
425 if total_width < min_width {
426 box_margins[box_idx] += (min_width - total_width) / 2.0;
427 }
428 }
429
430 let mut actor_top_offset_y = 0.0;
432 if has_boxes {
433 actor_top_offset_y += box_margin;
434 if has_box_titles {
435 actor_top_offset_y += max_box_title_height;
436 }
437 }
438
439 let mut actor_box: Vec<Option<usize>> = vec![None; model.actor_order.len()];
441 for (box_idx, b) in model.boxes.iter().enumerate() {
442 for actor_key in &b.actor_keys {
443 let Some(&i) = actor_index.get(actor_key.as_str()) else {
444 continue;
445 };
446 actor_box[i] = Some(box_idx);
447 }
448 }
449
450 let mut actor_left_x: Vec<f64> = Vec::with_capacity(model.actor_order.len());
451 let mut prev_width = 0.0;
452 let mut prev_margin = 0.0;
453 let mut prev_box: Option<usize> = None;
454 for i in 0..model.actor_order.len() {
455 let w = actor_widths[i];
456 let cur_box = actor_box[i];
457
458 if prev_box.is_some() && prev_box != cur_box {
460 if let Some(prev) = prev_box {
461 prev_margin += box_margin + box_margins[prev];
462 }
463 }
464
465 if cur_box.is_some() && cur_box != prev_box {
467 if let Some(bi) = cur_box {
468 prev_margin += box_margins[bi];
469 }
470 }
471
472 if model.created_actors.contains_key(&model.actor_order[i]) {
474 prev_margin += w / 2.0;
475 }
476 let x = prev_width + prev_margin;
477 actor_left_x.push(x);
478 prev_width += w + prev_margin;
479 prev_margin = actor_margins[i];
480 prev_box = cur_box;
481 }
482
483 let mut actor_centers_x: Vec<f64> = Vec::with_capacity(model.actor_order.len());
484 for i in 0..model.actor_order.len() {
485 actor_centers_x.push(actor_left_x[i] + actor_widths[i] / 2.0);
486 }
487
488 let message_step = message_margin + (message_font_size / 2.0) + bottom_margin_adj;
489 let msg_label_offset = (message_step - message_font_size) + bottom_margin_adj;
490
491 let mut edges: Vec<LayoutEdge> = Vec::new();
492 let mut nodes: Vec<LayoutNode> = Vec::new();
493 let clusters: Vec<LayoutCluster> = Vec::new();
494
495 let mut max_actor_visual_height: f64 = 0.0;
499 for (idx, id) in model.actor_order.iter().enumerate() {
500 let w = actor_widths[idx];
501 let cx = actor_centers_x[idx];
502 let base_h = actor_base_heights[idx];
503 let actor_type = model
504 .actors
505 .get(id)
506 .map(|a| a.actor_type.as_str())
507 .unwrap_or("participant");
508 let visual_h = sequence_actor_visual_height(actor_type, w, base_h, label_box_height);
509 max_actor_visual_height = max_actor_visual_height.max(visual_h.max(1.0));
510 let top_y = actor_top_offset_y + visual_h / 2.0;
511 nodes.push(LayoutNode {
512 id: format!("actor-top-{id}"),
513 x: cx,
514 y: top_y,
515 width: w,
516 height: visual_h,
517 is_cluster: false,
518 label_width: None,
519 label_height: None,
520 });
521 }
522
523 fn bracketize(s: &str) -> String {
526 let t = s.trim();
527 if t.is_empty() {
528 return "\u{200B}".to_string();
529 }
530 if t.starts_with('[') && t.ends_with(']') {
531 return t.to_string();
532 }
533 format!("[{t}]")
534 }
535
536 fn block_label_text(raw_label: &str) -> String {
537 bracketize(raw_label)
538 }
539
540 let block_base_step = (2.0 * box_margin + box_text_margin + label_box_height).max(0.0);
554 let block_base_step_empty = (block_base_step - label_box_height).max(0.0);
555 let line_step = sequence_text_overrides::sequence_text_line_step_px(message_font_size);
556 let block_extra_per_line = (line_step - box_text_margin).max(0.0);
557 let block_end_step = 10.0;
558
559 let mut msg_by_id: std::collections::HashMap<&str, &SequenceMessage> =
560 std::collections::HashMap::new();
561 for msg in &model.messages {
562 msg_by_id.insert(msg.id.as_str(), msg);
563 }
564
565 fn is_self_message_id(
566 msg_id: &str,
567 msg_by_id: &std::collections::HashMap<&str, &SequenceMessage>,
568 ) -> bool {
569 let Some(msg) = msg_by_id.get(msg_id).copied() else {
570 return false;
571 };
572 if msg.message_type == 2 {
574 return false;
575 }
576 msg.from
577 .as_deref()
578 .is_some_and(|from| Some(from) == msg.to.as_deref())
579 }
580
581 fn message_span_x(
582 msg: &SequenceMessage,
583 actor_index: &std::collections::HashMap<&str, usize>,
584 actor_centers_x: &[f64],
585 measurer: &dyn TextMeasurer,
586 msg_text_style: &TextStyle,
587 message_width_scale: f64,
588 ) -> Option<(f64, f64)> {
589 let (Some(from), Some(to)) = (msg.from.as_deref(), msg.to.as_deref()) else {
590 return None;
591 };
592 let (Some(fi), Some(ti)) = (actor_index.get(from).copied(), actor_index.get(to).copied())
593 else {
594 return None;
595 };
596 let from_x = actor_centers_x[fi];
597 let to_x = actor_centers_x[ti];
598 let sign = if to_x >= from_x { 1.0 } else { -1.0 };
599 let x1 = from_x + sign * 1.0;
600 let x2 = if from == to { x1 } else { to_x - sign * 4.0 };
601 let cx = (x1 + x2) / 2.0;
602
603 let text = msg.message.as_str().unwrap_or_default();
604 let w = if text.is_empty() {
605 1.0
606 } else {
607 let (w, _h) = measure_svg_like_with_html_br(measurer, text, msg_text_style);
608 (w * message_width_scale).max(1.0)
609 };
610 Some((cx - w / 2.0, cx + w / 2.0))
611 }
612
613 fn block_frame_width(
614 message_ids: &[String],
615 msg_by_id: &std::collections::HashMap<&str, &SequenceMessage>,
616 actor_index: &std::collections::HashMap<&str, usize>,
617 actor_centers_x: &[f64],
618 actor_widths: &[f64],
619 message_margin: f64,
620 box_text_margin: f64,
621 bottom_margin_adj: f64,
622 measurer: &dyn TextMeasurer,
623 msg_text_style: &TextStyle,
624 message_width_scale: f64,
625 ) -> Option<f64> {
626 let mut actor_idxs: Vec<usize> = Vec::new();
627 for msg_id in message_ids {
628 let Some(msg) = msg_by_id.get(msg_id.as_str()).copied() else {
629 continue;
630 };
631 let (Some(from), Some(to)) = (msg.from.as_deref(), msg.to.as_deref()) else {
632 continue;
633 };
634 if let Some(i) = actor_index.get(from).copied() {
635 actor_idxs.push(i);
636 }
637 if let Some(i) = actor_index.get(to).copied() {
638 actor_idxs.push(i);
639 }
640 }
641 actor_idxs.sort();
642 actor_idxs.dedup();
643 if actor_idxs.is_empty() {
644 return None;
645 }
646
647 if actor_idxs.len() == 1 {
648 let i = actor_idxs[0];
649 let actor_w = actor_widths.get(i).copied().unwrap_or(150.0);
650 let half_width =
651 actor_w / 2.0 + (message_margin / 2.0) + box_text_margin + bottom_margin_adj;
652 let w = (2.0 * half_width).max(1.0);
653 return Some(w);
654 }
655
656 let min_i = actor_idxs.first().copied()?;
657 let max_i = actor_idxs.last().copied()?;
658 let mut x1 = actor_centers_x[min_i] - 11.0;
659 let mut x2 = actor_centers_x[max_i] + 11.0;
660
661 for msg_id in message_ids {
663 let Some(msg) = msg_by_id.get(msg_id.as_str()).copied() else {
664 continue;
665 };
666 let Some((l, r)) = message_span_x(
667 msg,
668 actor_index,
669 actor_centers_x,
670 measurer,
671 msg_text_style,
672 message_width_scale,
673 ) else {
674 continue;
675 };
676 if l < x1 {
677 x1 = l.floor();
678 }
679 if r > x2 {
680 x2 = r.ceil();
681 }
682 }
683
684 Some((x2 - x1).max(1.0))
685 }
686
687 #[derive(Debug, Clone)]
688 enum BlockStackEntry {
689 Loop {
690 start_id: String,
691 raw_label: String,
692 messages: Vec<String>,
693 },
694 Opt {
695 start_id: String,
696 raw_label: String,
697 messages: Vec<String>,
698 },
699 Break {
700 start_id: String,
701 raw_label: String,
702 messages: Vec<String>,
703 },
704 Alt {
705 section_directives: Vec<(String, String)>,
706 sections: Vec<Vec<String>>,
707 },
708 Par {
709 section_directives: Vec<(String, String)>,
710 sections: Vec<Vec<String>>,
711 },
712 Critical {
713 section_directives: Vec<(String, String)>,
714 sections: Vec<Vec<String>>,
715 },
716 }
717
718 let mut directive_steps: std::collections::HashMap<String, f64> =
719 std::collections::HashMap::new();
720 let mut stack: Vec<BlockStackEntry> = Vec::new();
721 for msg in &model.messages {
722 let raw_label = msg.message.as_str().unwrap_or_default();
723 match msg.message_type {
724 10 => stack.push(BlockStackEntry::Loop {
726 start_id: msg.id.clone(),
727 raw_label: raw_label.to_string(),
728 messages: Vec::new(),
729 }),
730 11 => {
731 if let Some(BlockStackEntry::Loop {
732 start_id,
733 raw_label,
734 messages,
735 }) = stack.pop()
736 {
737 let loop_has_self_message = messages
738 .iter()
739 .any(|msg_id| is_self_message_id(msg_id.as_str(), &msg_by_id));
740 let loop_end_step = if loop_has_self_message {
741 40.0
742 } else {
743 block_end_step
744 };
745
746 if raw_label.trim().is_empty() {
747 directive_steps.insert(start_id, block_base_step_empty);
748 } else if let Some(w) = block_frame_width(
749 &messages,
750 &msg_by_id,
751 &actor_index,
752 &actor_centers_x,
753 &actor_widths,
754 message_margin,
755 box_text_margin,
756 bottom_margin_adj,
757 measurer,
758 &msg_text_style,
759 message_width_scale,
760 ) {
761 let label = block_label_text(&raw_label);
762 let metrics = measurer.measure_wrapped(
763 &label,
764 &msg_text_style,
765 Some(w),
766 WrapMode::SvgLikeSingleRun,
767 );
768 let extra =
769 (metrics.line_count.saturating_sub(1) as f64) * block_extra_per_line;
770 directive_steps.insert(start_id, block_base_step + extra);
771 } else {
772 directive_steps.insert(start_id, block_base_step);
773 }
774
775 directive_steps.insert(msg.id.clone(), loop_end_step);
776 }
777 }
778 15 => stack.push(BlockStackEntry::Opt {
780 start_id: msg.id.clone(),
781 raw_label: raw_label.to_string(),
782 messages: Vec::new(),
783 }),
784 16 => {
785 let mut end_step = block_end_step;
786 if let Some(BlockStackEntry::Opt {
787 start_id,
788 raw_label,
789 messages,
790 }) = stack.pop()
791 {
792 let has_self = messages
793 .iter()
794 .any(|msg_id| is_self_message_id(msg_id.as_str(), &msg_by_id));
795 end_step = if has_self { 40.0 } else { block_end_step };
796 if raw_label.trim().is_empty() {
797 directive_steps.insert(start_id, block_base_step_empty);
798 } else if let Some(w) = block_frame_width(
799 &messages,
800 &msg_by_id,
801 &actor_index,
802 &actor_centers_x,
803 &actor_widths,
804 message_margin,
805 box_text_margin,
806 bottom_margin_adj,
807 measurer,
808 &msg_text_style,
809 message_width_scale,
810 ) {
811 let label = block_label_text(&raw_label);
812 let metrics = measurer.measure_wrapped(
813 &label,
814 &msg_text_style,
815 Some(w),
816 WrapMode::SvgLikeSingleRun,
817 );
818 let extra =
819 (metrics.line_count.saturating_sub(1) as f64) * block_extra_per_line;
820 directive_steps.insert(start_id, block_base_step + extra);
821 } else {
822 directive_steps.insert(start_id, block_base_step);
823 }
824 }
825 directive_steps.insert(msg.id.clone(), end_step);
826 }
827 30 => stack.push(BlockStackEntry::Break {
829 start_id: msg.id.clone(),
830 raw_label: raw_label.to_string(),
831 messages: Vec::new(),
832 }),
833 31 => {
834 let mut end_step = block_end_step;
835 if let Some(BlockStackEntry::Break {
836 start_id,
837 raw_label,
838 messages,
839 }) = stack.pop()
840 {
841 let has_self = messages
842 .iter()
843 .any(|msg_id| is_self_message_id(msg_id.as_str(), &msg_by_id));
844 end_step = if has_self { 40.0 } else { block_end_step };
845 if raw_label.trim().is_empty() {
846 directive_steps.insert(start_id, block_base_step_empty);
847 } else if let Some(w) = block_frame_width(
848 &messages,
849 &msg_by_id,
850 &actor_index,
851 &actor_centers_x,
852 &actor_widths,
853 message_margin,
854 box_text_margin,
855 bottom_margin_adj,
856 measurer,
857 &msg_text_style,
858 message_width_scale,
859 ) {
860 let label = block_label_text(&raw_label);
861 let metrics = measurer.measure_wrapped(
862 &label,
863 &msg_text_style,
864 Some(w),
865 WrapMode::SvgLikeSingleRun,
866 );
867 let extra =
868 (metrics.line_count.saturating_sub(1) as f64) * block_extra_per_line;
869 directive_steps.insert(start_id, block_base_step + extra);
870 } else {
871 directive_steps.insert(start_id, block_base_step);
872 }
873 }
874 directive_steps.insert(msg.id.clone(), end_step);
875 }
876 12 => stack.push(BlockStackEntry::Alt {
878 section_directives: vec![(msg.id.clone(), raw_label.to_string())],
879 sections: vec![Vec::new()],
880 }),
881 13 => {
882 if let Some(BlockStackEntry::Alt {
883 section_directives,
884 sections,
885 }) = stack.last_mut()
886 {
887 section_directives.push((msg.id.clone(), raw_label.to_string()));
888 sections.push(Vec::new());
889 }
890 }
891 14 => {
892 let mut end_step = block_end_step;
893 if let Some(BlockStackEntry::Alt {
894 section_directives,
895 sections,
896 }) = stack.pop()
897 {
898 let has_self = sections
899 .iter()
900 .flatten()
901 .any(|msg_id| is_self_message_id(msg_id.as_str(), &msg_by_id));
902 end_step = if has_self { 40.0 } else { block_end_step };
903 let mut message_ids: Vec<String> = Vec::new();
904 for sec in §ions {
905 message_ids.extend(sec.iter().cloned());
906 }
907 if let Some(w) = block_frame_width(
908 &message_ids,
909 &msg_by_id,
910 &actor_index,
911 &actor_centers_x,
912 &actor_widths,
913 message_margin,
914 box_text_margin,
915 bottom_margin_adj,
916 measurer,
917 &msg_text_style,
918 message_width_scale,
919 ) {
920 for (idx, (id, raw)) in section_directives.into_iter().enumerate() {
921 let is_empty = raw.trim().is_empty();
922 if is_empty {
923 directive_steps.insert(id, block_base_step_empty);
924 continue;
925 }
926 let _ = idx;
927 let label = block_label_text(&raw);
928 let metrics = measurer.measure_wrapped(
929 &label,
930 &msg_text_style,
931 Some(w),
932 WrapMode::SvgLikeSingleRun,
933 );
934 let extra = (metrics.line_count.saturating_sub(1) as f64)
935 * block_extra_per_line;
936 directive_steps.insert(id, block_base_step + extra);
937 }
938 } else {
939 for (id, raw) in section_directives {
940 let step = if raw.trim().is_empty() {
941 block_base_step_empty
942 } else {
943 block_base_step
944 };
945 directive_steps.insert(id, step);
946 }
947 }
948 }
949 directive_steps.insert(msg.id.clone(), end_step);
950 }
951 19 | 32 => stack.push(BlockStackEntry::Par {
953 section_directives: vec![(msg.id.clone(), raw_label.to_string())],
954 sections: vec![Vec::new()],
955 }),
956 20 => {
957 if let Some(BlockStackEntry::Par {
958 section_directives,
959 sections,
960 }) = stack.last_mut()
961 {
962 section_directives.push((msg.id.clone(), raw_label.to_string()));
963 sections.push(Vec::new());
964 }
965 }
966 21 => {
967 let mut end_step = block_end_step;
968 if let Some(BlockStackEntry::Par {
969 section_directives,
970 sections,
971 }) = stack.pop()
972 {
973 let has_self = sections
974 .iter()
975 .flatten()
976 .any(|msg_id| is_self_message_id(msg_id.as_str(), &msg_by_id));
977 end_step = if has_self { 40.0 } else { block_end_step };
978 let mut message_ids: Vec<String> = Vec::new();
979 for sec in §ions {
980 message_ids.extend(sec.iter().cloned());
981 }
982 if let Some(w) = block_frame_width(
983 &message_ids,
984 &msg_by_id,
985 &actor_index,
986 &actor_centers_x,
987 &actor_widths,
988 message_margin,
989 box_text_margin,
990 bottom_margin_adj,
991 measurer,
992 &msg_text_style,
993 message_width_scale,
994 ) {
995 for (idx, (id, raw)) in section_directives.into_iter().enumerate() {
996 let is_empty = raw.trim().is_empty();
997 if is_empty {
998 directive_steps.insert(id, block_base_step_empty);
999 continue;
1000 }
1001 let _ = idx;
1002 let label = block_label_text(&raw);
1003 let metrics = measurer.measure_wrapped(
1004 &label,
1005 &msg_text_style,
1006 Some(w),
1007 WrapMode::SvgLikeSingleRun,
1008 );
1009 let extra = (metrics.line_count.saturating_sub(1) as f64)
1010 * block_extra_per_line;
1011 directive_steps.insert(id, block_base_step + extra);
1012 }
1013 } else {
1014 for (id, raw) in section_directives {
1015 let step = if raw.trim().is_empty() {
1016 block_base_step_empty
1017 } else {
1018 block_base_step
1019 };
1020 directive_steps.insert(id, step);
1021 }
1022 }
1023 }
1024 directive_steps.insert(msg.id.clone(), end_step);
1025 }
1026 27 => stack.push(BlockStackEntry::Critical {
1028 section_directives: vec![(msg.id.clone(), raw_label.to_string())],
1029 sections: vec![Vec::new()],
1030 }),
1031 28 => {
1032 if let Some(BlockStackEntry::Critical {
1033 section_directives,
1034 sections,
1035 }) = stack.last_mut()
1036 {
1037 section_directives.push((msg.id.clone(), raw_label.to_string()));
1038 sections.push(Vec::new());
1039 }
1040 }
1041 29 => {
1042 let mut end_step = block_end_step;
1043 if let Some(BlockStackEntry::Critical {
1044 section_directives,
1045 sections,
1046 }) = stack.pop()
1047 {
1048 let has_self = sections
1049 .iter()
1050 .flatten()
1051 .any(|msg_id| is_self_message_id(msg_id.as_str(), &msg_by_id));
1052 end_step = if has_self { 40.0 } else { block_end_step };
1053 let mut message_ids: Vec<String> = Vec::new();
1054 for sec in §ions {
1055 message_ids.extend(sec.iter().cloned());
1056 }
1057 if let Some(w) = block_frame_width(
1058 &message_ids,
1059 &msg_by_id,
1060 &actor_index,
1061 &actor_centers_x,
1062 &actor_widths,
1063 message_margin,
1064 box_text_margin,
1065 bottom_margin_adj,
1066 measurer,
1067 &msg_text_style,
1068 message_width_scale,
1069 ) {
1070 for (idx, (id, raw)) in section_directives.into_iter().enumerate() {
1071 let is_empty = raw.trim().is_empty();
1072 if is_empty {
1073 directive_steps.insert(id, block_base_step_empty);
1074 continue;
1075 }
1076 let _ = idx;
1077 let label = block_label_text(&raw);
1078 let metrics = measurer.measure_wrapped(
1079 &label,
1080 &msg_text_style,
1081 Some(w),
1082 WrapMode::SvgLikeSingleRun,
1083 );
1084 let extra = (metrics.line_count.saturating_sub(1) as f64)
1085 * block_extra_per_line;
1086 directive_steps.insert(id, block_base_step + extra);
1087 }
1088 } else {
1089 for (id, raw) in section_directives {
1090 let step = if raw.trim().is_empty() {
1091 block_base_step_empty
1092 } else {
1093 block_base_step
1094 };
1095 directive_steps.insert(id, step);
1096 }
1097 }
1098 }
1099 directive_steps.insert(msg.id.clone(), end_step);
1100 }
1101 _ => {
1102 if msg.from.is_some() && msg.to.is_some() {
1105 for entry in stack.iter_mut() {
1106 match entry {
1107 BlockStackEntry::Alt { sections, .. }
1108 | BlockStackEntry::Par { sections, .. }
1109 | BlockStackEntry::Critical { sections, .. } => {
1110 if let Some(cur) = sections.last_mut() {
1111 cur.push(msg.id.clone());
1112 }
1113 }
1114 BlockStackEntry::Loop { messages, .. }
1115 | BlockStackEntry::Opt { messages, .. }
1116 | BlockStackEntry::Break { messages, .. } => {
1117 messages.push(msg.id.clone());
1118 }
1119 }
1120 }
1121 }
1122 }
1123 }
1124 }
1125
1126 #[derive(Debug, Clone)]
1127 struct RectOpen {
1128 start_id: String,
1129 top_y: f64,
1130 bounds: Option<merman_core::geom::Box2>,
1131 }
1132
1133 impl RectOpen {
1134 fn include_min_max(&mut self, min_x: f64, max_x: f64, max_y: f64) {
1135 let r = merman_core::geom::Box2::from_min_max(min_x, self.top_y, max_x, max_y);
1136 if let Some(ref mut cur) = self.bounds {
1137 cur.union(r);
1138 } else {
1139 self.bounds = Some(r);
1140 }
1141 }
1142 }
1143
1144 let note_width_single = actor_width_min;
1148 let rect_step_start = 20.0;
1149 let rect_step_end = 10.0;
1150 let note_gap = 10.0;
1151 let note_text_pad_total = sequence_text_overrides::sequence_note_text_pad_total_px();
1154 let note_top_offset = message_step - note_gap;
1155
1156 let mut cursor_y = actor_top_offset_y + max_actor_visual_height + message_step;
1157 let mut rect_stack: Vec<RectOpen> = Vec::new();
1158 let activation_width = config_f64(seq_cfg, &["activationWidth"])
1159 .unwrap_or(10.0)
1160 .max(1.0);
1161 let mut activation_stacks: std::collections::BTreeMap<&str, Vec<f64>> =
1162 std::collections::BTreeMap::new();
1163
1164 let mut created_actor_top_center_y: std::collections::BTreeMap<String, f64> =
1169 std::collections::BTreeMap::new();
1170 let mut destroyed_actor_bottom_top_y: std::collections::BTreeMap<String, f64> =
1171 std::collections::BTreeMap::new();
1172
1173 let actor_visual_height_for_id = |actor_id: &str| -> f64 {
1174 let Some(idx) = actor_index.get(actor_id).copied() else {
1175 return actor_height.max(1.0);
1176 };
1177 let w = actor_widths.get(idx).copied().unwrap_or(actor_width_min);
1178 let base_h = actor_base_heights.get(idx).copied().unwrap_or(actor_height);
1179 model
1180 .actors
1181 .get(actor_id)
1182 .map(|a| a.actor_type.as_str())
1183 .map(|t| sequence_actor_visual_height(t, w, base_h, label_box_height))
1184 .unwrap_or(base_h.max(1.0))
1185 };
1186 let actor_is_type_width_limited = |actor_id: &str| -> bool {
1187 model
1188 .actors
1189 .get(actor_id)
1190 .map(|a| {
1191 matches!(
1192 a.actor_type.as_str(),
1193 "actor" | "control" | "entity" | "database"
1194 )
1195 })
1196 .unwrap_or(false)
1197 };
1198
1199 for (msg_idx, msg) in model.messages.iter().enumerate() {
1200 match msg.message_type {
1201 17 => {
1203 let Some(actor_id) = msg.from.as_deref() else {
1204 continue;
1205 };
1206 let Some(&idx) = actor_index.get(actor_id) else {
1207 continue;
1208 };
1209 let cx = actor_centers_x[idx];
1210 let stack = activation_stacks.entry(actor_id).or_default();
1211 let stacked_size = stack.len();
1212 let startx = cx + (((stacked_size as f64) - 1.0) * activation_width) / 2.0;
1213 stack.push(startx);
1214 continue;
1215 }
1216 18 => {
1218 let Some(actor_id) = msg.from.as_deref() else {
1219 continue;
1220 };
1221 if let Some(stack) = activation_stacks.get_mut(actor_id) {
1222 let _ = stack.pop();
1223 }
1224 continue;
1225 }
1226 _ => {}
1227 }
1228
1229 if let Some(step) = directive_steps.get(msg.id.as_str()).copied() {
1230 cursor_y += step;
1231 continue;
1232 }
1233 match msg.message_type {
1234 22 => {
1236 rect_stack.push(RectOpen {
1237 start_id: msg.id.clone(),
1238 top_y: cursor_y - note_top_offset,
1239 bounds: None,
1240 });
1241 cursor_y += rect_step_start;
1242 continue;
1243 }
1244 23 => {
1246 if let Some(open) = rect_stack.pop() {
1247 let rect_left = open.bounds.map(|b| b.min_x()).unwrap_or_else(|| {
1248 actor_centers_x
1249 .iter()
1250 .copied()
1251 .fold(f64::INFINITY, f64::min)
1252 - 11.0
1253 });
1254 let rect_right = open.bounds.map(|b| b.max_x()).unwrap_or_else(|| {
1255 actor_centers_x
1256 .iter()
1257 .copied()
1258 .fold(f64::NEG_INFINITY, f64::max)
1259 + 11.0
1260 });
1261 let rect_bottom = open
1262 .bounds
1263 .map(|b| b.max_y() + 10.0)
1264 .unwrap_or(open.top_y + 10.0);
1265 let rect_w = (rect_right - rect_left).max(1.0);
1266 let rect_h = (rect_bottom - open.top_y).max(1.0);
1267
1268 nodes.push(LayoutNode {
1269 id: format!("rect-{}", open.start_id),
1270 x: rect_left + rect_w / 2.0,
1271 y: open.top_y + rect_h / 2.0,
1272 width: rect_w,
1273 height: rect_h,
1274 is_cluster: false,
1275 label_width: None,
1276 label_height: None,
1277 });
1278
1279 if let Some(parent) = rect_stack.last_mut() {
1280 parent.include_min_max(rect_left - 10.0, rect_right + 10.0, rect_bottom);
1281 }
1282 }
1283 cursor_y += rect_step_end;
1284 continue;
1285 }
1286 _ => {}
1287 }
1288
1289 if msg.message_type == 2 {
1291 let (Some(from), Some(to)) = (msg.from.as_deref(), msg.to.as_deref()) else {
1292 continue;
1293 };
1294 let (Some(fi), Some(ti)) =
1295 (actor_index.get(from).copied(), actor_index.get(to).copied())
1296 else {
1297 continue;
1298 };
1299 let fx = actor_centers_x[fi];
1300 let tx = actor_centers_x[ti];
1301
1302 let placement = msg.placement.unwrap_or(2);
1303 let (mut note_x, mut note_w) = match placement {
1304 0 => (fx - 25.0 - note_width_single, note_width_single),
1306 1 => (fx + 25.0, note_width_single),
1308 _ => {
1310 if (fx - tx).abs() < 0.0001 {
1311 let mut w = note_width_single;
1318 if msg.wrap {
1319 w = w.max(actor_widths.get(fi).copied().unwrap_or(note_width_single));
1320 }
1321 (fx - (w / 2.0), w)
1322 } else {
1323 let left = fx.min(tx) - 25.0;
1324 let right = fx.max(tx) + 25.0;
1325 let w = (right - left).max(note_width_single);
1326 (left, w)
1327 }
1328 }
1329 };
1330
1331 let text = msg.message.as_str().unwrap_or_default();
1332 let (text_w, h) = if msg.wrap {
1333 let w0 = {
1341 let init_lines = wrap_label_like_mermaid_lines_floored_bbox(
1342 text,
1343 measurer,
1344 ¬e_text_style,
1345 (note_width_single
1346 + sequence_text_overrides::sequence_note_wrap_slack_px())
1347 .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 + sequence_text_overrides::sequence_note_wrap_slack_px()).max(1.0),
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 let edge_id = format!("msg-{item_id}");
1749 if let Some(e) = edges_by_id.get(edge_id.as_str()).copied() {
1750 let y = e.points.first()?.y;
1751 let extra = msg_endpoints
1752 .get(item_id)
1753 .copied()
1754 .filter(|(from, to)| from == to)
1755 .map(|_| sequence_text_overrides::sequence_self_message_frame_extra_y_px())
1756 .unwrap_or(0.0);
1757 return Some((y, y + extra));
1758 }
1759
1760 let node_id = format!("note-{item_id}");
1761 let n = nodes_by_id.get(node_id.as_str()).copied()?;
1762 let top = n.y - n.height / 2.0;
1763 let bottom = n.y + n.height / 2.0;
1764 Some((top, bottom))
1765 }
1766
1767 fn frame_x_from_item_ids<'a>(
1768 item_ids: impl IntoIterator<Item = &'a String>,
1769 nodes_by_id: &HashMap<&str, &LayoutNode>,
1770 edges_by_id: &HashMap<&str, &LayoutEdge>,
1771 msg_endpoints: &HashMap<&str, (&str, &str)>,
1772 ) -> Option<(f64, f64, f64)> {
1773 let mut min_cx = f64::INFINITY;
1774 let mut max_cx = f64::NEG_INFINITY;
1775 let mut min_left = f64::INFINITY;
1776 let mut geom_min_x = f64::INFINITY;
1777 let mut geom_max_x = f64::NEG_INFINITY;
1778
1779 for id in item_ids {
1780 let note_id = format!("note-{id}");
1782 if let Some(n) = nodes_by_id.get(note_id.as_str()).copied() {
1783 geom_min_x = geom_min_x.min(
1784 n.x - n.width / 2.0 - sequence_text_overrides::sequence_frame_geom_pad_px(),
1785 );
1786 geom_max_x = geom_max_x.max(
1787 n.x + n.width / 2.0 + sequence_text_overrides::sequence_frame_geom_pad_px(),
1788 );
1789 }
1790
1791 let Some((from, to)) = msg_endpoints.get(id.as_str()).copied() else {
1792 continue;
1793 };
1794 for actor_id in [from, to] {
1795 let actor_node_id = format!("actor-top-{actor_id}");
1796 let Some(n) = nodes_by_id.get(actor_node_id.as_str()).copied() else {
1797 continue;
1798 };
1799 min_cx = min_cx.min(n.x);
1800 max_cx = max_cx.max(n.x);
1801 min_left = min_left.min(n.x - n.width / 2.0);
1802 }
1803
1804 let edge_id = format!("msg-{id}");
1806 if let Some(e) = edges_by_id.get(edge_id.as_str()).copied() {
1807 for p in &e.points {
1808 geom_min_x = geom_min_x.min(p.x);
1809 geom_max_x = geom_max_x.max(p.x);
1810 }
1811 if let Some(label) = e.label.as_ref() {
1812 geom_min_x = geom_min_x.min(
1813 label.x
1814 - (label.width / 2.0)
1815 - sequence_text_overrides::sequence_frame_geom_pad_px(),
1816 );
1817 geom_max_x = geom_max_x.max(
1818 label.x
1819 + (label.width / 2.0)
1820 + sequence_text_overrides::sequence_frame_geom_pad_px(),
1821 );
1822 }
1823 }
1824 }
1825
1826 if !min_cx.is_finite() || !max_cx.is_finite() {
1827 return None;
1828 }
1829 let mut x1 = min_cx - sequence_text_overrides::sequence_frame_side_pad_px();
1830 let mut x2 = max_cx + sequence_text_overrides::sequence_frame_side_pad_px();
1831 if geom_min_x.is_finite() {
1832 x1 = x1.min(geom_min_x);
1833 }
1834 if geom_max_x.is_finite() {
1835 x2 = x2.max(geom_max_x);
1836 }
1837 Some((x1, x2, min_left))
1838 }
1839
1840 #[derive(Debug)]
1841 enum BlockStackEntry {
1842 Loop { items: Vec<String> },
1843 Opt { items: Vec<String> },
1844 Break { items: Vec<String> },
1845 Alt { sections: Vec<Vec<String>> },
1846 Par { sections: Vec<Vec<String>> },
1847 Critical { sections: Vec<Vec<String>> },
1848 }
1849
1850 let mut block_min_x = f64::INFINITY;
1851 let mut block_min_y = f64::INFINITY;
1852 let mut block_max_x = f64::NEG_INFINITY;
1853 let mut block_max_y = f64::NEG_INFINITY;
1854
1855 let mut stack: Vec<BlockStackEntry> = Vec::new();
1856 for msg in &model.messages {
1857 let msg_id = msg.id.clone();
1858 match msg.message_type {
1859 10 => stack.push(BlockStackEntry::Loop { items: Vec::new() }),
1860 11 => {
1861 if let Some(BlockStackEntry::Loop { items }) = stack.pop() {
1862 if let (Some((x1, x2, _min_left)), Some((y0, y1))) = (
1863 frame_x_from_item_ids(
1864 &items,
1865 &nodes_by_id,
1866 &edges_by_id,
1867 &msg_endpoints,
1868 ),
1869 items
1870 .iter()
1871 .filter_map(|id| {
1872 item_y_range(id, &nodes_by_id, &edges_by_id, &msg_endpoints)
1873 })
1874 .reduce(|a, b| (a.0.min(b.0), a.1.max(b.1))),
1875 ) {
1876 let frame_y1 = y0 - 79.0;
1877 let frame_y2 = y1 + 10.0;
1878 block_min_x = block_min_x.min(x1);
1879 block_max_x = block_max_x.max(x2);
1880 block_min_y = block_min_y.min(frame_y1);
1881 block_max_y = block_max_y.max(frame_y2);
1882 }
1883 }
1884 }
1885 15 => stack.push(BlockStackEntry::Opt { items: Vec::new() }),
1886 16 => {
1887 if let Some(BlockStackEntry::Opt { items }) = stack.pop() {
1888 if let (Some((x1, x2, _min_left)), Some((y0, y1))) = (
1889 frame_x_from_item_ids(
1890 &items,
1891 &nodes_by_id,
1892 &edges_by_id,
1893 &msg_endpoints,
1894 ),
1895 items
1896 .iter()
1897 .filter_map(|id| {
1898 item_y_range(id, &nodes_by_id, &edges_by_id, &msg_endpoints)
1899 })
1900 .reduce(|a, b| (a.0.min(b.0), a.1.max(b.1))),
1901 ) {
1902 let frame_y1 = y0 - 79.0;
1903 let frame_y2 = y1 + 10.0;
1904 block_min_x = block_min_x.min(x1);
1905 block_max_x = block_max_x.max(x2);
1906 block_min_y = block_min_y.min(frame_y1);
1907 block_max_y = block_max_y.max(frame_y2);
1908 }
1909 }
1910 }
1911 30 => stack.push(BlockStackEntry::Break { items: Vec::new() }),
1912 31 => {
1913 if let Some(BlockStackEntry::Break { items }) = stack.pop() {
1914 if let (Some((x1, x2, _min_left)), Some((y0, y1))) = (
1915 frame_x_from_item_ids(
1916 &items,
1917 &nodes_by_id,
1918 &edges_by_id,
1919 &msg_endpoints,
1920 ),
1921 items
1922 .iter()
1923 .filter_map(|id| {
1924 item_y_range(id, &nodes_by_id, &edges_by_id, &msg_endpoints)
1925 })
1926 .reduce(|a, b| (a.0.min(b.0), a.1.max(b.1))),
1927 ) {
1928 let frame_y1 = y0 - 93.0;
1929 let frame_y2 = y1 + 10.0;
1930 block_min_x = block_min_x.min(x1);
1931 block_max_x = block_max_x.max(x2);
1932 block_min_y = block_min_y.min(frame_y1);
1933 block_max_y = block_max_y.max(frame_y2);
1934 }
1935 }
1936 }
1937 12 => stack.push(BlockStackEntry::Alt {
1938 sections: vec![Vec::new()],
1939 }),
1940 13 => {
1941 if let Some(BlockStackEntry::Alt { sections }) = stack.last_mut() {
1942 sections.push(Vec::new());
1943 }
1944 }
1945 14 => {
1946 if let Some(BlockStackEntry::Alt { sections }) = stack.pop() {
1947 let items: Vec<String> = sections.into_iter().flatten().collect();
1948 if let (Some((x1, x2, _min_left)), Some((y0, y1))) = (
1949 frame_x_from_item_ids(
1950 &items,
1951 &nodes_by_id,
1952 &edges_by_id,
1953 &msg_endpoints,
1954 ),
1955 items
1956 .iter()
1957 .filter_map(|id| {
1958 item_y_range(id, &nodes_by_id, &edges_by_id, &msg_endpoints)
1959 })
1960 .reduce(|a, b| (a.0.min(b.0), a.1.max(b.1))),
1961 ) {
1962 let frame_y1 = y0 - 79.0;
1963 let frame_y2 = y1 + 10.0;
1964 block_min_x = block_min_x.min(x1);
1965 block_max_x = block_max_x.max(x2);
1966 block_min_y = block_min_y.min(frame_y1);
1967 block_max_y = block_max_y.max(frame_y2);
1968 }
1969 }
1970 }
1971 19 | 32 => stack.push(BlockStackEntry::Par {
1972 sections: vec![Vec::new()],
1973 }),
1974 20 => {
1975 if let Some(BlockStackEntry::Par { sections }) = stack.last_mut() {
1976 sections.push(Vec::new());
1977 }
1978 }
1979 21 => {
1980 if let Some(BlockStackEntry::Par { sections }) = stack.pop() {
1981 let items: Vec<String> = sections.into_iter().flatten().collect();
1982 if let (Some((x1, x2, _min_left)), Some((y0, y1))) = (
1983 frame_x_from_item_ids(
1984 &items,
1985 &nodes_by_id,
1986 &edges_by_id,
1987 &msg_endpoints,
1988 ),
1989 items
1990 .iter()
1991 .filter_map(|id| {
1992 item_y_range(id, &nodes_by_id, &edges_by_id, &msg_endpoints)
1993 })
1994 .reduce(|a, b| (a.0.min(b.0), a.1.max(b.1))),
1995 ) {
1996 let frame_y1 = y0 - 79.0;
1997 let frame_y2 = y1 + 10.0;
1998 block_min_x = block_min_x.min(x1);
1999 block_max_x = block_max_x.max(x2);
2000 block_min_y = block_min_y.min(frame_y1);
2001 block_max_y = block_max_y.max(frame_y2);
2002 }
2003 }
2004 }
2005 27 => stack.push(BlockStackEntry::Critical {
2006 sections: vec![Vec::new()],
2007 }),
2008 28 => {
2009 if let Some(BlockStackEntry::Critical { sections }) = stack.last_mut() {
2010 sections.push(Vec::new());
2011 }
2012 }
2013 29 => {
2014 if let Some(BlockStackEntry::Critical { sections }) = stack.pop() {
2015 let section_count = sections.len();
2016 let items: Vec<String> = sections.into_iter().flatten().collect();
2017 if let (Some((mut x1, x2, min_left)), Some((y0, y1))) = (
2018 frame_x_from_item_ids(
2019 &items,
2020 &nodes_by_id,
2021 &edges_by_id,
2022 &msg_endpoints,
2023 ),
2024 items
2025 .iter()
2026 .filter_map(|id| {
2027 item_y_range(id, &nodes_by_id, &edges_by_id, &msg_endpoints)
2028 })
2029 .reduce(|a, b| (a.0.min(b.0), a.1.max(b.1))),
2030 ) {
2031 if min_left.is_finite() && !items.is_empty() && section_count > 1 {
2032 x1 = x1.min(min_left - 9.0);
2033 }
2034 let frame_y1 = y0 - 79.0;
2035 let frame_y2 = y1 + 10.0;
2036 block_min_x = block_min_x.min(x1);
2037 block_max_x = block_max_x.max(x2);
2038 block_min_y = block_min_y.min(frame_y1);
2039 block_max_y = block_max_y.max(frame_y2);
2040 }
2041 }
2042 }
2043 2 => {
2044 for entry in stack.iter_mut() {
2045 match entry {
2046 BlockStackEntry::Alt { sections }
2047 | BlockStackEntry::Par { sections }
2048 | BlockStackEntry::Critical { sections } => {
2049 if let Some(cur) = sections.last_mut() {
2050 cur.push(msg_id.clone());
2051 }
2052 }
2053 BlockStackEntry::Loop { items }
2054 | BlockStackEntry::Opt { items }
2055 | BlockStackEntry::Break { items } => {
2056 items.push(msg_id.clone());
2057 }
2058 }
2059 }
2060 }
2061 _ => {
2062 if msg.from.is_some() && msg.to.is_some() {
2063 for entry in stack.iter_mut() {
2064 match entry {
2065 BlockStackEntry::Alt { sections }
2066 | BlockStackEntry::Par { sections }
2067 | BlockStackEntry::Critical { sections } => {
2068 if let Some(cur) = sections.last_mut() {
2069 cur.push(msg_id.clone());
2070 }
2071 }
2072 BlockStackEntry::Loop { items }
2073 | BlockStackEntry::Opt { items }
2074 | BlockStackEntry::Break { items } => {
2075 items.push(msg_id.clone());
2076 }
2077 }
2078 }
2079 }
2080 }
2081 }
2082 }
2083
2084 if block_min_x.is_finite() && block_min_y.is_finite() {
2085 Some((block_min_x, block_min_y, block_max_x, block_max_y))
2086 } else {
2087 None
2088 }
2089 };
2090
2091 let mut content_min_x = f64::INFINITY;
2092 let mut content_max_x = f64::NEG_INFINITY;
2093 let mut content_max_y = f64::NEG_INFINITY;
2094 for n in &nodes {
2095 let left = n.x - n.width / 2.0;
2096 let right = n.x + n.width / 2.0;
2097 let bottom = n.y + n.height / 2.0;
2098 content_min_x = content_min_x.min(left);
2099 content_max_x = content_max_x.max(right);
2100 content_max_y = content_max_y.max(bottom);
2101 }
2102 if !content_min_x.is_finite() {
2103 content_min_x = 0.0;
2104 content_max_x = actor_width_min.max(1.0);
2105 content_max_y = (bottom_box_top_y + actor_height).max(1.0);
2106 }
2107
2108 if let Some((min_x, _min_y, max_x, max_y)) = block_bounds {
2109 content_min_x = content_min_x.min(min_x);
2110 content_max_x = content_max_x.max(max_x);
2111 content_max_y = content_max_y.max(max_y);
2112 }
2113
2114 let extra_vert_for_title = if model.title.is_some() { 40.0 } else { 0.0 };
2117
2118 let vb_min_y = -(diagram_margin_y + extra_vert_for_title);
2121
2122 let mut bounds_box_stopy = (content_max_y + bottom_margin_adj).max(0.0);
2129 if has_boxes {
2130 bounds_box_stopy += box_margin;
2131 }
2132
2133 let mut bounds_box_startx = content_min_x;
2136 let mut bounds_box_stopx = content_max_x;
2137 for i in 0..model.actor_order.len() {
2138 let left = actor_left_x[i];
2139 let right = left + actor_widths[i];
2140 if let Some(bi) = actor_box[i] {
2141 let m = box_margins[bi];
2142 bounds_box_startx = bounds_box_startx.min(left - m);
2143 bounds_box_stopx = bounds_box_stopx.max(right + m);
2144 } else {
2145 bounds_box_startx = bounds_box_startx.min(left);
2146 bounds_box_stopx = bounds_box_stopx.max(right);
2147 }
2148 }
2149
2150 for msg in &model.messages {
2154 let (Some(from), Some(to)) = (msg.from.as_deref(), msg.to.as_deref()) else {
2155 continue;
2156 };
2157 if from != to {
2158 continue;
2159 }
2160 if msg.message_type == 2 {
2162 continue;
2163 }
2164 let Some(&i) = actor_index.get(from) else {
2165 continue;
2166 };
2167 let center_x = actor_centers_x[i] + 1.0;
2168 let text = msg.message.as_str().unwrap_or_default();
2169 let (text_w, _text_h) = if text.is_empty() {
2170 (1.0, 1.0)
2171 } else {
2172 measure_svg_like_with_html_br(measurer, text, &msg_text_style)
2173 };
2174 let dx = (text_w.max(1.0) / 2.0).max(actor_width_min / 2.0);
2175 bounds_box_startx = bounds_box_startx.min(center_x - dx);
2176 bounds_box_stopx = bounds_box_stopx.max(center_x + dx);
2177 }
2178
2179 let bounds = Some(Bounds {
2180 min_x: bounds_box_startx - diagram_margin_x,
2181 min_y: vb_min_y,
2182 max_x: bounds_box_stopx + diagram_margin_x,
2183 max_y: bounds_box_stopy + diagram_margin_y,
2184 });
2185
2186 Ok(SequenceDiagramLayout {
2187 nodes,
2188 edges,
2189 clusters,
2190 bounds,
2191 })
2192}
2193
2194#[cfg(test)]
2195mod tests {
2196 use crate::generated::sequence_text_overrides_11_12_2 as sequence_text_overrides;
2197
2198 #[test]
2199 fn sequence_text_constants_are_generated() {
2200 assert_eq!(sequence_text_overrides::sequence_note_wrap_slack_px(), 12.0);
2201 assert_eq!(
2202 sequence_text_overrides::sequence_text_dimensions_height_px(16.0),
2203 17.0
2204 );
2205 assert_eq!(
2206 sequence_text_overrides::sequence_text_line_step_px(16.0),
2207 19.0
2208 );
2209 assert_eq!(
2210 sequence_text_overrides::sequence_note_text_pad_total_px(),
2211 20.0
2212 );
2213 assert_eq!(
2214 sequence_text_overrides::sequence_self_message_frame_extra_y_px(),
2215 60.0
2216 );
2217 assert_eq!(
2218 sequence_text_overrides::sequence_self_message_separator_extra_y_px(),
2219 30.0
2220 );
2221 assert_eq!(sequence_text_overrides::sequence_frame_side_pad_px(), 11.0);
2222 assert_eq!(sequence_text_overrides::sequence_frame_geom_pad_px(), 10.0);
2223 assert_eq!(
2224 sequence_text_overrides::sequence_self_only_frame_min_pad_left_px(),
2225 5.0
2226 );
2227 assert_eq!(
2228 sequence_text_overrides::sequence_self_only_frame_min_pad_right_px(),
2229 15.0
2230 );
2231 }
2232}