1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
//! SVG rendering for layout layers.
use log::debug;
use svg::{self, node::element as svg_element};
use orrery_core::{draw::LayeredOutput, geometry::Bounds};
use super::Svg;
use crate::layout::{
component,
layer::{ContentStack, Layer, LayeredLayout, LayoutContent},
positioning::LayoutBounds,
sequence,
};
impl Svg {
/// Renders the complete layered layout to an SVG document.
pub fn render_layered_layout(&mut self, layout: &LayeredLayout) -> svg::Document {
// Calculate content bounds
let content_bounds = self.calculate_layered_layout_bounds(layout);
let content_size = content_bounds.to_size();
// Calculate final SVG dimensions with margins
let svg_size = self.calculate_svg_dimensions(content_size);
// Create the SVG document with calculated dimensions
let doc = svg::Document::new()
.set(
"viewBox",
format!("0 0 {} {}", svg_size.width(), svg_size.height()),
)
.set("width", svg_size.width())
.set("height", svg_size.height());
// Add background
let mut doc = self.add_background(doc, svg_size);
// Add clip paths for all layers that need clipping
// Each clip path gets a unique ID based on the layer's z-index
for layer in layout.iter_from_bottom() {
if let Some(bounds) = layer.clip_bounds() {
let clip_id = format!("clip-layer-{}", layer.z_index());
let clip_path = self.create_clip_path(&clip_id, bounds);
doc = doc.add(clip_path);
}
}
// Calculate margins for centering
let margin_x = (svg_size.width() - content_size.width()) / 2.0;
let margin_y = (svg_size.height() - content_size.height()) / 2.0;
// Create a main group with translation to center content and adjust for min bounds
let mut main_group = svg_element::Group::new().set(
"transform",
format!(
"translate({}, {})",
margin_x - content_bounds.min_x(),
margin_y - content_bounds.min_y()
),
);
// Add each layer in order
for layer in layout.iter_from_bottom() {
main_group = main_group.add(self.render_layer(layer));
}
// Add marker definitions for all layers
let arrow_markers_defs = self.arrow_with_text_drawer.draw_marker_definitions();
doc = doc.add(arrow_markers_defs);
// Add the main group to the document
doc.add(main_group)
}
/// Creates an SVG clip path for a layer.
///
/// This generates an SVG Definitions element containing a ClipPath with the specified ID.
/// The clip path contains a rectangle that matches the provided bounds.
///
/// # Arguments
///
/// * `clip_id` - Unique identifier for the clip path.
/// * `bounds` - The bounds to use for clipping.
fn create_clip_path(&self, clip_id: &str, bounds: Bounds) -> svg_element::Definitions {
let defs = svg_element::Definitions::new();
// Create a clip path with a rectangle matching the bounds
let clip_rect = svg_element::Rectangle::new()
.set("x", bounds.min_x())
.set("y", bounds.min_y())
.set("width", bounds.width())
.set("height", bounds.height());
let clip_path = svg_element::ClipPath::new()
.set("id", clip_id)
.add(clip_rect);
defs.add(clip_path)
}
/// Calculates the combined bounds of all layers, considering their offsets.
///
/// Computes the total bounding box that contains all layers in the layout,
/// accounting for layer offsets. This is used to determine the overall size needed
/// for the SVG document.
///
/// # Returns
///
/// A `Bounds` object that encompasses all content in all layers.
fn calculate_layered_layout_bounds(&self, layout: &LayeredLayout) -> Bounds {
let mut layout_iter = layout.iter_from_bottom();
// Start with the bounds of the first (bottom) layer.
// Note: `iter_from_bottom().next()` returning `None` is equivalent to `is_empty()`..
let Some(first_layer) = layout_iter.next() else {
return Bounds::default();
};
let mut combined_bounds = self.calculate_layer_bounds(first_layer);
// Merge with bounds of additional layers, adjusting for layer offset
for layer in layout_iter {
let layer_bounds = self.calculate_layer_bounds(layer);
// Adjust bounds for layer offset by creating a translated copy
let offset_bounds = layer_bounds.translate(layer.offset());
// Merge with the combined bounds to include this layer
combined_bounds = combined_bounds.merge(&offset_bounds);
}
combined_bounds
}
/// Calculates bounds for a single layer.
fn calculate_layer_bounds(&self, layer: &Layer) -> Bounds {
match layer.content() {
LayoutContent::Component(comp_layout) => {
self.calculate_component_diagram_bounds(comp_layout)
}
LayoutContent::Sequence(seq_layout) => {
self.calculate_sequence_diagram_bounds(seq_layout)
}
}
}
/// Renders a single layer to SVG.
///
/// Creates an SVG group for the layer, applies transformations and clipping,
/// and renders the layer's content (either component or sequence diagram).
///
/// # Arguments
///
/// * `layer` - The layer to render.
///
/// # Returns
///
/// An SVG `Group` element containing the rendered layer.
fn render_layer(&mut self, layer: &Layer) -> svg_element::Group {
// Create a group for this layer
let mut layer_group = svg_element::Group::new();
let offset = layer.offset();
// Apply offset transformation if not at origin
if !offset.is_zero() {
layer_group = layer_group.set(
"transform",
format!("translate({}, {})", offset.x(), offset.y()),
);
}
// Apply clipping if specified for this layer
if let Some(_bounds) = layer.clip_bounds() {
// Create a unique clip ID for this layer based on its z-index
let clip_id = format!("clip-layer-{}", layer.z_index());
// Apply the clip-path property referencing the previously defined clip path
layer_group = layer_group.set("clip-path", format!("url(#{clip_id})"));
}
// Render the layer content based on its type
self.render_layer_content(layer.content())
.into_iter()
.fold(layer_group, |group, content_group| group.add(content_group))
}
/// Renders layer content by dispatching to the appropriate content-specific renderer.
fn render_layer_content(&mut self, content: &LayoutContent) -> Vec<Box<dyn svg::Node>> {
match content {
LayoutContent::Component(layout) => self
.render_content_stack(layout, |svg, content| svg.render_component_content(content)),
LayoutContent::Sequence(layout) => self
.render_content_stack(layout, |svg, content| svg.render_sequence_content(content)),
}
}
/// Renders a [`ContentStack`] with positioned content.
fn render_content_stack<T: LayoutBounds>(
&mut self,
content_stack: &ContentStack<T>,
render_fn: impl Fn(&mut Self, &T) -> Vec<Box<dyn svg::Node>>,
) -> Vec<Box<dyn svg::Node>> {
let mut groups = Vec::with_capacity(content_stack.len());
// Render all positioned content in the stack (reverse order for proper layering)
for positioned_content in content_stack.iter().rev() {
let offset = positioned_content.offset();
let content = positioned_content.content();
debug!(offset:?; "Rendering positioned content");
// Create a group for this positioned content with its offset applied
let mut positioned_group = svg_element::Group::new();
// Apply the positioned content's offset as a transform
if !positioned_content.offset().is_zero() {
positioned_group = positioned_group.set(
"transform",
format!("translate({}, {})", offset.x(), offset.y()),
);
}
// Use the provided render function to render the content
positioned_group = render_fn(self, content)
.into_iter()
.fold(positioned_group, |group, content_group| {
group.add(content_group)
});
// Add the positioned group to the layer
groups.push(positioned_group.into());
}
groups
}
/// Renders component-specific content.
fn render_component_content(&mut self, content: &component::Layout) -> Vec<Box<dyn svg::Node>> {
let mut output = LayeredOutput::new();
// Render all components within this positioned content
for component in content.components() {
let component_output = self.render_component(component);
output.merge(component_output);
}
// Render all relations within this positioned content
for relation in content.relations() {
let relation_output = self.render_relation(
content.source(relation),
content.target(relation),
relation.arrow_with_text(),
);
output.merge(relation_output);
}
output.render()
}
/// Renders sequence-specific content.
fn render_sequence_content(&mut self, content: &sequence::Layout) -> Vec<Box<dyn svg::Node>> {
let mut output = LayeredOutput::new();
// Render all participants within this positioned content
for participant in content.participants().values() {
let participant_output = self.render_participant(participant);
output.merge(participant_output);
}
// Render all fragments within this positioned content
for fragment in content.fragments() {
let fragment_output = self.render_fragment(fragment);
output.merge(fragment_output);
}
// Render all activation boxes within this positioned content
// Sort by nesting level to ensure proper z-order (lower levels render first, higher levels on top)
let mut sorted_activations: Vec<_> = content.activations().iter().collect();
sorted_activations.sort_by_key(|activation_box| activation_box.drawable().nesting_level());
for activation_box in sorted_activations {
let activation_output = self.render_activation_box(activation_box, content);
output.merge(activation_output);
}
// Render all notes within this positioned content
for note in content.notes() {
let note_output = self.render_note(note);
output.merge(note_output);
}
// Render all messages within this positioned content
for message in content.messages() {
let message_output = self.render_message(message, content);
output.merge(message_output);
}
output.render()
}
}