Skip to main content

merman_render/
model.rs

1#[cfg(any(feature = "diagram-class", feature = "diagram-sequence"))]
2use rustc_hash::FxHashMap;
3use serde::{Deserialize, Serialize};
4#[cfg(feature = "diagram-class")]
5use std::sync::Arc;
6
7#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
8pub struct Bounds {
9    pub min_x: f64,
10    pub min_y: f64,
11    pub max_x: f64,
12    pub max_y: f64,
13}
14
15impl Bounds {
16    pub fn from_points(points: impl IntoIterator<Item = (f64, f64)>) -> Option<Self> {
17        let mut it = points.into_iter();
18        let (x0, y0) = it.next()?;
19        let mut b = Self {
20            min_x: x0,
21            min_y: y0,
22            max_x: x0,
23            max_y: y0,
24        };
25        for (x, y) in it {
26            b.min_x = b.min_x.min(x);
27            b.min_y = b.min_y.min(y);
28            b.max_x = b.max_x.max(x);
29            b.max_y = b.max_y.max(y);
30        }
31        Some(b)
32    }
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct LayoutPoint {
37    pub x: f64,
38    pub y: f64,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct LayoutLabel {
43    pub x: f64,
44    pub y: f64,
45    pub width: f64,
46    pub height: f64,
47}
48
49#[derive(Debug, Clone)]
50#[cfg(feature = "diagram-class")]
51pub struct ClassNodeRowMetrics {
52    pub members: Vec<crate::text::TextMetrics>,
53    pub methods: Vec<crate::text::TextMetrics>,
54}
55
56#[derive(Debug, Clone)]
57#[cfg(feature = "diagram-class")]
58pub struct ClassPreparedHtmlLabel {
59    pub metrics: crate::text::TextMetrics,
60    pub max_width_px: i64,
61    pub xhtml: String,
62}
63
64#[derive(Debug, Clone)]
65#[cfg(feature = "diagram-class")]
66pub struct ClassPreparedHtmlNodeLabels {
67    pub title: ClassPreparedHtmlLabel,
68    pub annotation: Option<ClassPreparedHtmlLabel>,
69    pub members: Vec<ClassPreparedHtmlLabel>,
70    pub methods: Vec<ClassPreparedHtmlLabel>,
71}
72
73#[derive(Debug, Clone)]
74#[cfg(feature = "diagram-class")]
75pub enum ClassNodeLabelPlan {
76    RowMetrics(ClassNodeRowMetrics),
77    PreparedHtml(ClassPreparedHtmlNodeLabels),
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct LayoutNode {
82    pub id: String,
83    pub x: f64,
84    pub y: f64,
85    pub width: f64,
86    pub height: f64,
87    pub is_cluster: bool,
88    #[serde(skip)]
89    pub label_width: Option<f64>,
90    #[serde(skip)]
91    pub label_height: Option<f64>,
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct LayoutCluster {
96    pub id: String,
97    pub x: f64,
98    pub y: f64,
99    pub width: f64,
100    pub height: f64,
101    /// Mermaid cluster "diff" value used during cluster positioning.
102    pub diff: f64,
103    /// Mermaid cluster "offsetY" value: title bbox height minus half padding.
104    pub offset_y: f64,
105    pub title: String,
106    pub title_label: LayoutLabel,
107    pub requested_dir: Option<String>,
108    pub effective_dir: String,
109    pub padding: f64,
110    pub title_margin_top: f64,
111    pub title_margin_bottom: f64,
112}
113
114#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct LayoutEdge {
116    pub id: String,
117    pub from: String,
118    pub to: String,
119    #[serde(default)]
120    pub from_cluster: Option<String>,
121    #[serde(default)]
122    pub to_cluster: Option<String>,
123    pub points: Vec<LayoutPoint>,
124    pub label: Option<LayoutLabel>,
125    #[serde(default)]
126    pub start_label_left: Option<LayoutLabel>,
127    #[serde(default)]
128    pub start_label_right: Option<LayoutLabel>,
129    #[serde(default)]
130    pub end_label_left: Option<LayoutLabel>,
131    #[serde(default)]
132    pub end_label_right: Option<LayoutLabel>,
133    /// Optional SVG marker id for marker-start (e.g. ER cardinality markers).
134    #[serde(default)]
135    pub start_marker: Option<String>,
136    /// Optional SVG marker id for marker-end (e.g. ER cardinality markers).
137    #[serde(default)]
138    pub end_marker: Option<String>,
139    /// Optional SVG dash pattern (e.g. identifying vs non-identifying ER relationships).
140    #[serde(default)]
141    pub stroke_dasharray: Option<String>,
142}
143
144/// Post-paint edge geometry, recovered from the same compute phase that emits the SVG.
145///
146/// `LayoutEdge::points` is the ELK port polyline *before* the endpoints are clipped to the
147/// endpoint shapes' ink. Clipping happens during paint, and consumers that need the geometry that
148/// is actually on screen currently have to render a whole SVG and recover it from the
149/// `data-points` attribute, which is why this projection exists.
150///
151/// Coordinates are in the **same space as `LayoutEdge::points`** (layout coordinates), so a
152/// consumer can treat this as a refinement of the layout projection rather than a second,
153/// unrelated geometry system.
154#[derive(Debug, Clone, Serialize)]
155pub struct EdgePaintGeometry {
156    /// Matches `LayoutEdge::id`.
157    pub id: String,
158    /// Edge polyline after clipping to the endpoint shapes' ink, and after any swimlane line
159    /// hops. This is the exact point list emitted as the `data-points` attribute.
160    ///
161    /// `None` when the renderer produced no path for this edge (for example an edge with fewer
162    /// than two layout points).
163    pub points: Option<Vec<LayoutPoint>>,
164    /// Center of the edge label after paint-time positioning, in layout coordinates.
165    ///
166    /// `None` when the edge has no positioned label.
167    pub label_position: Option<LayoutPoint>,
168}
169
170/// Versioned container for [`EdgePaintGeometry`].
171///
172/// Versioned independently of the crate, so consumers can pin a payload shape without pinning a
173/// merman release — the same contract `svg-plan-json` establishes for the capability plan.
174#[derive(Debug, Clone, Serialize)]
175pub struct EdgePaintGeometryOutput {
176    schema_version: u32,
177    diagram_type: String,
178    edges: Vec<EdgePaintGeometry>,
179}
180
181impl EdgePaintGeometryOutput {
182    /// Bumped whenever the payload shape changes incompatibly.
183    pub const SCHEMA_VERSION: u32 = 1;
184
185    pub fn new(diagram_type: String, edges: Vec<EdgePaintGeometry>) -> Self {
186        Self {
187            schema_version: Self::SCHEMA_VERSION,
188            diagram_type,
189            edges,
190        }
191    }
192
193    pub const fn schema_version(&self) -> u32 {
194        self.schema_version
195    }
196
197    pub fn diagram_type(&self) -> &str {
198        &self.diagram_type
199    }
200
201    pub fn edges(&self) -> &[EdgePaintGeometry] {
202        &self.edges
203    }
204}
205
206#[derive(Debug, Clone, Serialize, Deserialize)]
207#[cfg(feature = "diagram-block")]
208pub struct BlockDiagramLayout {
209    pub nodes: Vec<LayoutNode>,
210    pub edges: Vec<LayoutEdge>,
211    #[serde(
212        default,
213        rename = "shapeGeometries",
214        skip_serializing_if = "Vec::is_empty"
215    )]
216    pub shape_geometries: Vec<crate::block::BlockShapeGeometry>,
217    pub bounds: Option<Bounds>,
218}
219
220#[derive(Debug, Clone, Serialize, Deserialize)]
221#[cfg(feature = "diagram-requirement")]
222pub struct RequirementDiagramLayout {
223    pub nodes: Vec<LayoutNode>,
224    pub edges: Vec<LayoutEdge>,
225    pub bounds: Option<Bounds>,
226}
227
228#[derive(Debug, Clone, Serialize, Deserialize)]
229#[cfg(feature = "diagram-architecture")]
230pub struct ArchitectureDiagramLayout {
231    pub nodes: Vec<LayoutNode>,
232    pub edges: Vec<LayoutEdge>,
233    /// Local Cytoscape child contribution phases for Architecture services.
234    ///
235    /// These bounds are exposed for source-backed group/content audits. SVG renderers should use
236    /// their own phase-specific logic instead of treating this as a generic root-bounds source.
237    #[serde(default, skip_serializing_if = "Vec::is_empty")]
238    pub cytoscape_service_bounds: Vec<ArchitectureCytoscapeServiceBounds>,
239    /// Final FCoSE compound rectangles exposed for source-backed Architecture audits.
240    ///
241    /// Renderers should not treat these as Mermaid SVG group rects without a phase-specific
242    /// source audit; they are layout-engine rects, not browser `node.boundingBox()` values.
243    #[serde(default, skip_serializing_if = "Vec::is_empty")]
244    pub fcose_compound_bounds: Vec<ArchitectureCompoundBounds>,
245    pub bounds: Option<Bounds>,
246}
247
248#[derive(Debug, Clone, Serialize, Deserialize)]
249#[cfg(feature = "diagram-architecture")]
250pub struct ArchitectureCompoundBounds {
251    pub id: String,
252    pub bounds: Bounds,
253}
254
255#[derive(Debug, Clone, Serialize, Deserialize)]
256#[cfg(feature = "diagram-architecture")]
257pub struct ArchitectureCytoscapeServiceBounds {
258    pub id: String,
259    #[serde(default, skip_serializing_if = "Option::is_none")]
260    pub in_group: Option<String>,
261    pub body_bounds: Bounds,
262    #[serde(default, skip_serializing_if = "Option::is_none")]
263    pub label_bounds: Option<Bounds>,
264    #[serde(default, skip_serializing_if = "Option::is_none")]
265    pub label_metrics: Option<ArchitectureCytoscapeServiceLabelMetrics>,
266    pub union_bounds: Bounds,
267}
268
269#[derive(Debug, Clone, Serialize, Deserialize)]
270#[cfg(feature = "diagram-architecture")]
271pub struct ArchitectureCytoscapeServiceLabelMetrics {
272    pub text_width: f64,
273    pub half_width: f64,
274}
275
276#[derive(Debug, Clone, Serialize, Deserialize)]
277#[cfg(feature = "diagram-mindmap")]
278pub struct MindmapDiagramLayout {
279    pub nodes: Vec<LayoutNode>,
280    pub edges: Vec<LayoutEdge>,
281    pub bounds: Option<Bounds>,
282    /// Swimlane decoration owned by the selected renderer, not semantic Mindmap nodes.
283    #[serde(skip)]
284    pub swimlane_lanes: Vec<LayoutCluster>,
285}
286
287#[derive(Debug, Clone, Serialize, Deserialize)]
288#[cfg(feature = "diagram-sankey")]
289pub struct SankeyNodeLayout {
290    pub id: String,
291    pub index: usize,
292    pub depth: usize,
293    pub height: usize,
294    pub layer: usize,
295    pub value: f64,
296    pub x0: f64,
297    pub x1: f64,
298    pub y0: f64,
299    pub y1: f64,
300}
301
302#[derive(Debug, Clone, Serialize, Deserialize)]
303#[cfg(feature = "diagram-sankey")]
304pub struct SankeyLinkLayout {
305    pub index: usize,
306    pub source: String,
307    pub target: String,
308    pub value: f64,
309    pub width: f64,
310    pub y0: f64,
311    pub y1: f64,
312}
313
314#[derive(Debug, Clone, Serialize, Deserialize)]
315#[cfg(feature = "diagram-sankey")]
316pub struct SankeyDiagramLayout {
317    pub bounds: Option<Bounds>,
318    pub width: f64,
319    pub height: f64,
320    pub node_width: f64,
321    pub node_padding: f64,
322    pub nodes: Vec<SankeyNodeLayout>,
323    pub links: Vec<SankeyLinkLayout>,
324}
325
326#[derive(Debug, Clone, Serialize, Deserialize)]
327#[cfg(feature = "diagram-radar")]
328pub struct RadarAxisLayout {
329    pub label: String,
330    pub angle: f64,
331    pub line_x2: f64,
332    pub line_y2: f64,
333    pub label_x: f64,
334    pub label_y: f64,
335}
336
337#[derive(Debug, Clone, Serialize, Deserialize)]
338#[cfg(feature = "diagram-radar")]
339pub struct RadarGraticuleShapeLayout {
340    pub kind: String,
341    pub r: Option<f64>,
342    #[serde(default)]
343    pub points: Vec<LayoutPoint>,
344}
345
346#[derive(Debug, Clone, Serialize, Deserialize)]
347#[cfg(feature = "diagram-radar")]
348pub struct RadarCurveLayout {
349    pub label: String,
350    pub class_index: i64,
351    #[serde(default)]
352    pub points: Vec<LayoutPoint>,
353    pub path_d: String,
354}
355
356#[derive(Debug, Clone, Serialize, Deserialize)]
357#[cfg(feature = "diagram-radar")]
358pub struct RadarLegendItemLayout {
359    pub label: String,
360    pub class_index: i64,
361    pub x: f64,
362    pub y: f64,
363}
364
365#[derive(Debug, Clone, Serialize, Deserialize)]
366#[cfg(feature = "diagram-radar")]
367pub struct RadarDiagramLayout {
368    pub bounds: Option<Bounds>,
369    pub svg_width: f64,
370    pub svg_height: f64,
371    pub center_x: f64,
372    pub center_y: f64,
373    pub radius: f64,
374    pub axis_label_factor: f64,
375    pub title_y: f64,
376    #[serde(default)]
377    pub axes: Vec<RadarAxisLayout>,
378    #[serde(default)]
379    pub graticules: Vec<RadarGraticuleShapeLayout>,
380    #[serde(default)]
381    pub curves: Vec<RadarCurveLayout>,
382    #[serde(default)]
383    pub legend_items: Vec<RadarLegendItemLayout>,
384}
385
386#[derive(Debug, Clone, Serialize, Deserialize)]
387#[cfg(feature = "diagram-treemap")]
388pub struct TreemapSectionLayout {
389    pub name: String,
390    pub depth: i64,
391    pub value: f64,
392    pub x0: f64,
393    pub y0: f64,
394    pub x1: f64,
395    pub y1: f64,
396    #[serde(default)]
397    pub class_selector: Option<String>,
398    #[serde(default)]
399    pub css_compiled_styles: Option<Vec<String>>,
400}
401
402#[derive(Debug, Clone, Serialize, Deserialize)]
403#[cfg(feature = "diagram-treemap")]
404pub struct TreemapLeafLayout {
405    pub name: String,
406    pub value: f64,
407    #[serde(default)]
408    pub parent_name: Option<String>,
409    pub x0: f64,
410    pub y0: f64,
411    pub x1: f64,
412    pub y1: f64,
413    #[serde(default)]
414    pub class_selector: Option<String>,
415    #[serde(default)]
416    pub css_compiled_styles: Option<Vec<String>>,
417}
418
419#[derive(Debug, Clone, Serialize, Deserialize)]
420#[cfg(feature = "diagram-treemap")]
421pub struct TreemapDiagramLayout {
422    pub title_height: f64,
423    pub width: f64,
424    pub height: f64,
425    pub use_max_width: bool,
426    pub diagram_padding: f64,
427    pub show_values: bool,
428    pub value_format: String,
429    #[serde(default, rename = "accTitle")]
430    pub acc_title: Option<String>,
431    #[serde(default, rename = "accDescr")]
432    pub acc_descr: Option<String>,
433    #[serde(default)]
434    pub title: Option<String>,
435    #[serde(default)]
436    pub sections: Vec<TreemapSectionLayout>,
437    #[serde(default)]
438    pub leaves: Vec<TreemapLeafLayout>,
439}
440
441#[derive(Debug, Clone, Serialize, Deserialize)]
442#[cfg(feature = "diagram-venn")]
443pub struct VennCircleLayout {
444    pub set: String,
445    pub x: f64,
446    pub y: f64,
447    pub radius: f64,
448}
449
450#[derive(Debug, Clone, Serialize, Deserialize)]
451#[cfg(feature = "diagram-venn")]
452pub struct VennAreaLayout {
453    pub sets: Vec<String>,
454    pub size: f64,
455    #[serde(default, skip_serializing_if = "Option::is_none")]
456    pub label: Option<String>,
457    pub text_x: f64,
458    pub text_y: f64,
459    pub text_disjoint: bool,
460    #[serde(default)]
461    pub circles: Vec<VennCircleLayout>,
462    pub path: String,
463}
464
465#[derive(Debug, Clone, Serialize, Deserialize)]
466#[cfg(feature = "diagram-venn")]
467pub struct VennTextDebugCellLayout {
468    pub x: f64,
469    pub y: f64,
470    pub width: f64,
471    pub height: f64,
472}
473
474#[derive(Debug, Clone, Serialize, Deserialize)]
475#[cfg(feature = "diagram-venn")]
476pub struct VennTextAreaLayout {
477    pub sets: Vec<String>,
478    pub center_x: f64,
479    pub center_y: f64,
480    pub inner_radius: f64,
481    pub font_size: f64,
482    #[serde(default)]
483    pub debug_cells: Vec<VennTextDebugCellLayout>,
484}
485
486#[derive(Debug, Clone, Serialize, Deserialize)]
487#[cfg(feature = "diagram-venn")]
488pub struct VennTextNodeLayout {
489    pub sets: Vec<String>,
490    pub id: String,
491    #[serde(default, skip_serializing_if = "Option::is_none")]
492    pub label: Option<String>,
493    pub x: f64,
494    pub y: f64,
495    pub width: f64,
496    pub height: f64,
497}
498
499#[derive(Debug, Clone, Serialize, Deserialize)]
500#[cfg(feature = "diagram-venn")]
501pub struct VennDiagramLayout {
502    pub bounds: Option<Bounds>,
503    pub width: f64,
504    pub height: f64,
505    pub diagram_height: f64,
506    pub title_height: f64,
507    pub scale: f64,
508    pub padding: f64,
509    pub use_max_width: bool,
510    pub use_debug_layout: bool,
511    #[serde(default)]
512    pub areas: Vec<VennAreaLayout>,
513    #[serde(default)]
514    pub text_areas: Vec<VennTextAreaLayout>,
515    #[serde(default)]
516    pub text_nodes: Vec<VennTextNodeLayout>,
517}
518
519#[derive(Debug, Clone, Serialize, Deserialize)]
520#[cfg(feature = "diagram-xychart")]
521pub struct XyChartRectData {
522    pub x: f64,
523    pub y: f64,
524    pub width: f64,
525    pub height: f64,
526    pub fill: String,
527    #[serde(rename = "strokeFill")]
528    pub stroke_fill: String,
529    #[serde(rename = "strokeWidth")]
530    pub stroke_width: f64,
531}
532
533#[derive(Debug, Clone, Serialize, Deserialize)]
534#[cfg(feature = "diagram-xychart")]
535pub struct XyChartTextData {
536    pub text: String,
537    pub x: f64,
538    pub y: f64,
539    pub fill: String,
540    #[serde(rename = "fontSize")]
541    pub font_size: f64,
542    #[serde(default)]
543    pub rotation: f64,
544    #[serde(rename = "verticalPos")]
545    pub vertical_pos: String,
546    #[serde(rename = "horizontalPos")]
547    pub horizontal_pos: String,
548}
549
550#[derive(Debug, Clone, Serialize, Deserialize)]
551#[cfg(feature = "diagram-xychart")]
552pub struct XyChartPathData {
553    pub path: String,
554    #[serde(default)]
555    pub fill: Option<String>,
556    #[serde(rename = "strokeFill")]
557    pub stroke_fill: String,
558    #[serde(rename = "strokeWidth")]
559    pub stroke_width: f64,
560}
561
562#[derive(Debug, Clone, Serialize, Deserialize)]
563#[serde(tag = "type")]
564#[cfg(feature = "diagram-xychart")]
565pub enum XyChartDrawableElem {
566    #[serde(rename = "rect")]
567    Rect {
568        #[serde(rename = "groupTexts")]
569        group_texts: Vec<String>,
570        data: Vec<XyChartRectData>,
571    },
572    #[serde(rename = "text")]
573    Text {
574        #[serde(rename = "groupTexts")]
575        group_texts: Vec<String>,
576        data: Vec<XyChartTextData>,
577    },
578    #[serde(rename = "path")]
579    Path {
580        #[serde(rename = "groupTexts")]
581        group_texts: Vec<String>,
582        data: Vec<XyChartPathData>,
583    },
584}
585
586#[derive(Debug, Clone, Serialize, Deserialize)]
587#[cfg(feature = "diagram-xychart")]
588pub struct XyChartDiagramLayout {
589    pub width: f64,
590    pub height: f64,
591    #[serde(rename = "chartOrientation")]
592    pub chart_orientation: String,
593    #[serde(rename = "showDataLabel")]
594    pub show_data_label: bool,
595    #[serde(rename = "showDataLabelOutsideBar", default)]
596    pub show_data_label_outside_bar: bool,
597    #[serde(rename = "backgroundColor")]
598    pub background_color: String,
599    #[serde(rename = "labelData")]
600    pub label_data: Vec<String>,
601    #[serde(default)]
602    pub drawables: Vec<XyChartDrawableElem>,
603}
604
605#[derive(Debug, Clone, Serialize, Deserialize)]
606#[cfg(feature = "diagram-quadrant-chart")]
607pub struct QuadrantChartTextData {
608    pub text: String,
609    pub x: f64,
610    pub y: f64,
611    pub fill: String,
612    #[serde(rename = "fontSize")]
613    pub font_size: f64,
614    #[serde(default)]
615    pub rotation: f64,
616    #[serde(rename = "verticalPos")]
617    pub vertical_pos: String,
618    #[serde(rename = "horizontalPos")]
619    pub horizontal_pos: String,
620}
621
622#[cfg(feature = "diagram-quadrant-chart")]
623pub type QuadrantChartAxisLabelData = QuadrantChartTextData;
624
625#[derive(Debug, Clone, Serialize, Deserialize)]
626#[cfg(feature = "diagram-quadrant-chart")]
627pub struct QuadrantChartQuadrantData {
628    pub x: f64,
629    pub y: f64,
630    pub width: f64,
631    pub height: f64,
632    pub fill: String,
633    pub text: QuadrantChartTextData,
634}
635
636#[derive(Debug, Clone, Serialize, Deserialize)]
637#[cfg(feature = "diagram-quadrant-chart")]
638pub struct QuadrantChartBorderLineData {
639    #[serde(rename = "strokeWidth")]
640    pub stroke_width: f64,
641    #[serde(rename = "strokeFill")]
642    pub stroke_fill: String,
643    pub x1: f64,
644    pub y1: f64,
645    pub x2: f64,
646    pub y2: f64,
647}
648
649#[derive(Debug, Clone, Serialize, Deserialize)]
650#[cfg(feature = "diagram-quadrant-chart")]
651pub struct QuadrantChartPointData {
652    pub x: f64,
653    pub y: f64,
654    pub fill: String,
655    pub radius: f64,
656    #[serde(rename = "strokeColor")]
657    pub stroke_color: String,
658    #[serde(rename = "strokeWidth")]
659    pub stroke_width: String,
660    pub text: QuadrantChartTextData,
661}
662
663#[derive(Debug, Clone, Serialize, Deserialize)]
664#[cfg(feature = "diagram-quadrant-chart")]
665pub struct QuadrantChartDiagramLayout {
666    pub width: f64,
667    pub height: f64,
668    #[serde(default)]
669    pub title: Option<QuadrantChartTextData>,
670    pub quadrants: Vec<QuadrantChartQuadrantData>,
671    #[serde(rename = "borderLines")]
672    pub border_lines: Vec<QuadrantChartBorderLineData>,
673    pub points: Vec<QuadrantChartPointData>,
674    #[serde(rename = "axisLabels")]
675    pub axis_labels: Vec<QuadrantChartAxisLabelData>,
676}
677
678#[derive(Debug, Clone, Serialize, Deserialize)]
679#[cfg(any(
680    feature = "diagram-flowchart",
681    feature = "diagram-swimlane",
682    feature = "diagram-agentflow"
683))]
684pub struct FlowchartLayout {
685    pub nodes: Vec<LayoutNode>,
686    pub edges: Vec<LayoutEdge>,
687    pub clusters: Vec<LayoutCluster>,
688    pub bounds: Option<Bounds>,
689    /// Mermaid's DOM insertion order for each extracted root graph (`""` = top-level root).
690    #[serde(skip)]
691    pub dom_node_order_by_root: std::collections::HashMap<String, Vec<String>>,
692    /// Whether SVG emission follows Mermaid's ELK adapter DOM structure.
693    #[serde(skip)]
694    pub uses_elk_adapter_dom: bool,
695}
696
697#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
698#[cfg(any(
699    feature = "diagram-flowchart",
700    feature = "diagram-swimlane",
701    feature = "diagram-agentflow",
702    feature = "diagram-mindmap"
703))]
704pub enum SwimlaneDirection {
705    #[serde(rename = "TB")]
706    Tb,
707    #[serde(rename = "LR")]
708    Lr,
709    #[serde(rename = "BT")]
710    Bt,
711    #[serde(rename = "RL")]
712    Rl,
713}
714
715#[cfg(any(
716    feature = "diagram-flowchart",
717    feature = "diagram-swimlane",
718    feature = "diagram-agentflow",
719    feature = "diagram-mindmap"
720))]
721impl SwimlaneDirection {
722    pub const fn as_str(self) -> &'static str {
723        match self {
724            Self::Tb => "TB",
725            Self::Lr => "LR",
726            Self::Bt => "BT",
727            Self::Rl => "RL",
728        }
729    }
730}
731
732#[derive(Debug, Clone, Serialize, Deserialize)]
733#[cfg(any(
734    feature = "diagram-flowchart",
735    feature = "diagram-swimlane",
736    feature = "diagram-agentflow",
737    feature = "diagram-mindmap"
738))]
739pub struct SwimlaneTitleRect {
740    pub left: f64,
741    pub right: f64,
742    pub top: f64,
743    pub bottom: f64,
744}
745
746#[derive(Debug, Clone, Serialize, Deserialize)]
747#[cfg(any(
748    feature = "diagram-flowchart",
749    feature = "diagram-swimlane",
750    feature = "diagram-agentflow",
751    feature = "diagram-mindmap"
752))]
753pub struct SwimlaneNodeLayout {
754    pub id: String,
755    pub label: String,
756    pub label_type: String,
757    pub shape: String,
758    #[serde(default)]
759    pub parent_id: Option<String>,
760    #[serde(default)]
761    pub top_lane_id: Option<String>,
762    pub x: f64,
763    pub y: f64,
764    pub width: f64,
765    pub height: f64,
766    pub label_width: f64,
767    pub label_height: f64,
768    pub layer: usize,
769    pub order: usize,
770    pub is_edge_label: bool,
771}
772
773#[derive(Debug, Clone, Serialize, Deserialize)]
774#[cfg(any(
775    feature = "diagram-flowchart",
776    feature = "diagram-swimlane",
777    feature = "diagram-agentflow",
778    feature = "diagram-mindmap"
779))]
780pub struct SwimlaneLaneLayout {
781    pub id: String,
782    pub title: String,
783    #[serde(default)]
784    pub parent_id: Option<String>,
785    pub x: f64,
786    pub y: f64,
787    pub width: f64,
788    pub height: f64,
789    pub padding: f64,
790    /// Layout-phase label width. SVG rendering must remeasure the emitted title with its
791    /// `SvgBBox` measurer rather than treating this as a browser `<text>.getBBox()` fact.
792    pub title_label_width: f64,
793    /// Layout-phase label height; see `title_label_width` for the phase boundary.
794    pub title_label_height: f64,
795    #[serde(default)]
796    pub content_top: Option<f64>,
797    #[serde(default)]
798    pub title_rect: Option<SwimlaneTitleRect>,
799    #[serde(default)]
800    pub requested_dir: Option<String>,
801}
802
803#[derive(Debug, Clone, Serialize, Deserialize)]
804#[cfg(any(
805    feature = "diagram-flowchart",
806    feature = "diagram-swimlane",
807    feature = "diagram-agentflow",
808    feature = "diagram-mindmap"
809))]
810pub struct SwimlaneEdgeLayout {
811    pub id: String,
812    pub from: String,
813    pub to: String,
814    pub points: Vec<LayoutPoint>,
815    #[serde(default)]
816    pub label_node_id: Option<String>,
817    pub reversed_for_layout: bool,
818    pub curve: String,
819}
820
821#[derive(Debug, Clone, Serialize, Deserialize)]
822#[cfg(any(
823    feature = "diagram-flowchart",
824    feature = "diagram-swimlane",
825    feature = "diagram-agentflow",
826    feature = "diagram-mindmap"
827))]
828pub struct SwimlaneLayout {
829    pub direction: SwimlaneDirection,
830    pub nodes: Vec<SwimlaneNodeLayout>,
831    pub lanes: Vec<SwimlaneLaneLayout>,
832    pub edges: Vec<SwimlaneEdgeLayout>,
833    pub bounds: Option<Bounds>,
834}
835
836#[derive(Debug, Clone, Serialize, Deserialize)]
837#[cfg(feature = "diagram-state")]
838pub struct StateDiagramLayout {
839    pub nodes: Vec<LayoutNode>,
840    pub edges: Vec<LayoutEdge>,
841    pub clusters: Vec<LayoutCluster>,
842    pub bounds: Option<Bounds>,
843    /// Registered ELK layout uses its own edge painting projection.
844    #[serde(skip)]
845    pub uses_elk_adapter_dom: bool,
846    #[serde(skip)]
847    pub elk_edge_paths: std::collections::HashMap<String, Vec<LayoutPoint>>,
848}
849
850#[derive(Debug, Clone, Serialize, Deserialize)]
851#[cfg(feature = "diagram-class")]
852pub struct ClassDiagramLayout {
853    pub nodes: Vec<LayoutNode>,
854    pub edges: Vec<LayoutEdge>,
855    pub clusters: Vec<LayoutCluster>,
856    pub bounds: Option<Bounds>,
857    /// Whether SVG emission follows Mermaid's registered ELK renderer DOM structure.
858    #[serde(skip)]
859    pub uses_elk_adapter_dom: bool,
860    #[serde(skip)]
861    pub class_label_plans_by_id: FxHashMap<String, Arc<ClassNodeLabelPlan>>,
862    /// DOM ownership produced by the same recursive Dagre preparation that owns layout.
863    ///
864    /// Compatibility layout JSON remains flat, while SVG rendering consumes this typed tree so it
865    /// never has to reconstruct extracted namespace roots from the semantic model.
866    #[serde(skip)]
867    pub render_tree: ClassRenderTree,
868}
869
870#[derive(Debug, Clone, Default)]
871#[cfg(feature = "diagram-class")]
872pub struct ClassRenderRoot {
873    pub namespace_id: Option<String>,
874    pub cluster_ids: Vec<String>,
875    pub edge_ids: Vec<String>,
876    pub items: Vec<ClassRenderItem>,
877}
878
879#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
880#[cfg(feature = "diagram-class")]
881pub struct ClassRenderRootId(pub usize);
882
883#[derive(Debug, Clone)]
884#[cfg(feature = "diagram-class")]
885pub struct ClassRenderTree {
886    pub roots: Vec<ClassRenderRoot>,
887    pub top: ClassRenderRootId,
888}
889
890#[cfg(feature = "diagram-class")]
891impl Default for ClassRenderTree {
892    fn default() -> Self {
893        Self {
894            roots: vec![ClassRenderRoot::default()],
895            top: ClassRenderRootId(0),
896        }
897    }
898}
899
900#[derive(Debug, Clone)]
901#[cfg(feature = "diagram-class")]
902pub enum ClassRenderItem {
903    Node(String),
904    Subgraph(ClassRenderRootId),
905}
906
907#[derive(Debug, Clone, Serialize, Deserialize)]
908#[cfg(feature = "diagram-er")]
909pub struct ErDiagramLayout {
910    pub nodes: Vec<LayoutNode>,
911    pub edges: Vec<LayoutEdge>,
912    /// SVG-facing edge projection.
913    ///
914    /// Dagre needs self-loop helper nodes and three internal edge segments to produce stable
915    /// ranks. Mermaid's common painter merges those segments back into one visible edge. Keep
916    /// that projection beside the canonical layout without serializing it into layout artifacts.
917    #[serde(default, skip)]
918    pub(crate) render_edges: Vec<LayoutEdge>,
919    #[serde(default, skip_serializing_if = "Vec::is_empty")]
920    pub clusters: Vec<LayoutCluster>,
921    pub bounds: Option<Bounds>,
922}
923
924#[derive(Debug, Clone)]
925#[cfg(feature = "diagram-sequence")]
926pub struct SequenceBlockLayout {
927    pub start_y: f64,
928    pub stop_y: f64,
929    /// Mermaid expands every active sequence frame by its nesting depth.
930    /// These coordinates are captured during layout replay rather than inferred from SVG nodes.
931    pub start_x: Option<f64>,
932    pub stop_x: Option<f64>,
933    pub section_ys_by_id: FxHashMap<String, f64>,
934}
935
936#[derive(Debug, Clone, Serialize, Deserialize)]
937#[cfg(feature = "diagram-sequence")]
938pub struct SequenceDiagramLayout {
939    pub nodes: Vec<LayoutNode>,
940    pub edges: Vec<LayoutEdge>,
941    pub clusters: Vec<LayoutCluster>,
942    pub bounds: Option<Bounds>,
943    /// Canonical vertical geometry captured while replaying Mermaid's sequence layout cursor.
944    ///
945    /// This remains outside compatibility layout JSON because it is renderer-owned phase data.
946    #[serde(skip)]
947    pub block_layouts_by_id: FxHashMap<String, SequenceBlockLayout>,
948}
949
950#[derive(Debug, Clone, Serialize, Deserialize)]
951#[cfg(feature = "diagram-info")]
952pub struct InfoDiagramLayout {
953    pub bounds: Option<Bounds>,
954    pub version: String,
955}
956
957#[derive(Debug, Clone, Serialize, Deserialize)]
958#[cfg(feature = "diagram-packet")]
959pub struct PacketBlockLayout {
960    pub start: i64,
961    pub end: i64,
962    pub label: String,
963    pub x: f64,
964    pub y: f64,
965    pub width: f64,
966    pub height: f64,
967}
968
969#[derive(Debug, Clone, Serialize, Deserialize)]
970#[cfg(feature = "diagram-packet")]
971pub struct PacketWordLayout {
972    pub blocks: Vec<PacketBlockLayout>,
973}
974
975#[derive(Debug, Clone, Serialize, Deserialize)]
976#[cfg(feature = "diagram-packet")]
977pub struct PacketDiagramLayout {
978    pub bounds: Option<Bounds>,
979    pub width: f64,
980    pub height: f64,
981    pub row_height: f64,
982    pub padding_x: f64,
983    pub padding_y: f64,
984    pub bit_width: f64,
985    pub bits_per_row: i64,
986    pub show_bits: bool,
987    pub words: Vec<PacketWordLayout>,
988}
989
990#[derive(Debug, Clone, Serialize, Deserialize)]
991#[cfg(feature = "diagram-pie")]
992pub struct PieSliceLayout {
993    pub label: String,
994    pub value: f64,
995    pub start_angle: f64,
996    pub end_angle: f64,
997    pub is_full_circle: bool,
998    pub percent: i64,
999    pub text_x: f64,
1000    pub text_y: f64,
1001    pub fill: String,
1002}
1003
1004#[derive(Debug, Clone, Serialize, Deserialize)]
1005#[cfg(feature = "diagram-pie")]
1006pub struct PieLegendItemLayout {
1007    pub label: String,
1008    pub value: f64,
1009    pub fill: String,
1010    pub y: f64,
1011}
1012
1013#[derive(Debug, Clone, Serialize, Deserialize)]
1014#[cfg(feature = "diagram-pie")]
1015pub struct PieDiagramLayout {
1016    pub bounds: Option<Bounds>,
1017    #[serde(default)]
1018    pub title: Option<String>,
1019    pub center_x: f64,
1020    pub center_y: f64,
1021    pub radius: f64,
1022    pub outer_radius: f64,
1023    pub legend_x: f64,
1024    pub legend_start_y: f64,
1025    pub legend_step_y: f64,
1026    pub slices: Vec<PieSliceLayout>,
1027    pub legend_items: Vec<PieLegendItemLayout>,
1028}
1029
1030#[derive(Debug, Clone, Serialize, Deserialize)]
1031#[cfg(feature = "diagram-timeline")]
1032pub struct TimelineNodeLayout {
1033    pub x: f64,
1034    pub y: f64,
1035    pub width: f64,
1036    pub height: f64,
1037    /// Width used for wrapping (excluding padding).
1038    pub content_width: f64,
1039    /// Padding used to compute `width` (Mermaid 11.12.2 uses 20).
1040    pub padding: f64,
1041    pub section_class: String,
1042    pub label: String,
1043    /// Wrapped lines as rendered into `<tspan>` nodes.
1044    pub label_lines: Vec<String>,
1045    pub kind: String,
1046}
1047
1048#[derive(Debug, Clone, Serialize, Deserialize)]
1049#[cfg(feature = "diagram-timeline")]
1050pub struct TimelineLineLayout {
1051    pub kind: String,
1052    pub x1: f64,
1053    pub y1: f64,
1054    pub x2: f64,
1055    pub y2: f64,
1056}
1057
1058#[derive(Debug, Clone, Serialize, Deserialize)]
1059#[cfg(feature = "diagram-timeline")]
1060pub struct TimelineTaskLayout {
1061    pub node: TimelineNodeLayout,
1062    #[serde(default)]
1063    pub connectors: Vec<TimelineLineLayout>,
1064    pub events: Vec<TimelineNodeLayout>,
1065}
1066
1067#[derive(Debug, Clone, Serialize, Deserialize)]
1068#[cfg(feature = "diagram-timeline")]
1069pub struct TimelineSectionLayout {
1070    pub node: TimelineNodeLayout,
1071    pub tasks: Vec<TimelineTaskLayout>,
1072}
1073
1074#[derive(Debug, Clone, Serialize, Deserialize)]
1075#[cfg(feature = "diagram-timeline")]
1076pub struct TimelineDiagramLayout {
1077    pub direction: merman_core::diagrams::timeline::TimelineDirection,
1078    pub bounds: Option<Bounds>,
1079    pub left_margin: f64,
1080    pub base_x: f64,
1081    pub base_y: f64,
1082    /// `svg.node().getBBox().width` computed *before* title/activity line are inserted.
1083    pub pre_title_box_width: f64,
1084    pub sections: Vec<TimelineSectionLayout>,
1085    #[serde(default)]
1086    pub orphan_tasks: Vec<TimelineTaskLayout>,
1087    pub activity_line: TimelineLineLayout,
1088    pub title: Option<String>,
1089    pub title_x: f64,
1090    pub title_y: f64,
1091    #[serde(default = "crate::timeline::default_use_max_width")]
1092    pub use_max_width: bool,
1093}
1094
1095#[derive(Debug, Clone, Serialize, Deserialize)]
1096#[cfg(feature = "diagram-journey")]
1097pub struct JourneyActorLegendLineLayout {
1098    pub text: String,
1099    pub x: f64,
1100    pub y: f64,
1101    pub tspan_x: f64,
1102    pub text_margin: f64,
1103}
1104
1105#[derive(Debug, Clone, Serialize, Deserialize)]
1106#[cfg(feature = "diagram-journey")]
1107pub struct JourneyActorLegendItemLayout {
1108    pub actor: String,
1109    pub pos: i64,
1110    pub color: String,
1111    pub circle_cx: f64,
1112    pub circle_cy: f64,
1113    pub circle_r: f64,
1114    pub label_lines: Vec<JourneyActorLegendLineLayout>,
1115}
1116
1117#[derive(Debug, Clone, Serialize, Deserialize)]
1118#[cfg(feature = "diagram-journey")]
1119pub enum JourneyMouthKind {
1120    Smile,
1121    Sad,
1122    Ambivalent,
1123}
1124
1125#[derive(Debug, Clone, Serialize, Deserialize)]
1126#[cfg(feature = "diagram-journey")]
1127pub struct JourneyTaskActorCircleLayout {
1128    pub actor: String,
1129    pub pos: i64,
1130    pub color: String,
1131    pub cx: f64,
1132    pub cy: f64,
1133    pub r: f64,
1134}
1135
1136#[derive(Debug, Clone, Serialize, Deserialize)]
1137#[cfg(feature = "diagram-journey")]
1138pub struct JourneyTaskLayout {
1139    pub index: i64,
1140    pub section: String,
1141    pub task: String,
1142    pub score: i64,
1143    pub x: f64,
1144    pub y: f64,
1145    pub width: f64,
1146    pub height: f64,
1147    pub fill: String,
1148    pub num: i64,
1149    pub people: Vec<String>,
1150    pub actor_circles: Vec<JourneyTaskActorCircleLayout>,
1151    pub line_id: String,
1152    pub line_x1: f64,
1153    pub line_y1: f64,
1154    pub line_x2: f64,
1155    pub line_y2: f64,
1156    pub face_cx: f64,
1157    pub face_cy: Option<f64>,
1158    pub mouth: JourneyMouthKind,
1159}
1160
1161#[derive(Debug, Clone, Serialize, Deserialize)]
1162#[cfg(feature = "diagram-journey")]
1163pub struct JourneySectionLayout {
1164    pub section: String,
1165    pub num: i64,
1166    pub x: f64,
1167    pub y: f64,
1168    pub width: f64,
1169    pub height: f64,
1170    pub fill: String,
1171    pub task_count: i64,
1172}
1173
1174#[derive(Debug, Clone, Serialize, Deserialize)]
1175#[cfg(feature = "diagram-journey")]
1176pub struct JourneyLineLayout {
1177    pub x1: f64,
1178    pub y1: f64,
1179    pub x2: f64,
1180    pub y2: f64,
1181}
1182
1183#[derive(Debug, Clone, Serialize, Deserialize)]
1184#[cfg(feature = "diagram-journey")]
1185pub struct JourneyDiagramLayout {
1186    pub bounds: Option<Bounds>,
1187    pub left_margin: f64,
1188    pub max_actor_label_width: f64,
1189    pub width: f64,
1190    pub height: f64,
1191    pub svg_height: f64,
1192    #[serde(default = "crate::journey::default_use_max_width")]
1193    pub use_max_width: bool,
1194    pub title: Option<String>,
1195    pub title_x: f64,
1196    pub title_y: f64,
1197    pub actor_legend: Vec<JourneyActorLegendItemLayout>,
1198    pub sections: Vec<JourneySectionLayout>,
1199    pub tasks: Vec<JourneyTaskLayout>,
1200    pub activity_line: JourneyLineLayout,
1201}
1202
1203#[derive(Debug, Clone, Serialize, Deserialize)]
1204#[cfg(feature = "diagram-kanban")]
1205pub struct KanbanSectionLayout {
1206    pub id: String,
1207    pub label: String,
1208    pub index: i64,
1209    pub center_x: f64,
1210    pub center_y: f64,
1211    pub width: f64,
1212    pub rect_y: f64,
1213    pub rect_height: f64,
1214    pub rx: f64,
1215    pub ry: f64,
1216    pub label_width: f64,
1217    #[serde(skip)]
1218    pub label_height: f64,
1219}
1220
1221#[derive(Debug, Clone, Serialize, Deserialize)]
1222#[cfg(feature = "diagram-kanban")]
1223pub struct KanbanItemLayout {
1224    pub id: String,
1225    pub label: String,
1226    pub parent_id: String,
1227    pub center_x: f64,
1228    pub center_y: f64,
1229    pub width: f64,
1230    pub height: f64,
1231    pub rx: f64,
1232    pub ry: f64,
1233    #[serde(default)]
1234    pub ticket: Option<String>,
1235    #[serde(default)]
1236    pub assigned: Option<String>,
1237    #[serde(default)]
1238    pub priority: Option<String>,
1239    #[serde(default)]
1240    pub icon: Option<String>,
1241}
1242
1243#[derive(Debug, Clone, Serialize, Deserialize)]
1244#[cfg(feature = "diagram-kanban")]
1245pub struct KanbanDiagramLayout {
1246    pub bounds: Option<Bounds>,
1247    pub section_width: f64,
1248    pub padding: f64,
1249    pub max_label_height: f64,
1250    pub viewbox_padding: f64,
1251    #[serde(default = "crate::kanban::default_use_max_width")]
1252    pub use_max_width: bool,
1253    pub sections: Vec<KanbanSectionLayout>,
1254    pub items: Vec<KanbanItemLayout>,
1255}
1256
1257#[derive(Debug, Clone, Serialize, Deserialize)]
1258#[cfg(feature = "diagram-git-graph")]
1259pub struct GitGraphBranchLayout {
1260    pub name: String,
1261    pub index: i64,
1262    pub pos: f64,
1263    pub bbox_width: f64,
1264    pub bbox_height: f64,
1265}
1266
1267#[derive(Debug, Clone, Serialize, Deserialize)]
1268#[cfg(feature = "diagram-git-graph")]
1269pub struct GitGraphCommitLayout {
1270    pub id: String,
1271    pub message: String,
1272    pub seq: i64,
1273    pub commit_type: i64,
1274    #[serde(default)]
1275    pub custom_type: Option<i64>,
1276    #[serde(default)]
1277    pub custom_id: Option<bool>,
1278    #[serde(default)]
1279    pub tags: Vec<String>,
1280    #[serde(default)]
1281    pub parents: Vec<String>,
1282    pub branch: String,
1283    pub pos: f64,
1284    pub pos_with_offset: f64,
1285    pub x: f64,
1286    pub y: f64,
1287}
1288
1289#[derive(Debug, Clone, Serialize, Deserialize)]
1290#[cfg(feature = "diagram-git-graph")]
1291pub struct GitGraphArrowLayout {
1292    pub from: String,
1293    pub to: String,
1294    pub class_index: i64,
1295    pub d: String,
1296}
1297
1298#[derive(Debug, Clone, Serialize, Deserialize)]
1299#[cfg(feature = "diagram-git-graph")]
1300pub struct GitGraphDiagramLayout {
1301    pub bounds: Option<Bounds>,
1302    pub direction: String,
1303    pub rotate_commit_label: bool,
1304    pub show_branches: bool,
1305    pub show_commit_label: bool,
1306    pub parallel_commits: bool,
1307    pub diagram_padding: f64,
1308    pub max_pos: f64,
1309    pub branches: Vec<GitGraphBranchLayout>,
1310    pub commits: Vec<GitGraphCommitLayout>,
1311    pub arrows: Vec<GitGraphArrowLayout>,
1312}
1313
1314#[derive(Debug, Clone, Serialize, Deserialize)]
1315#[cfg(feature = "diagram-tree-view")]
1316pub struct TreeViewNodeLayout {
1317    pub id: i64,
1318    pub level: i64,
1319    pub name: String,
1320    #[serde(rename = "nodeType")]
1321    pub node_type: String,
1322    #[serde(default, rename = "cssClass", skip_serializing_if = "Option::is_none")]
1323    pub css_class: Option<String>,
1324    #[serde(default, skip_serializing_if = "Option::is_none")]
1325    pub icon: Option<String>,
1326    #[serde(default, skip_serializing_if = "Option::is_none")]
1327    pub resolved_icon: Option<String>,
1328    #[serde(default, skip_serializing_if = "Option::is_none")]
1329    pub description: Option<String>,
1330    pub depth: usize,
1331    pub x: f64,
1332    pub y: f64,
1333    pub width: f64,
1334    pub height: f64,
1335    pub label_x: f64,
1336    pub label_y: f64,
1337    pub label_width: f64,
1338    pub label_height: f64,
1339    #[serde(default, skip_serializing_if = "Option::is_none")]
1340    pub description_x: Option<f64>,
1341    #[serde(default, skip_serializing_if = "Option::is_none")]
1342    pub description_width: Option<f64>,
1343}
1344
1345#[derive(Debug, Clone, Serialize, Deserialize)]
1346#[cfg(feature = "diagram-tree-view")]
1347pub struct TreeViewLineLayout {
1348    pub x1: f64,
1349    pub y1: f64,
1350    pub x2: f64,
1351    pub y2: f64,
1352    pub stroke_width: f64,
1353    pub kind: String,
1354}
1355
1356#[derive(Debug, Clone, Serialize, Deserialize)]
1357#[cfg(feature = "diagram-tree-view")]
1358pub struct TreeViewDiagramLayout {
1359    pub bounds: Option<Bounds>,
1360    pub total_width: f64,
1361    pub total_height: f64,
1362    pub row_indent: f64,
1363    pub padding_x: f64,
1364    pub padding_y: f64,
1365    pub line_thickness: f64,
1366    pub use_max_width: bool,
1367    pub label_font_size: f64,
1368    pub nodes: Vec<TreeViewNodeLayout>,
1369    pub lines: Vec<TreeViewLineLayout>,
1370}
1371
1372#[derive(Debug, Clone, Serialize, Deserialize)]
1373#[cfg(feature = "diagram-ishikawa")]
1374pub struct IshikawaLineLayout {
1375    pub x1: f64,
1376    pub y1: f64,
1377    pub x2: f64,
1378    pub y2: f64,
1379    pub class_name: String,
1380    pub marker_start: bool,
1381}
1382
1383#[derive(Debug, Clone, Serialize, Deserialize)]
1384#[cfg(feature = "diagram-ishikawa")]
1385pub struct IshikawaTextLayout {
1386    pub text: String,
1387    pub lines: Vec<String>,
1388    pub class_name: String,
1389    pub x: f64,
1390    pub y: f64,
1391    pub anchor: String,
1392    pub line_height: f64,
1393    pub font_size: f64,
1394    pub bbox: Bounds,
1395}
1396
1397#[derive(Debug, Clone, Serialize, Deserialize)]
1398#[cfg(feature = "diagram-ishikawa")]
1399pub struct IshikawaLabelBoxLayout {
1400    pub x: f64,
1401    pub y: f64,
1402    pub width: f64,
1403    pub height: f64,
1404}
1405
1406#[derive(Debug, Clone, Serialize, Deserialize)]
1407#[cfg(feature = "diagram-ishikawa")]
1408pub struct IshikawaCauseLabelGroupLayout {
1409    pub label_box: IshikawaLabelBoxLayout,
1410    pub label: IshikawaTextLayout,
1411}
1412
1413#[derive(Debug, Clone, Serialize, Deserialize)]
1414#[cfg(feature = "diagram-ishikawa")]
1415pub struct IshikawaSubGroupLayout {
1416    pub line: IshikawaLineLayout,
1417    pub label: IshikawaTextLayout,
1418}
1419
1420#[derive(Debug, Clone, Serialize, Deserialize)]
1421#[cfg(feature = "diagram-ishikawa")]
1422pub struct IshikawaBranchLayout {
1423    pub line: IshikawaLineLayout,
1424    pub label_group: IshikawaCauseLabelGroupLayout,
1425    pub sub_groups: Vec<IshikawaSubGroupLayout>,
1426}
1427
1428#[derive(Debug, Clone, Serialize, Deserialize)]
1429#[cfg(feature = "diagram-ishikawa")]
1430pub struct IshikawaPairLayout {
1431    pub upper: IshikawaBranchLayout,
1432    pub lower: Option<IshikawaBranchLayout>,
1433}
1434
1435#[derive(Debug, Clone, Serialize, Deserialize)]
1436#[cfg(feature = "diagram-ishikawa")]
1437pub struct IshikawaHeadLayout {
1438    pub x: f64,
1439    pub y: f64,
1440    pub width: f64,
1441    pub height: f64,
1442    pub path_d: String,
1443    pub label: IshikawaTextLayout,
1444}
1445
1446#[derive(Debug, Clone, Serialize, Deserialize)]
1447#[cfg(feature = "diagram-ishikawa")]
1448pub struct IshikawaDiagramLayout {
1449    pub bounds: Option<Bounds>,
1450    pub total_width: f64,
1451    pub total_height: f64,
1452    pub viewbox_x: f64,
1453    pub viewbox_y: f64,
1454    pub padding: f64,
1455    pub use_max_width: bool,
1456    pub font_size: f64,
1457    pub head: Option<IshikawaHeadLayout>,
1458    pub spine: Option<IshikawaLineLayout>,
1459    #[serde(default)]
1460    pub pairs: Vec<IshikawaPairLayout>,
1461}
1462
1463#[derive(Debug, Clone, Serialize, Deserialize)]
1464#[cfg(feature = "diagram-event-modeling")]
1465pub struct EventModelingSwimlaneLayout {
1466    pub index: i64,
1467    pub label: String,
1468    pub namespace: Option<String>,
1469    pub x: f64,
1470    pub y: f64,
1471    pub width: f64,
1472    pub height: f64,
1473}
1474
1475#[derive(Debug, Clone, Serialize, Deserialize)]
1476#[cfg(feature = "diagram-event-modeling")]
1477pub struct EventModelingBoxLayout {
1478    pub index: usize,
1479    pub frame_name: String,
1480    pub frame_kind: String,
1481    pub model_entity_type: String,
1482    pub entity_identifier: String,
1483    pub text: String,
1484    pub x: f64,
1485    pub y: f64,
1486    pub width: f64,
1487    pub height: f64,
1488    pub fill: String,
1489    pub stroke: String,
1490    pub swimlane_index: i64,
1491}
1492
1493#[derive(Debug, Clone, Serialize, Deserialize)]
1494#[cfg(feature = "diagram-event-modeling")]
1495pub struct EventModelingRelationLayout {
1496    pub source_frame: String,
1497    pub target_frame: String,
1498    pub x1: f64,
1499    pub y1: f64,
1500    pub x2: f64,
1501    pub y2: f64,
1502    pub stroke: String,
1503}
1504
1505#[derive(Debug, Clone, Serialize, Deserialize)]
1506#[cfg(feature = "diagram-event-modeling")]
1507pub struct EventModelingDiagramLayout {
1508    pub bounds: Option<Bounds>,
1509    pub total_width: f64,
1510    pub total_height: f64,
1511    pub viewbox_x: f64,
1512    pub viewbox_y: f64,
1513    pub padding: f64,
1514    pub use_max_width: bool,
1515    pub swimlanes: Vec<EventModelingSwimlaneLayout>,
1516    pub boxes: Vec<EventModelingBoxLayout>,
1517    pub relations: Vec<EventModelingRelationLayout>,
1518}
1519
1520#[derive(Debug, Clone, Serialize, Deserialize)]
1521#[cfg(feature = "diagram-cynefin")]
1522pub struct CynefinDomainLayout {
1523    pub name: String,
1524    pub cx: f64,
1525    pub cy: f64,
1526    pub x: f64,
1527    pub y: f64,
1528    pub width: f64,
1529    pub height: f64,
1530}
1531
1532#[derive(Debug, Clone, Serialize, Deserialize)]
1533#[cfg(feature = "diagram-cynefin")]
1534pub struct CynefinItemLayout {
1535    pub domain: String,
1536    pub label: String,
1537    pub x: f64,
1538    pub y: f64,
1539    pub width: f64,
1540    pub height: f64,
1541    pub text_x: f64,
1542    pub text_y: f64,
1543    pub overflow: bool,
1544}
1545
1546#[derive(Debug, Clone, Serialize, Deserialize)]
1547#[cfg(feature = "diagram-cynefin")]
1548pub struct CynefinTransitionLayout {
1549    pub from: String,
1550    pub to: String,
1551    pub label: Option<String>,
1552    pub x1: f64,
1553    pub y1: f64,
1554    pub x2: f64,
1555    pub y2: f64,
1556    pub cpx: f64,
1557    pub cpy: f64,
1558}
1559
1560#[derive(Debug, Clone, Serialize, Deserialize)]
1561#[cfg(feature = "diagram-cynefin")]
1562pub struct CynefinDiagramLayout {
1563    pub bounds: Option<Bounds>,
1564    pub width: f64,
1565    pub height: f64,
1566    pub padding: f64,
1567    pub total_width: f64,
1568    pub total_height: f64,
1569    pub use_max_width: bool,
1570    pub show_domain_descriptions: bool,
1571    pub boundary_amplitude: f64,
1572    #[serde(default, skip_serializing_if = "Option::is_none")]
1573    pub seed: Option<f64>,
1574    pub domain_layouts: Vec<CynefinDomainLayout>,
1575    pub items: Vec<CynefinItemLayout>,
1576    pub transitions: Vec<CynefinTransitionLayout>,
1577}
1578
1579#[derive(Debug, Clone, Serialize, Deserialize)]
1580#[cfg(feature = "diagram-railroad")]
1581pub struct RailroadDiagramLayout {
1582    pub bounds: Option<Bounds>,
1583    pub diagram_type: String,
1584    pub width: f64,
1585    pub height: f64,
1586    pub use_max_width: bool,
1587    pub rules: Vec<RailroadRuleLayout>,
1588}
1589
1590#[derive(Debug, Clone, Serialize, Deserialize)]
1591#[cfg(feature = "diagram-railroad")]
1592pub struct RailroadRuleLayout {
1593    pub name: String,
1594    pub x: f64,
1595    pub y: f64,
1596    pub width: f64,
1597    pub height: f64,
1598    pub baseline_y: f64,
1599    pub name_width: f64,
1600    pub definition_x: f64,
1601    pub start_marker_x: f64,
1602    pub end_marker_x: f64,
1603    pub marker_radius: f64,
1604    pub elements: Vec<RailroadElementLayout>,
1605    pub paths: Vec<RailroadPathLayout>,
1606}
1607
1608#[derive(Debug, Clone, Serialize, Deserialize)]
1609#[cfg(feature = "diagram-railroad")]
1610pub struct RailroadElementLayout {
1611    pub kind: String,
1612    pub label: String,
1613    pub x: f64,
1614    pub y: f64,
1615    pub width: f64,
1616    pub height: f64,
1617    pub text_x: f64,
1618    pub text_y: f64,
1619}
1620
1621#[derive(Debug, Clone, Serialize, Deserialize)]
1622#[cfg(feature = "diagram-railroad")]
1623pub struct RailroadPathLayout {
1624    #[serde(default)]
1625    pub x: f64,
1626    #[serde(default)]
1627    pub y: f64,
1628    pub d: String,
1629}
1630
1631#[derive(Debug, Clone, Serialize, Deserialize)]
1632#[cfg(feature = "diagram-gantt")]
1633pub struct GanttAxisTickLayout {
1634    pub time_ms: i64,
1635    pub x: f64,
1636    pub label: String,
1637}
1638
1639#[derive(Debug, Clone, Serialize, Deserialize)]
1640#[cfg(feature = "diagram-gantt")]
1641pub struct GanttExcludeRangeLayout {
1642    pub id: String,
1643    pub start_ms: i64,
1644    pub end_ms: i64,
1645    pub x: f64,
1646    pub y: f64,
1647    pub width: f64,
1648    pub height: f64,
1649}
1650
1651#[derive(Debug, Clone, Serialize, Deserialize)]
1652#[cfg(feature = "diagram-gantt")]
1653pub struct GanttSectionTitleLayout {
1654    pub section: String,
1655    pub index: i64,
1656    pub x: f64,
1657    pub y: f64,
1658    pub dy_em: f64,
1659    pub lines: Vec<String>,
1660    pub class: String,
1661}
1662
1663#[derive(Debug, Clone, Serialize, Deserialize)]
1664#[cfg(feature = "diagram-gantt")]
1665pub struct GanttRowLayout {
1666    pub index: i64,
1667    pub x: f64,
1668    pub y: f64,
1669    pub width: f64,
1670    pub height: f64,
1671    pub class: String,
1672}
1673
1674#[derive(Debug, Clone, Serialize, Deserialize)]
1675#[cfg(feature = "diagram-gantt")]
1676pub struct GanttTaskLabelLayout {
1677    pub id: String,
1678    pub text: String,
1679    pub font_size: f64,
1680    pub width: f64,
1681    pub x: f64,
1682    pub y: f64,
1683    pub class: String,
1684}
1685
1686#[derive(Debug, Clone, Serialize, Deserialize)]
1687#[cfg(feature = "diagram-gantt")]
1688pub struct GanttTaskBarLayout {
1689    pub id: String,
1690    pub x: f64,
1691    pub y: f64,
1692    pub width: f64,
1693    pub height: f64,
1694    pub rx: f64,
1695    pub ry: f64,
1696    pub class: String,
1697}
1698
1699#[derive(Debug, Clone, Serialize, Deserialize)]
1700#[cfg(feature = "diagram-gantt")]
1701pub struct GanttTaskLayout {
1702    pub id: String,
1703    pub task: String,
1704    pub section: String,
1705    pub task_type: String,
1706    pub order: i64,
1707    pub start_ms: i64,
1708    pub end_ms: i64,
1709    #[serde(default)]
1710    pub render_end_ms: Option<i64>,
1711    pub milestone: bool,
1712    pub vert: bool,
1713    pub bar: GanttTaskBarLayout,
1714    pub label: GanttTaskLabelLayout,
1715}
1716
1717#[derive(Debug, Clone, Serialize, Deserialize)]
1718#[cfg(feature = "diagram-gantt")]
1719pub struct GanttDiagramLayout {
1720    pub bounds: Option<Bounds>,
1721    pub width: f64,
1722    pub height: f64,
1723    pub left_padding: f64,
1724    pub right_padding: f64,
1725    pub top_padding: f64,
1726    pub grid_line_start_padding: f64,
1727    pub bar_height: f64,
1728    pub bar_gap: f64,
1729    pub title_top_margin: f64,
1730    pub font_size: f64,
1731    pub section_font_size: f64,
1732    pub number_section_styles: i64,
1733    pub display_mode: String,
1734    pub date_format: String,
1735    pub axis_format: String,
1736    #[serde(default)]
1737    pub tick_interval: Option<String>,
1738    pub top_axis: bool,
1739    pub today_marker: String,
1740    pub categories: Vec<String>,
1741    pub rows: Vec<GanttRowLayout>,
1742    pub section_titles: Vec<GanttSectionTitleLayout>,
1743    pub tasks: Vec<GanttTaskLayout>,
1744    pub excludes: Vec<GanttExcludeRangeLayout>,
1745    #[serde(default)]
1746    pub has_excludes_layer: bool,
1747    pub bottom_ticks: Vec<GanttAxisTickLayout>,
1748    #[serde(default)]
1749    pub top_ticks: Vec<GanttAxisTickLayout>,
1750    pub title: Option<String>,
1751    pub title_x: f64,
1752    pub title_y: f64,
1753}
1754
1755#[derive(Debug, Clone)]
1756#[cfg(feature = "diagram-c4")]
1757pub(crate) struct C4TextRowLayout {
1758    /// Source words emitted into one Mermaid `createText` outer tspan.
1759    ///
1760    /// Keeping the source words (rather than only a joined string) preserves the same
1761    /// whitespace/token boundaries for layout and SVG emission.
1762    pub(crate) words: Vec<String>,
1763}
1764
1765#[derive(Debug, Clone)]
1766#[cfg(feature = "diagram-c4")]
1767pub(crate) struct C4TextRenderPlan {
1768    /// Wrapped source-word rows consumed by the C4 SVG emitter.
1769    pub(crate) rows: Vec<C4TextRowLayout>,
1770    /// The section bbox x offset relative to the centered outer tspan anchor.
1771    pub(crate) bbox_x: f64,
1772    /// The section bbox y offset produced by Mermaid's createText DOM shape.
1773    pub(crate) bbox_y: f64,
1774}
1775
1776#[derive(Debug, Clone, Serialize, Deserialize)]
1777#[cfg(feature = "diagram-c4")]
1778pub struct C4TextBlockLayout {
1779    pub text: String,
1780    pub y: f64,
1781    pub width: f64,
1782    pub height: f64,
1783    pub line_count: usize,
1784    /// Unified C4 labels carry the exact wrapped rows used during measurement. Legacy C4
1785    /// boundary and relationship labels leave this unset and continue using their historical
1786    /// text path.
1787    /// This is an in-memory render artifact; the public layout projection intentionally keeps
1788    /// the stable historical C4 layout schema and does not expose tokenization details.
1789    #[serde(skip)]
1790    pub(crate) render_plan: Option<C4TextRenderPlan>,
1791}
1792
1793#[derive(Debug, Clone, Serialize, Deserialize)]
1794#[cfg(feature = "diagram-c4")]
1795pub struct C4ImageLayout {
1796    pub width: f64,
1797    pub height: f64,
1798    pub y: f64,
1799}
1800
1801#[derive(Debug, Clone, Serialize, Deserialize)]
1802#[cfg(feature = "diagram-c4")]
1803pub struct C4ShapeLayout {
1804    pub alias: String,
1805    pub parent_boundary: String,
1806    pub type_c4_shape: String,
1807    pub x: f64,
1808    pub y: f64,
1809    pub width: f64,
1810    pub height: f64,
1811    pub margin: f64,
1812    pub image: C4ImageLayout,
1813    pub type_block: C4TextBlockLayout,
1814    pub label: C4TextBlockLayout,
1815    #[serde(default)]
1816    pub ty: Option<C4TextBlockLayout>,
1817    #[serde(default)]
1818    pub techn: Option<C4TextBlockLayout>,
1819    #[serde(default)]
1820    pub descr: Option<C4TextBlockLayout>,
1821}
1822
1823#[derive(Debug, Clone, Serialize, Deserialize)]
1824#[cfg(feature = "diagram-c4")]
1825pub struct C4BoundaryLayout {
1826    pub alias: String,
1827    pub parent_boundary: String,
1828    pub x: f64,
1829    pub y: f64,
1830    pub width: f64,
1831    pub height: f64,
1832    pub image: C4ImageLayout,
1833    pub label: C4TextBlockLayout,
1834    #[serde(default)]
1835    pub ty: Option<C4TextBlockLayout>,
1836    #[serde(default)]
1837    pub descr: Option<C4TextBlockLayout>,
1838}
1839
1840#[derive(Debug, Clone, Serialize, Deserialize)]
1841#[cfg(feature = "diagram-c4")]
1842pub struct C4RelLayout {
1843    pub from: String,
1844    pub to: String,
1845    pub rel_type: String,
1846    pub start_point: LayoutPoint,
1847    pub end_point: LayoutPoint,
1848    #[serde(default)]
1849    pub offset_x: Option<i64>,
1850    #[serde(default)]
1851    pub offset_y: Option<i64>,
1852    pub label: C4TextBlockLayout,
1853    #[serde(default)]
1854    pub techn: Option<C4TextBlockLayout>,
1855    #[serde(default)]
1856    pub descr: Option<C4TextBlockLayout>,
1857}
1858
1859#[derive(Debug, Clone, Serialize, Deserialize)]
1860#[non_exhaustive]
1861#[cfg(feature = "diagram-c4")]
1862pub struct C4DiagramLayout {
1863    pub bounds: Option<Bounds>,
1864    pub width: f64,
1865    pub height: f64,
1866    #[serde(default = "crate::c4::default_use_max_width")]
1867    pub use_max_width: bool,
1868    pub container_width: f64,
1869    pub container_height: f64,
1870    #[serde(default, skip_serializing_if = "Option::is_none")]
1871    pub screen_available_width: Option<f64>,
1872    pub c4_type: String,
1873    pub title: Option<String>,
1874    pub boundaries: Vec<C4BoundaryLayout>,
1875    pub shapes: Vec<C4ShapeLayout>,
1876    pub rels: Vec<C4RelLayout>,
1877}
1878
1879#[derive(Debug, Clone, Serialize, Deserialize)]
1880pub struct ErrorDiagramLayout {
1881    pub viewbox_width: f64,
1882    pub viewbox_height: f64,
1883    pub max_width_px: f64,
1884}