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 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 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 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 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 let (items_group, surrogate_group, mut highlight_cd_shape_group) = {
165 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 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 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 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 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 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 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 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 let (tx, ty) = dt.translation();
458 let r = dt.rotation().to_degrees();
459 format!("translate({tx} {ty}), rotate({r})")
460}