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
46pub fn layout_to_svg_group(
47    layout: &Layout,
48    instance: &impl Instance,
49    options: SvgDrawOptions,
50    title: &str,
51) -> (Group, Rect) {
52    let container = &layout.container;
53
54    let bbox = container
55        .outer_orig
56        .bbox()
57        .resize_by(
58            container.outer_orig.bbox().height() * 0.01,
59            container.outer_orig.bbox().height() * 0.01,
60        )
61        .unwrap();
62
63    let theme = &options.theme;
64
65    let stroke_width =
66        f32::min(bbox.width(), bbox.height()) * 0.001 * theme.stroke_width_multiplier;
67
68    let label = {
69        //print some information on above the left top of the container
70        let bbox = container.outer_orig.bbox();
71
72        let label_content = format!(
73            "h: {:.3} | w: {:.3} | d: {:.3}% | {}",
74            bbox.height(),
75            bbox.width(),
76            layout.density(instance) * 100.0,
77            title,
78        );
79        Text::new(label_content)
80            .set("x", bbox.x_min)
81            .set(
82                "y",
83                bbox.y_min - 0.5 * 0.025 * f32::min(bbox.width(), bbox.height()),
84            )
85            .set("font-size", f32::min(bbox.width(), bbox.height()) * 0.025)
86            .set("font-family", "monospace")
87            .set("font-weight", "500")
88    };
89
90    let highlight_cd_shape_style = &[
91        ("fill", "none"),
92        ("stroke-width", &*format!("{}", 0.5 * stroke_width)),
93        ("stroke", "black"),
94        ("stroke-opacity", "0.3"),
95        (
96            "stroke-dasharray",
97            &*format!("{} {}", 1.0 * stroke_width, 2.0 * stroke_width),
98        ),
99        ("stroke-linecap", "round"),
100        ("stroke-linejoin", "round"),
101    ];
102
103    //draw container
104    let container_group = {
105        let container_group = Group::new().set("id", format!("container_{}", container.id));
106        let bbox = container.outer_orig.bbox();
107        let title = Title::new(format!(
108            "container, id: {}, bbox: [x_min: {:.3}, y_min: {:.3}, x_max: {:.3}, y_max: {:.3}]",
109            container.id, bbox.x_min, bbox.y_min, bbox.x_max, bbox.y_max
110        ));
111
112        //outer
113        container_group
114            .add(svg_util::data_to_path(
115                svg_util::original_shape_data(
116                    &container.outer_orig,
117                    &container.outer_cd,
118                    options.draw_cd_shapes,
119                ),
120                &[
121                    ("fill", &*format!("{}", theme.container_fill)),
122                    ("stroke", "black"),
123                    ("stroke-width", &*format!("{}", 2.0 * stroke_width)),
124                ],
125            ))
126            .add(title)
127    };
128
129    let qz_group = {
130        let mut qz_group = Group::new().set("id", "quality_zones");
131
132        //quality zones
133        for qz in container.quality_zones.iter().rev().flatten() {
134            let color = theme.qz_fill[qz.quality];
135            let stroke_color = svg_util::change_brightness(color, 0.5);
136            for (orig_qz_shape, intern_qz_shape) in qz.shapes_orig.iter().zip(qz.shapes_cd.iter()) {
137                qz_group = qz_group.add(
138                    svg_util::data_to_path(
139                        svg_util::original_shape_data(
140                            orig_qz_shape,
141                            intern_qz_shape,
142                            options.draw_cd_shapes,
143                        ),
144                        &[
145                            ("fill", &*format!("{color}")),
146                            ("fill-opacity", "0.50"),
147                            ("stroke", &*format!("{stroke_color}")),
148                            ("stroke-width", &*format!("{}", 2.0 * stroke_width)),
149                            ("stroke-opacity", &*format!("{}", theme.qz_stroke_opac)),
150                            ("stroke-dasharray", &*format!("{}", 5.0 * stroke_width)),
151                            ("stroke-linecap", "round"),
152                            ("stroke-linejoin", "round"),
153                        ],
154                    )
155                    .add(Title::new(format!("quality zone, q: {}", qz.quality))),
156                );
157            }
158        }
159        qz_group
160    };
161
162    //draw items
163    let (items_group, surrogate_group, mut highlight_cd_shape_group) = {
164        //define all the items and their surrogates (if enabled)
165        let mut item_defs = Definitions::new();
166        let mut surrogate_defs = Definitions::new();
167        for item in instance.items() {
168            let color = match item.min_quality {
169                None => theme.item_fill.to_owned(),
170                Some(q) => svg_util::blend_colors(theme.item_fill, theme.qz_fill[q]),
171            };
172            item_defs = item_defs.add(Group::new().set("id", format!("item_{}", item.id)).add(
173                svg_util::data_to_path(
174                    svg_util::original_shape_data(
175                        &item.shape_orig,
176                        &item.shape_cd,
177                        options.draw_cd_shapes,
178                    ),
179                    &[
180                        ("fill", &*format!("{color}")),
181                        ("stroke-width", &*format!("{stroke_width}")),
182                        ("fill-rule", "nonzero"),
183                        ("stroke", "black"),
184                        ("fill-opacity", "0.5"),
185                    ],
186                ),
187            ));
188
189            let int_transf = match options.draw_cd_shapes {
190                true => Transformation::empty(), //already in internal coordinates
191                false => {
192                    // The original shape is drawn on the SVG, we need to inverse the pre-transform
193                    let pre_transform = item.shape_orig.pre_transform.compose();
194                    pre_transform.inverse()
195                }
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.iter() {
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.iter() {
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 = match options.draw_cd_shapes {
281                true => pi.d_transf,
282                false => {
283                    let item = instance.item(pi.item_id);
284                    int_to_ext_transformation(&pi.d_transf, &item.shape_orig.pre_transform)
285                }
286            };
287            let title = Title::new(format!("item, id: {}, transf: [{}]", pi.item_id, dtransf));
288            let pi_ref = Use::new()
289                .set("transform", transform_to_svg(dtransf))
290                .set("href", format!("#item_{}", pi.item_id))
291                .add(title);
292
293            items_group = items_group.add(pi_ref);
294
295            if options.surrogate {
296                let pi_surr_ref = Use::new()
297                    .set("transform", transform_to_svg(dtransf))
298                    .set("href", format!("#surrogate_{}", pi.item_id));
299
300                surrogate_group = surrogate_group.add(pi_surr_ref);
301            }
302            if options.highlight_cd_shapes {
303                let pi_cd_ref = Use::new()
304                    .set("transform", transform_to_svg(dtransf))
305                    .set("href", format!("#cd_shape_{}", pi.item_id));
306                highlight_cd_shapes_group = highlight_cd_shapes_group.add(pi_cd_ref);
307            }
308        }
309
310        (items_group, surrogate_group, highlight_cd_shapes_group)
311    };
312
313    //draw quadtree (if enabled)
314    let qt_group = match options.quadtree {
315        false => None,
316        true => {
317            let qt_data = svg_util::quad_tree_data(&layout.cde().quadtree, &NoFilter);
318            let qt_group = Group::new()
319                .set("id", "quadtree")
320                .add(svg_util::data_to_path(
321                    qt_data.0,
322                    &[
323                        ("fill", "red"),
324                        ("stroke-width", &*format!("{}", stroke_width * 0.25)),
325                        ("fill-rule", "nonzero"),
326                        ("fill-opacity", "0.6"),
327                        ("stroke", "black"),
328                    ],
329                ))
330                .add(svg_util::data_to_path(
331                    qt_data.1,
332                    &[
333                        ("fill", "none"),
334                        ("stroke-width", &*format!("{}", stroke_width * 0.25)),
335                        ("fill-rule", "nonzero"),
336                        ("fill-opacity", "0.3"),
337                        ("stroke", "black"),
338                    ],
339                ))
340                .add(svg_util::data_to_path(
341                    qt_data.2,
342                    &[
343                        ("fill", "green"),
344                        ("fill-opacity", "0.6"),
345                        ("stroke-width", &*format!("{}", stroke_width * 0.25)),
346                        ("stroke", "black"),
347                    ],
348                ));
349            Some(qt_group)
350        }
351    };
352
353    //highlight colliding items (if enabled)
354    let collision_group = match options.highlight_collisions {
355        false => None,
356        true => {
357            let mut collision_group = Group::new().set("id", "collision_lines");
358            for (pk, pi) in layout.placed_items.iter() {
359                let collector = {
360                    let mut collector =
361                        BasicHazardCollector::with_capacity(layout.cde().hazards_map.len());
362                    layout
363                        .cde()
364                        .collect_poly_collisions(&pi.shape, &mut collector);
365                    collector.retain(|_, entity| {
366                        // filter out the item itself
367                        if let HazardEntity::PlacedItem {
368                            pk: colliding_pk, ..
369                        } = entity
370                        {
371                            *colliding_pk != pk
372                        } else {
373                            true
374                        }
375                    });
376                    collector
377                };
378                for (_, haz_entity) in collector.iter() {
379                    match haz_entity {
380                        HazardEntity::PlacedItem {
381                            pk: colliding_pk, ..
382                        } => {
383                            let haz_hash = {
384                                let mut hasher = DefaultHasher::new();
385                                haz_entity.hash(&mut hasher);
386                                hasher.finish()
387                            };
388                            let pi_hash = {
389                                let mut hasher = DefaultHasher::new();
390                                HazardEntity::from((pk, pi)).hash(&mut hasher);
391                                hasher.finish()
392                            };
393
394                            if haz_hash < pi_hash {
395                                // avoid duplicate lines
396                                let start = pi.shape.poi.center;
397                                let end = layout.placed_items[*colliding_pk].shape.poi.center;
398                                collision_group = collision_group.add(svg_util::data_to_path(
399                                    svg_util::edge_data(Edge { start, end }),
400                                    &[
401                                        (
402                                            "stroke",
403                                            &*format!("{}", theme.collision_highlight_color),
404                                        ),
405                                        ("stroke-opacity", "0.75"),
406                                        ("stroke-width", &*format!("{}", stroke_width * 4.0)),
407                                        (
408                                            "stroke-dasharray",
409                                            &*format!(
410                                                "{} {}",
411                                                4.0 * stroke_width,
412                                                8.0 * stroke_width
413                                            ),
414                                        ),
415                                        ("stroke-linecap", "round"),
416                                        ("stroke-linejoin", "round"),
417                                    ],
418                                ));
419                            }
420                        }
421                        HazardEntity::Exterior => {
422                            collision_group = collision_group.add(svg_util::point(
423                                pi.shape.poi.center,
424                                Some(&*format!("{}", theme.collision_highlight_color)),
425                                Some(3.0 * stroke_width),
426                            ));
427                        }
428                        _ => {
429                            warn!("unexpected hazard entity");
430                        }
431                    }
432                }
433            }
434            Some(collision_group)
435        }
436    };
437
438    if options.highlight_cd_shapes {
439        highlight_cd_shape_group = highlight_cd_shape_group.add(svg_util::data_to_path(
440            svg_util::simple_polygon_data(&container.outer_cd),
441            highlight_cd_shape_style,
442        ));
443    }
444
445    let optionals = [
446        Some(highlight_cd_shape_group),
447        Some(surrogate_group),
448        qt_group,
449        collision_group,
450    ]
451    .into_iter()
452    .flatten()
453    .fold(Group::new().set("id", "optionals"), |g, opt| g.add(opt));
454
455    let combined_group = Group::new()
456        .add(container_group)
457        .add(items_group)
458        .add(qz_group)
459        .add(optionals)
460        .add(label);
461
462    (combined_group, bbox)
463}
464fn transform_to_svg(dt: DTransformation) -> String {
465    //https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform
466    //operations are effectively applied from right to left
467    let (tx, ty) = dt.translation();
468    let r = dt.rotation().to_degrees();
469    format!("translate({tx} {ty}), rotate({r})")
470}