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#[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#[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)]
52pub 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 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 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 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 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 let (items_group, surrogate_group, mut highlight_cd_shape_group) = {
176 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 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 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 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 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 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 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 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}