jagua_rs/io/svg/
layout_to_svg.rs

1use crate::collision_detection::hazards::HazardEntity;
2use crate::collision_detection::hazards::collector::BasicHazardCollector;
3use crate::collision_detection::hazards::filter::NoFilter;
4use crate::entities::{Instance, Layout, LayoutSnapshot};
5use crate::geometry::geo_traits::Transformable;
6use crate::geometry::primitives::{Circle, Edge, Rect};
7use crate::geometry::{DTransformation, Transformation};
8use crate::io::export::int_to_ext_transformation;
9use crate::io::svg::svg_util;
10use crate::io::svg::svg_util::SvgDrawOptions;
11use log::warn;
12use std::hash::{DefaultHasher, Hash, Hasher};
13use svg::Document;
14use svg::node::element::{Definitions, Group, Text, Title, Use};
15
16pub fn s_layout_to_svg(
17    s_layout: &LayoutSnapshot,
18    instance: &impl Instance,
19    options: SvgDrawOptions,
20    title: &str,
21) -> Document {
22    let layout = Layout::from_snapshot(s_layout);
23    layout_to_svg(&layout, instance, options, title)
24}
25
26pub fn layout_to_svg(
27    layout: &Layout,
28    instance: &impl Instance,
29    options: SvgDrawOptions,
30    title: &str,
31) -> Document {
32    let (group, bbox) = layout_to_svg_group(layout, instance, options, title);
33
34    let vbox = bbox.scale(1.1);
35    let vbox_svg = format!(
36        "{} {} {} {}",
37        vbox.x_min,
38        vbox.y_min,
39        vbox.width(),
40        vbox.height()
41    );
42
43    Document::new().set("viewBox", vbox_svg).add(group)
44}
45
46#[allow(clippy::too_many_lines)]
47pub fn layout_to_svg_group(
48    layout: &Layout,
49    instance: &impl Instance,
50    options: SvgDrawOptions,
51    title: &str,
52) -> (Group, Rect) {
53    let container = &layout.container;
54
55    let bbox = container
56        .outer_orig
57        .bbox()
58        .resize_by(
59            container.outer_orig.bbox().height() * 0.01,
60            container.outer_orig.bbox().height() * 0.01,
61        )
62        .unwrap();
63
64    let theme = &options.theme;
65
66    let stroke_width =
67        f32::min(bbox.width(), bbox.height()) * 0.001 * theme.stroke_width_multiplier;
68
69    let label = {
70        //print some information on above the left top of the container
71        let bbox = container.outer_orig.bbox();
72
73        let label_content = format!(
74            "h: {:.3} | w: {:.3} | d: {:.3}% | {}",
75            bbox.height(),
76            bbox.width(),
77            layout.density(instance) * 100.0,
78            title,
79        );
80        Text::new(label_content)
81            .set("x", bbox.x_min)
82            .set(
83                "y",
84                bbox.y_min - 0.5 * 0.025 * f32::min(bbox.width(), bbox.height()),
85            )
86            .set("font-size", f32::min(bbox.width(), bbox.height()) * 0.025)
87            .set("font-family", "monospace")
88            .set("font-weight", "500")
89    };
90
91    let highlight_cd_shape_style = &[
92        ("fill", "none"),
93        ("stroke-width", &*format!("{}", 0.5 * stroke_width)),
94        ("stroke", "black"),
95        ("stroke-opacity", "0.3"),
96        (
97            "stroke-dasharray",
98            &*format!("{} {}", 1.0 * stroke_width, 2.0 * stroke_width),
99        ),
100        ("stroke-linecap", "round"),
101        ("stroke-linejoin", "round"),
102    ];
103
104    //draw container
105    let container_group = {
106        let container_group = Group::new().set("id", format!("container_{}", container.id));
107        let bbox = container.outer_orig.bbox();
108        let title = Title::new(format!(
109            "container, id: {}, bbox: [x_min: {:.3}, y_min: {:.3}, x_max: {:.3}, y_max: {:.3}]",
110            container.id, bbox.x_min, bbox.y_min, bbox.x_max, bbox.y_max
111        ));
112
113        //outer
114        container_group
115            .add(svg_util::data_to_path(
116                svg_util::original_shape_data(
117                    &container.outer_orig,
118                    &container.outer_cd,
119                    options.draw_cd_shapes,
120                ),
121                &[
122                    ("fill", &*format!("{}", theme.container_fill)),
123                    ("stroke", "black"),
124                    ("stroke-width", &*format!("{}", 2.0 * stroke_width)),
125                ],
126            ))
127            .add(title)
128    };
129
130    let quality_zones_group = {
131        let mut qz_group = Group::new().set("id", "quality_zones");
132
133        //quality zones
134        for qz in container.quality_zones.iter().rev().flatten() {
135            let color = theme.qz_fill[qz.quality];
136            let stroke_color = svg_util::change_brightness(color, 0.5);
137            for (orig_qz_shape, intern_qz_shape) in qz.shapes_orig.iter().zip(qz.shapes_cd.iter()) {
138                qz_group = qz_group.add(
139                    svg_util::data_to_path(
140                        svg_util::original_shape_data(
141                            orig_qz_shape,
142                            intern_qz_shape,
143                            options.draw_cd_shapes,
144                        ),
145                        &[
146                            ("fill", &*format!("{color}")),
147                            ("fill-opacity", "0.50"),
148                            ("stroke", &*format!("{stroke_color}")),
149                            ("stroke-width", &*format!("{}", 2.0 * stroke_width)),
150                            ("stroke-opacity", &*format!("{}", theme.qz_stroke_opac)),
151                            ("stroke-dasharray", &*format!("{}", 5.0 * stroke_width)),
152                            ("stroke-linecap", "round"),
153                            ("stroke-linejoin", "round"),
154                        ],
155                    )
156                    .add(Title::new(format!("quality zone, q: {}", qz.quality))),
157                );
158            }
159        }
160        qz_group
161    };
162
163    //draw items
164    let (items_group, surrogate_group, mut highlight_cd_shape_group) = {
165        //define all the items and their surrogates (if enabled)
166        let mut item_defs = Definitions::new();
167        let mut surrogate_defs = Definitions::new();
168        for item in instance.items() {
169            let color = match item.min_quality {
170                None => theme.item_fill,
171                Some(q) => svg_util::blend_colors(theme.item_fill, theme.qz_fill[q]),
172            };
173            item_defs = item_defs.add(Group::new().set("id", format!("item_{}", item.id)).add(
174                svg_util::data_to_path(
175                    svg_util::original_shape_data(
176                        &item.shape_orig,
177                        &item.shape_cd,
178                        options.draw_cd_shapes,
179                    ),
180                    &[
181                        ("fill", &*format!("{color}")),
182                        ("stroke-width", &*format!("{stroke_width}")),
183                        ("fill-rule", "nonzero"),
184                        ("stroke", "black"),
185                        ("fill-opacity", "0.5"),
186                    ],
187                ),
188            ));
189
190            let int_transf = if options.draw_cd_shapes {
191                Transformation::empty()
192            } else {
193                // The original shape is drawn on the SVG, we need to inverse the pre-transform
194                let pre_transform = item.shape_orig.pre_transform.compose();
195                pre_transform.inverse()
196            };
197
198            if options.surrogate {
199                let mut surrogate_group = Group::new().set("id", format!("surrogate_{}", item.id));
200                let poi_style = [
201                    ("fill", "black"),
202                    ("fill-opacity", "0.1"),
203                    ("stroke", "black"),
204                    ("stroke-width", &*format!("{stroke_width}")),
205                    ("stroke-opacity", "0.8"),
206                ];
207                let ff_style = [
208                    ("fill", "none"),
209                    ("stroke", "black"),
210                    ("stroke-width", &*format!("{stroke_width}")),
211                    ("stroke-opacity", "0.8"),
212                ];
213                let no_ff_style = [
214                    ("fill", "none"),
215                    ("stroke", "black"),
216                    ("stroke-width", &*format!("{stroke_width}")),
217                    ("stroke-opacity", "0.5"),
218                    ("stroke-dasharray", &*format!("{}", 5.0 * stroke_width)),
219                    ("stroke-linecap", "round"),
220                    ("stroke-linejoin", "round"),
221                ];
222
223                let surrogate = item.shape_cd.surrogate();
224                let poi = &surrogate.poles[0];
225                let ff_poles = surrogate.ff_poles();
226
227                for pole in &surrogate.poles {
228                    if pole == poi {
229                        let svg_circle =
230                            svg_util::circle(pole.transform_clone(&int_transf), &poi_style);
231                        surrogate_group = surrogate_group.add(svg_circle);
232                    } else if ff_poles.contains(pole) {
233                        let svg_circle =
234                            svg_util::circle(pole.transform_clone(&int_transf), &ff_style);
235                        surrogate_group = surrogate_group.add(svg_circle);
236                    } else {
237                        let svg_circle =
238                            svg_util::circle(pole.transform_clone(&int_transf), &no_ff_style);
239                        surrogate_group = surrogate_group.add(svg_circle);
240                    }
241                }
242                for pier in &surrogate.piers {
243                    surrogate_group = surrogate_group.add(svg_util::data_to_path(
244                        svg_util::edge_data(pier.transform_clone(&int_transf)),
245                        &ff_style,
246                    ));
247                }
248                surrogate_defs = surrogate_defs.add(surrogate_group);
249            }
250
251            if options.highlight_cd_shapes {
252                let t_shape_cd = item.shape_cd.transform_clone(&int_transf);
253                //draw the CD shape with a dotted line, and no fill
254                let mut group = Group::new().add(svg_util::data_to_path(
255                    svg_util::simple_polygon_data(&t_shape_cd),
256                    highlight_cd_shape_style,
257                ));
258                if options.draw_cd_shapes {
259                    //draw all the vertices as dots
260                    for p in &t_shape_cd.vertices {
261                        let circle = Circle {
262                            center: *p,
263                            radius: 0.5 * stroke_width,
264                        };
265                        group = group.add(svg_util::circle(
266                            circle,
267                            &[("fill", "cyan"), ("fill-opacity", "0.8")],
268                        ));
269                    }
270                }
271                let group = group.set("id", format!("cd_shape_{}", item.id));
272                item_defs = item_defs.add(group);
273            }
274        }
275        let mut items_group = Group::new().set("id", "items").add(item_defs);
276        let mut surrogate_group = Group::new().set("id", "surrogates").add(surrogate_defs);
277        let mut highlight_cd_shapes_group = Group::new().set("id", "highlight_cd_shapes");
278
279        for pi in layout.placed_items.values() {
280            let dtransf = if options.draw_cd_shapes {
281                pi.d_transf
282            } else {
283                let item = instance.item(pi.item_id);
284                int_to_ext_transformation(&pi.d_transf, &item.shape_orig.pre_transform)
285            };
286            let title = Title::new(format!("item, id: {}, transf: [{}]", pi.item_id, dtransf));
287            let pi_ref = Use::new()
288                .set("transform", transform_to_svg(dtransf))
289                .set("href", format!("#item_{}", pi.item_id))
290                .add(title);
291
292            items_group = items_group.add(pi_ref);
293
294            if options.surrogate {
295                let pi_surr_ref = Use::new()
296                    .set("transform", transform_to_svg(dtransf))
297                    .set("href", format!("#surrogate_{}", pi.item_id));
298
299                surrogate_group = surrogate_group.add(pi_surr_ref);
300            }
301            if options.highlight_cd_shapes {
302                let pi_cd_ref = Use::new()
303                    .set("transform", transform_to_svg(dtransf))
304                    .set("href", format!("#cd_shape_{}", pi.item_id));
305                highlight_cd_shapes_group = highlight_cd_shapes_group.add(pi_cd_ref);
306            }
307        }
308
309        (items_group, surrogate_group, highlight_cd_shapes_group)
310    };
311
312    //draw quadtree (if enabled)
313    let quad_tree_group = if options.quadtree {
314        let qt_data = svg_util::quad_tree_data(&layout.cde().quadtree, &NoFilter);
315        let qt_group = Group::new()
316            .set("id", "quadtree")
317            .add(svg_util::data_to_path(
318                qt_data.0,
319                &[
320                    ("fill", "red"),
321                    ("stroke-width", &*format!("{}", stroke_width * 0.25)),
322                    ("fill-rule", "nonzero"),
323                    ("fill-opacity", "0.6"),
324                    ("stroke", "black"),
325                ],
326            ))
327            .add(svg_util::data_to_path(
328                qt_data.1,
329                &[
330                    ("fill", "none"),
331                    ("stroke-width", &*format!("{}", stroke_width * 0.25)),
332                    ("fill-rule", "nonzero"),
333                    ("fill-opacity", "0.3"),
334                    ("stroke", "black"),
335                ],
336            ))
337            .add(svg_util::data_to_path(
338                qt_data.2,
339                &[
340                    ("fill", "green"),
341                    ("fill-opacity", "0.6"),
342                    ("stroke-width", &*format!("{}", stroke_width * 0.25)),
343                    ("stroke", "black"),
344                ],
345            ));
346        Some(qt_group)
347    } else {
348        None
349    };
350
351    //highlight colliding items (if enabled)
352    let collision_group = if options.highlight_collisions {
353        let mut collision_group = Group::new().set("id", "collision_lines");
354        for (pk, pi) in &layout.placed_items {
355            let collector = {
356                let mut collector =
357                    BasicHazardCollector::with_capacity(layout.cde().hazards_map.len());
358                layout
359                    .cde()
360                    .collect_poly_collisions(&pi.shape, &mut collector);
361                collector.retain(|_, entity| {
362                    // filter out the item itself
363                    if let HazardEntity::PlacedItem {
364                        pk: colliding_pk, ..
365                    } = entity
366                    {
367                        *colliding_pk != pk
368                    } else {
369                        true
370                    }
371                });
372                collector
373            };
374            for (_, haz_entity) in &collector {
375                match haz_entity {
376                    HazardEntity::PlacedItem {
377                        pk: colliding_pk, ..
378                    } => {
379                        let haz_hash = {
380                            let mut hasher = DefaultHasher::new();
381                            haz_entity.hash(&mut hasher);
382                            hasher.finish()
383                        };
384                        let pi_hash = {
385                            let mut hasher = DefaultHasher::new();
386                            HazardEntity::from((pk, pi)).hash(&mut hasher);
387                            hasher.finish()
388                        };
389
390                        if haz_hash < pi_hash {
391                            // avoid duplicate lines
392                            let start = pi.shape.poi.center;
393                            let end = layout.placed_items[*colliding_pk].shape.poi.center;
394                            collision_group = collision_group.add(svg_util::data_to_path(
395                                svg_util::edge_data(Edge { start, end }),
396                                &[
397                                    ("stroke", &*format!("{}", theme.collision_highlight_color)),
398                                    ("stroke-opacity", "0.75"),
399                                    ("stroke-width", &*format!("{}", stroke_width * 4.0)),
400                                    (
401                                        "stroke-dasharray",
402                                        &*format!("{} {}", 4.0 * stroke_width, 8.0 * stroke_width),
403                                    ),
404                                    ("stroke-linecap", "round"),
405                                    ("stroke-linejoin", "round"),
406                                ],
407                            ));
408                        }
409                    }
410                    HazardEntity::Exterior => {
411                        collision_group = collision_group.add(svg_util::point(
412                            pi.shape.poi.center,
413                            Some(&*format!("{}", theme.collision_highlight_color)),
414                            Some(3.0 * stroke_width),
415                        ));
416                    }
417                    _ => {
418                        warn!("unexpected hazard entity");
419                    }
420                }
421            }
422        }
423        Some(collision_group)
424    } else {
425        None
426    };
427
428    if options.highlight_cd_shapes {
429        highlight_cd_shape_group = highlight_cd_shape_group.add(svg_util::data_to_path(
430            svg_util::simple_polygon_data(&container.outer_cd),
431            highlight_cd_shape_style,
432        ));
433    }
434
435    let optionals = [
436        Some(highlight_cd_shape_group),
437        Some(surrogate_group),
438        quad_tree_group,
439        collision_group,
440    ]
441    .into_iter()
442    .flatten()
443    .fold(Group::new().set("id", "optionals"), Group::add);
444
445    let combined_group = Group::new()
446        .add(container_group)
447        .add(items_group)
448        .add(quality_zones_group)
449        .add(optionals)
450        .add(label);
451
452    (combined_group, bbox)
453}
454fn transform_to_svg(dt: DTransformation) -> String {
455    //https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform
456    //operations are effectively applied from right to left
457    let (tx, ty) = dt.translation();
458    let r = dt.rotation().to_degrees();
459    format!("translate({tx} {ty}), rotate({r})")
460}