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 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 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 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 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 let (items_group, surrogate_group, mut highlight_cd_shape_group) = {
164 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(), false => {
192 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 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.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 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 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 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 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 let (tx, ty) = dt.translation();
468 let r = dt.rotation().to_degrees();
469 format!("translate({tx} {ty}), rotate({r})")
470}