Skip to main content

jagua_rs/io/svg/
layout_to_svg.rs

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