1use crate::collision_detection::hazards::HazardEntity;
2use crate::collision_detection::hazards::detector::{BasicHazardDetector, HazardDetector};
3use crate::collision_detection::hazards::filter::NoHazardFilter;
4use crate::entities::{Instance, Layout, LayoutSnapshot};
5use crate::geometry::geo_traits::Transformable;
6use crate::geometry::primitives::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 let bbox = container.outer_orig.bbox();
44
45 let label_content = format!(
46 "height: {:.3} | width: {:.3} | density: {:.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 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 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(svg_util::data_to_path(
100 svg_util::simple_polygon_data(&container.outer_cd),
101 highlight_cd_shape_style,
102 ))
103 .add(title)
104 };
105
106 let qz_group = {
107 let mut qz_group = Group::new().set("id", "quality_zones");
108
109 for qz in container.quality_zones.iter().rev().flatten() {
111 let color = theme.qz_fill[qz.quality];
112 let stroke_color = svg_util::change_brightness(color, 0.5);
113 for (orig_qz_shape, intern_qz_shape) in qz.shapes_orig.iter().zip(qz.shapes_cd.iter()) {
114 qz_group = qz_group.add(
115 svg_util::data_to_path(
116 svg_util::original_shape_data(
117 orig_qz_shape,
118 intern_qz_shape,
119 options.draw_cd_shapes,
120 ),
121 &[
122 ("fill", &*format!("{color}")),
123 ("fill-opacity", "0.50"),
124 ("stroke", &*format!("{stroke_color}")),
125 ("stroke-width", &*format!("{}", 2.0 * stroke_width)),
126 ("stroke-opacity", &*format!("{}", theme.qz_stroke_opac)),
127 ("stroke-dasharray", &*format!("{}", 5.0 * stroke_width)),
128 ("stroke-linecap", "round"),
129 ("stroke-linejoin", "round"),
130 ],
131 )
132 .add(Title::new(format!("quality zone, q: {}", qz.quality))),
133 );
134 }
135 }
136 qz_group
137 };
138
139 let (items_group, surrogate_group, highlight_cd_shape_group) = {
141 let mut item_defs = Definitions::new();
143 let mut surrogate_defs = Definitions::new();
144 for item in instance.items() {
145 let color = match item.min_quality {
146 None => theme.item_fill.to_owned(),
147 Some(q) => svg_util::blend_colors(theme.item_fill, theme.qz_fill[q]),
148 };
149 item_defs = item_defs.add(Group::new().set("id", format!("item_{}", item.id)).add(
150 svg_util::data_to_path(
151 svg_util::original_shape_data(
152 &item.shape_orig,
153 &item.shape_cd,
154 options.draw_cd_shapes,
155 ),
156 &[
157 ("fill", &*format!("{color}")),
158 ("stroke-width", &*format!("{stroke_width}")),
159 ("fill-rule", "nonzero"),
160 ("stroke", "black"),
161 ("fill-opacity", "0.5"),
162 ],
163 ),
164 ));
165
166 let int_transf = match options.draw_cd_shapes {
167 true => Transformation::empty(), false => {
169 let pre_transform = item.shape_orig.pre_transform.compose();
171 pre_transform.inverse()
172 }
173 };
174
175 if options.surrogate {
176 let mut surrogate_group = Group::new().set("id", format!("surrogate_{}", item.id));
177 let poi_style = [
178 ("fill", "black"),
179 ("fill-opacity", "0.1"),
180 ("stroke", "black"),
181 ("stroke-width", &*format!("{stroke_width}")),
182 ("stroke-opacity", "0.8"),
183 ];
184 let ff_style = [
185 ("fill", "none"),
186 ("stroke", "black"),
187 ("stroke-width", &*format!("{stroke_width}")),
188 ("stroke-opacity", "0.8"),
189 ];
190 let no_ff_style = [
191 ("fill", "none"),
192 ("stroke", "black"),
193 ("stroke-width", &*format!("{stroke_width}")),
194 ("stroke-opacity", "0.5"),
195 ("stroke-dasharray", &*format!("{}", 5.0 * stroke_width)),
196 ("stroke-linecap", "round"),
197 ("stroke-linejoin", "round"),
198 ];
199
200 let surrogate = item.shape_cd.surrogate();
201 let poi = &surrogate.poles[0];
202 let ff_poles = surrogate.ff_poles();
203
204 for pole in surrogate.poles.iter() {
205 if pole == poi {
206 let svg_circle =
207 svg_util::circle(pole.transform_clone(&int_transf), &poi_style);
208 surrogate_group = surrogate_group.add(svg_circle);
209 } else if ff_poles.contains(pole) {
210 let svg_circle =
211 svg_util::circle(pole.transform_clone(&int_transf), &ff_style);
212 surrogate_group = surrogate_group.add(svg_circle);
213 } else {
214 let svg_circle =
215 svg_util::circle(pole.transform_clone(&int_transf), &no_ff_style);
216 surrogate_group = surrogate_group.add(svg_circle);
217 }
218 }
219 for pier in &surrogate.piers {
220 surrogate_group = surrogate_group.add(svg_util::data_to_path(
221 svg_util::edge_data(pier.transform_clone(&int_transf)),
222 &ff_style,
223 ));
224 }
225 surrogate_defs = surrogate_defs.add(surrogate_group)
226 }
227
228 if options.highlight_cd_shapes {
229 let t_shape_cd = item.shape_cd.transform_clone(&int_transf);
230 let svg_cd_shape = svg_util::data_to_path(
232 svg_util::simple_polygon_data(&t_shape_cd),
233 highlight_cd_shape_style,
234 )
235 .set("id", format!("cd_shape_{}", item.id));
236 item_defs = item_defs.add(svg_cd_shape);
237 }
238 }
239 let mut items_group = Group::new().set("id", "items").add(item_defs);
240 let mut surrogate_group = Group::new().set("id", "surrogates").add(surrogate_defs);
241 let mut highlight_cd_shapes_group = Group::new().set("id", "highlight_cd_shapes");
242
243 for pi in layout.placed_items().values() {
244 let dtransf = match options.draw_cd_shapes {
245 true => pi.d_transf,
246 false => {
247 let item = instance.item(pi.item_id);
248 int_to_ext_transformation(&pi.d_transf, &item.shape_orig.pre_transform)
249 }
250 };
251 let title = Title::new(format!("item, id: {}, transf: [{}]", pi.item_id, dtransf));
252 let pi_ref = Use::new()
253 .set("transform", transform_to_svg(dtransf))
254 .set("xlink:href", format!("#item_{}", pi.item_id))
255 .add(title);
256
257 items_group = items_group.add(pi_ref);
258
259 if options.surrogate {
260 let pi_surr_ref = Use::new()
261 .set("transform", transform_to_svg(dtransf))
262 .set("xlink:href", format!("#surrogate_{}", pi.item_id));
263
264 surrogate_group = surrogate_group.add(pi_surr_ref);
265 }
266 if options.highlight_cd_shapes {
267 let pi_cd_ref = Use::new()
268 .set("transform", transform_to_svg(dtransf))
269 .set("xlink:href", format!("#cd_shape_{}", pi.item_id));
270 highlight_cd_shapes_group = highlight_cd_shapes_group.add(pi_cd_ref);
271 }
272 }
273
274 (items_group, surrogate_group, highlight_cd_shapes_group)
275 };
276
277 let qt_group = match options.quadtree {
279 false => None,
280 true => {
281 let qt_data = svg_util::quad_tree_data(layout.cde().quadtree(), &NoHazardFilter);
282 let qt_group = Group::new()
283 .set("id", "quadtree")
284 .add(svg_util::data_to_path(
285 qt_data.0,
286 &[
287 ("fill", "red"),
288 ("stroke-width", &*format!("{}", stroke_width * 0.25)),
289 ("fill-rule", "nonzero"),
290 ("fill-opacity", "0.6"),
291 ("stroke", "black"),
292 ],
293 ))
294 .add(svg_util::data_to_path(
295 qt_data.1,
296 &[
297 ("fill", "none"),
298 ("stroke-width", &*format!("{}", stroke_width * 0.25)),
299 ("fill-rule", "nonzero"),
300 ("fill-opacity", "0.3"),
301 ("stroke", "black"),
302 ],
303 ))
304 .add(svg_util::data_to_path(
305 qt_data.2,
306 &[
307 ("fill", "green"),
308 ("fill-opacity", "0.6"),
309 ("stroke-width", &*format!("{}", stroke_width * 0.25)),
310 ("stroke", "black"),
311 ],
312 ));
313 Some(qt_group)
314 }
315 };
316
317 let collision_group = match options.highlight_collisions {
319 false => None,
320 true => {
321 let mut collision_group = Group::new().set("id", "collision_lines");
322 for (pk, pi) in layout.placed_items().iter() {
323 let detector = {
324 let mut detector = BasicHazardDetector::new();
325 layout
326 .cde()
327 .collect_poly_collisions(pi.shape.as_ref(), &mut detector);
328 detector.remove(&HazardEntity::from((pk, pi)));
329 detector
330 };
331 for haz_entity in detector.iter() {
332 match haz_entity {
333 HazardEntity::PlacedItem {
334 pk: colliding_pk, ..
335 } => {
336 let haz_hash = {
337 let mut hasher = DefaultHasher::new();
338 haz_entity.hash(&mut hasher);
339 hasher.finish()
340 };
341 let pi_hash = {
342 let mut hasher = DefaultHasher::new();
343 HazardEntity::from((pk, pi)).hash(&mut hasher);
344 hasher.finish()
345 };
346
347 if haz_hash < pi_hash {
348 let start = pi.shape.poi.center;
350 let end = layout.placed_items[*colliding_pk].shape.poi.center;
351 collision_group = collision_group.add(svg_util::data_to_path(
352 svg_util::edge_data(Edge { start, end }),
353 &[
354 (
355 "stroke",
356 &*format!("{}", theme.collision_highlight_color),
357 ),
358 ("stroke-opacity", "0.75"),
359 ("stroke-width", &*format!("{}", stroke_width * 4.0)),
360 (
361 "stroke-dasharray",
362 &*format!(
363 "{} {}",
364 4.0 * stroke_width,
365 8.0 * stroke_width
366 ),
367 ),
368 ("stroke-linecap", "round"),
369 ("stroke-linejoin", "round"),
370 ],
371 ));
372 }
373 }
374 HazardEntity::Exterior => {
375 collision_group = collision_group.add(svg_util::point(
376 pi.shape.poi.center,
377 Some(&*format!("{}", theme.collision_highlight_color)),
378 Some(3.0 * stroke_width),
379 ));
380 }
381 _ => {
382 warn!("unexpected hazard entity");
383 }
384 }
385 }
386 }
387 Some(collision_group)
388 }
389 };
390
391 let vbox_svg = (vbox.x_min, vbox.y_min, vbox.width(), vbox.height());
392
393 let optionals = [
394 Some(highlight_cd_shape_group),
395 Some(surrogate_group),
396 qt_group,
397 collision_group,
398 ]
399 .into_iter()
400 .flatten()
401 .fold(Group::new().set("id", "optionals"), |g, opt| g.add(opt));
402
403 Document::new()
404 .set("viewBox", vbox_svg)
405 .set("xmlns:xlink", "http://www.w3.org/1999/xlink")
406 .add(container_group)
407 .add(items_group)
408 .add(qz_group)
409 .add(optionals)
410 .add(label)
411}
412fn transform_to_svg(dt: DTransformation) -> String {
413 let (tx, ty) = dt.translation();
416 let r = dt.rotation().to_degrees();
417 format!("translate({tx} {ty}), rotate({r})")
418}