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