microcad_export/svg/
primitives.rs

1// Copyright © 2025 The µcad authors <info@ucad.xyz>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4//! Scalable Vector Graphics (SVG) primitives ([`WriteSvg`] trait implementations).
5
6use cgmath::{Deg, InnerSpace};
7use geo::{CoordsIter as _, Point, Rect, Translate};
8use microcad_core::*;
9use microcad_lang::{model::Model, render::RenderOutput};
10
11use crate::svg::{attributes::SvgTagAttribute, *};
12
13impl WriteSvg for Line {
14    fn write_svg(&self, writer: &mut SvgWriter, attr: &SvgTagAttributes) -> std::io::Result<()> {
15        let ((x1, y1), (x2, y2)) = (self.0.x_y(), self.1.x_y());
16        writer.tag(
17            &format!("line x1=\"{x1}\" y1=\"{y1}\" x2=\"{x2}\" y2=\"{y2}\"",),
18            attr,
19        )
20    }
21}
22
23impl WriteSvgMapped for Line {}
24
25impl WriteSvg for Rect {
26    fn write_svg(&self, writer: &mut SvgWriter, attr: &SvgTagAttributes) -> std::io::Result<()> {
27        let x = self.min().x;
28        let y = self.min().y;
29        let width = self.width();
30        let height = self.height();
31
32        writer.tag(
33            &format!("rect x=\"{x}\" y=\"{y}\" width=\"{width}\" height=\"{height}\""),
34            attr,
35        )
36    }
37}
38
39impl WriteSvgMapped for Rect {}
40
41impl WriteSvg for Bounds2D {
42    fn write_svg(&self, writer: &mut SvgWriter, attr: &SvgTagAttributes) -> std::io::Result<()> {
43        if let Some(rect) = self.rect() {
44            rect.write_svg(writer, attr)
45        } else {
46            Ok(())
47        }
48    }
49}
50
51impl WriteSvgMapped for Bounds2D {}
52
53impl WriteSvg for Circle {
54    fn write_svg(&self, writer: &mut SvgWriter, attr: &SvgTagAttributes) -> std::io::Result<()> {
55        let r = self.radius;
56        let (cx, cy) = (self.offset.x, self.offset.y);
57        writer.tag(&format!("circle cx=\"{cx}\" cy=\"{cy}\" r=\"{r}\""), attr)
58    }
59}
60
61impl WriteSvgMapped for Circle {}
62
63impl WriteSvg for LineString {
64    fn write_svg(&self, writer: &mut SvgWriter, attr: &SvgTagAttributes) -> std::io::Result<()> {
65        let points = self.coords().fold(String::new(), |acc, p| {
66            acc + &format!("{x},{y} ", x = p.x, y = p.y)
67        });
68        writer.tag(&format!("polyline points=\"{points}\""), attr)
69    }
70}
71
72impl WriteSvgMapped for LineString {}
73
74impl WriteSvg for MultiLineString {
75    fn write_svg(&self, writer: &mut SvgWriter, attr: &SvgTagAttributes) -> std::io::Result<()> {
76        self.iter()
77            .try_for_each(|line_string| line_string.write_svg(writer, attr))
78    }
79}
80
81impl WriteSvgMapped for MultiLineString {}
82
83impl WriteSvg for Polygon {
84    fn write_svg(&self, writer: &mut SvgWriter, attr: &SvgTagAttributes) -> std::io::Result<()> {
85        fn line_string_path(l: &geo2d::LineString) -> String {
86            l.points()
87                .enumerate()
88                .fold(String::new(), |acc, (i, point)| {
89                    let (x, y) = point.x_y();
90                    let mut s = String::new();
91                    s += if i == 0 { "M" } else { "L" };
92                    s += &format!("{x},{y}");
93                    if i == l.coords_count() - 1 {
94                        s += " Z ";
95                    }
96                    acc + &s
97                })
98        }
99
100        let exterior = line_string_path(self.exterior());
101        let interior = self
102            .interiors()
103            .iter()
104            .map(line_string_path)
105            .fold(String::new(), |acc, s| acc + &s);
106
107        writer.tag(&format!("path d=\"{exterior} {interior}\""), attr)
108    }
109}
110
111impl WriteSvgMapped for Polygon {}
112
113impl WriteSvg for MultiPolygon {
114    fn write_svg(&self, writer: &mut SvgWriter, attr: &SvgTagAttributes) -> std::io::Result<()> {
115        self.iter()
116            .try_for_each(|polygon| polygon.write_svg(writer, attr))
117    }
118}
119
120impl WriteSvgMapped for MultiPolygon {}
121
122impl WriteSvg for Geometries2D {
123    fn write_svg(&self, writer: &mut SvgWriter, attr: &SvgTagAttributes) -> std::io::Result<()> {
124        self.iter().try_for_each(|geo| geo.write_svg(writer, attr))
125    }
126}
127
128impl WriteSvgMapped for Geometries2D {}
129
130impl WriteSvg for Geometry2D {
131    fn write_svg(&self, writer: &mut SvgWriter, attr: &SvgTagAttributes) -> std::io::Result<()> {
132        match self {
133            Geometry2D::LineString(line_string) => line_string.write_svg(writer, attr),
134            Geometry2D::MultiLineString(multi_line_string) => {
135                multi_line_string.write_svg(writer, attr)
136            }
137            Geometry2D::Polygon(polygon) => polygon.write_svg(writer, attr),
138            Geometry2D::MultiPolygon(multi_polygon) => multi_polygon.write_svg(writer, attr),
139            Geometry2D::Rect(rect) => rect.write_svg(writer, attr),
140            Geometry2D::Line(edge) => edge.write_svg(writer, attr),
141            Geometry2D::Collection(collection) => collection.write_svg(writer, attr),
142        }
143    }
144}
145
146impl WriteSvgMapped for Geometry2D {}
147
148impl WriteSvg for Model {
149    fn write_svg(&self, writer: &mut SvgWriter, attr: &SvgTagAttributes) -> std::io::Result<()> {
150        let node_attr = attr
151            .clone()
152            .apply_from_model(self)
153            .insert(SvgTagAttribute::class("entity"));
154
155        let self_ = self.borrow();
156        let output = self_.output();
157        match output {
158            RenderOutput::Geometry2D {
159                local_matrix,
160                geometry,
161                ..
162            } => {
163                let node_attr = match local_matrix {
164                    Some(matrix) => node_attr
165                        .clone()
166                        .insert(SvgTagAttribute::Transform(*matrix)),
167                    None => node_attr.clone(),
168                };
169
170                writer.begin_group(&node_attr)?;
171
172                match geometry {
173                    Some(geometry) => {
174                        geometry.write_svg_mapped(writer, attr)?;
175                    }
176                    None => {
177                        self_
178                            .children()
179                            .try_for_each(|model| model.write_svg(writer, attr))?;
180                    }
181                }
182
183                writer.end_group()
184            }
185            _ => Ok(()),
186        }
187    }
188}
189
190/// A struct for drawing a centered text.
191pub struct CenteredText {
192    /// The actual text.
193    pub text: String,
194    /// Bounding rectangle
195    pub rect: Rect,
196    /// Font size in mm.
197    pub font_size: Scalar,
198}
199
200impl WriteSvg for CenteredText {
201    fn write_svg(&self, writer: &mut SvgWriter, attr: &SvgTagAttributes) -> std::io::Result<()> {
202        let (x, y) = self.rect.center().x_y();
203        writer.open_tag(
204            format!(r#"text x="{x}" y="{y}" dominant-baseline="middle" text-anchor="middle""#,)
205                .as_str(),
206            attr,
207        )?;
208        writer.with_indent(&self.text)?;
209        writer.close_tag("text")
210    }
211}
212
213impl WriteSvgMapped for CenteredText {}
214
215/// A struct for drawing a grid.
216pub struct Grid {
217    /// Grid bounds.
218    pub bounds: Bounds2D,
219
220    /// Grid cell size.
221    pub cell_size: Size2,
222}
223
224impl Default for Grid {
225    fn default() -> Self {
226        Self {
227            bounds: Bounds2D::default(),
228            cell_size: Size2 {
229                width: 10.0,
230                height: 10.0,
231            },
232        }
233    }
234}
235
236impl WriteSvg for Grid {
237    fn write_svg(&self, writer: &mut SvgWriter, attr: &SvgTagAttributes) -> std::io::Result<()> {
238        let rect = self.bounds.rect().unwrap_or(writer.canvas().rect);
239        writer.begin_group(&attr.clone().insert(SvgTagAttribute::class("grid-stroke")))?;
240
241        rect.write_svg(writer, &SvgTagAttributes::default())?;
242
243        let mut left = rect.min().x;
244        let right = rect.max().x;
245        while left <= right {
246            Line(
247                geo::Point::new(left, rect.min().y),
248                geo::Point::new(left, rect.max().y),
249            )
250            .write_svg(writer, &SvgTagAttributes::default())?;
251            left += self.cell_size.width.map_to_canvas(writer.canvas());
252        }
253
254        let mut bottom = rect.min().y;
255        let top = rect.max().y;
256        while bottom <= top {
257            Line(
258                geo::Point::new(rect.min().x, bottom),
259                geo::Point::new(rect.max().x, bottom),
260            )
261            .write_svg(writer, &SvgTagAttributes::default())?;
262            bottom += self.cell_size.height.map_to_canvas(writer.canvas());
263        }
264
265        writer.end_group()?;
266
267        Ok(())
268    }
269}
270
271/// A struct for drawing a background.
272pub struct Background;
273
274impl WriteSvg for Background {
275    fn write_svg(&self, writer: &mut SvgWriter, attr: &SvgTagAttributes) -> std::io::Result<()> {
276        let x = 0;
277        let y = 0;
278        let width = writer.canvas().size.width;
279        let height = writer.canvas().size.height;
280
281        writer.tag(
282            &format!("rect class=\"background-fill\" x=\"{x}\" y=\"{y}\" width=\"{width}\" height=\"{height}\""),
283            attr,
284        )
285    }
286}
287
288/// A measure to measure a length of an edge.
289pub struct EdgeLengthMeasure {
290    // Optional name for this measure.
291    name: Option<String>,
292    // Original Length
293    length: Scalar,
294    // Edge.
295    edge: Line,
296    // Offset (default = 10mm).
297    offset: Scalar,
298}
299
300impl EdgeLengthMeasure {
301    /// Height measure of a rect.
302    pub fn height(rect: &Rect, offset: Scalar, name: Option<&str>) -> Self {
303        let edge = Line(
304            geo::Point::new(rect.min().x, rect.min().y),
305            geo::Point::new(rect.min().x, rect.max().y),
306        );
307        Self {
308            name: name.map(|s| s.into()),
309            length: edge.vec().magnitude(),
310            edge,
311            offset: -offset,
312        }
313    }
314
315    /// Width measure of a rect.
316    pub fn width(rect: &Rect, offset: Scalar, name: Option<&str>) -> Self {
317        let edge = Line(
318            geo::Point::new(rect.min().x, rect.min().y),
319            geo::Point::new(rect.max().x, rect.min().y),
320        );
321
322        Self {
323            name: name.map(|s| s.into()),
324            length: edge.vec().magnitude(),
325            edge,
326            offset,
327        }
328    }
329}
330
331impl MapToCanvas for EdgeLengthMeasure {
332    fn map_to_canvas(&self, canvas: &Canvas) -> Self {
333        Self {
334            name: self.name.clone(),
335            length: self.length,
336            edge: self.edge.map_to_canvas(canvas),
337            offset: self.offset.map_to_canvas(canvas),
338        }
339    }
340}
341
342impl WriteSvg for EdgeLengthMeasure {
343    fn write_svg(&self, writer: &mut SvgWriter, attr: &SvgTagAttributes) -> std::io::Result<()> {
344        let edge_length = self.edge.vec().magnitude();
345
346        use attributes::SvgTagAttribute::*;
347
348        writer.begin_group(&attr.clone().insert(Transform(self.edge.matrix())))?;
349
350        let center = self.offset / 2.0;
351        let bottom_left = Point::new(0.0, 0.0);
352        let bottom_right = Point::new(edge_length, 0.0);
353        let top_left = Point::new(0.0, center);
354        let top_right = Point::new(edge_length, center);
355
356        writer.begin_group(&attr.clone().insert(SvgTagAttribute::class("measure")))?;
357        Line(bottom_left, Point::new(0.0, center * 1.5)).write_svg(writer, attr)?;
358        Line(bottom_right, Point::new(edge_length, center * 1.5)).write_svg(writer, attr)?;
359        Line(top_left, top_right).shorter(1.5).write_svg(
360            writer,
361            &attr
362                .clone()
363                .insert(MarkerStart("arrow".into()))
364                .insert(MarkerEnd("arrow".into())),
365        )?;
366        writer.end_group()?;
367
368        CenteredText {
369            text: format!(
370                "{name}{length:.2}mm",
371                name = match &self.name {
372                    Some(name) => format!("{name} = "),
373                    None => String::new(),
374                },
375                length = self.length
376            ),
377            rect: Rect::new(bottom_left, top_right).translate(0.0, center),
378            font_size: 2.0,
379        }
380        .write_svg(writer, &SvgTagAttribute::class("measure-fill").into())?;
381
382        writer.end_group()
383    }
384}
385
386impl WriteSvgMapped for EdgeLengthMeasure {}
387
388/// A radius measure with an offset.
389pub struct RadiusMeasure {
390    /// Circle to measure.
391    pub circle: Circle,
392    /// Original radius to measure.
393    pub radius: Scalar,
394    /// Name of this measurement.
395    pub name: Option<String>,
396    /// Angle of the measurement.
397    pub angle: Deg<Scalar>,
398}
399
400impl RadiusMeasure {
401    /// Create new radius measure.
402    pub fn new(circle: Circle, name: Option<String>, angle: Option<Deg<Scalar>>) -> Self {
403        Self {
404            radius: circle.radius,
405            circle,
406            name,
407            angle: angle.unwrap_or(Deg(-45.0)),
408        }
409    }
410}
411
412impl MapToCanvas for RadiusMeasure {
413    fn map_to_canvas(&self, canvas: &Canvas) -> Self {
414        Self {
415            radius: self.radius,
416            circle: self.circle.map_to_canvas(canvas),
417            name: self.name.clone(),
418            angle: self.angle,
419        }
420    }
421}
422
423impl WriteSvg for RadiusMeasure {
424    fn write_svg(&self, writer: &mut SvgWriter, attr: &SvgTagAttributes) -> std::io::Result<()> {
425        writer.begin_group(attr)?;
426
427        let edge = Line::radius_edge(&self.circle, &self.angle.into());
428        edge.shorter(1.5).write_svg(
429            writer,
430            &attr
431                .clone()
432                .insert(SvgTagAttribute::MarkerEnd("arrow".into()))
433                .insert(SvgTagAttribute::class("measure")),
434        )?;
435        let center = edge.center();
436
437        CenteredText {
438            text: format!(
439                "{name}{radius:.2}mm",
440                name = match &self.name {
441                    Some(name) => format!("{name} = "),
442                    None => String::new(),
443                },
444                radius = self.radius,
445            ),
446            rect: Rect::new(center, center),
447            font_size: 2.0,
448        }
449        .write_svg(writer, &SvgTagAttribute::class("measure-fill").into())?;
450
451        writer.end_group()?;
452
453        Ok(())
454    }
455}
456
457impl WriteSvgMapped for RadiusMeasure {}
458
459/// Size measure of a bounds.
460pub struct SizeMeasure {
461    bounds: Bounds2D,
462    /// Width measure
463    width: Option<EdgeLengthMeasure>,
464    /// Height measure
465    height: Option<EdgeLengthMeasure>,
466}
467
468impl SizeMeasure {
469    /// Size measure for something that has bounds.
470    pub fn bounds<T: CalcBounds2D>(bounds: &T) -> Self {
471        let bounds = bounds.calc_bounds_2d();
472
473        if let Some(rect) = bounds.rect() {
474            Self {
475                bounds: bounds.clone(),
476                width: Some(EdgeLengthMeasure::width(&rect, 7.0, None)),
477                height: Some(EdgeLengthMeasure::height(&rect, 7.0, None)),
478            }
479        } else {
480            Self {
481                bounds: bounds.clone(),
482                width: None,
483                height: None,
484            }
485        }
486    }
487}
488
489impl MapToCanvas for SizeMeasure {
490    fn map_to_canvas(&self, canvas: &Canvas) -> Self {
491        Self {
492            bounds: self.bounds.map_to_canvas(canvas),
493            width: self.width.as_ref().map(|width| width.map_to_canvas(canvas)),
494            height: self
495                .height
496                .as_ref()
497                .map(|height| height.map_to_canvas(canvas)),
498        }
499    }
500}
501
502impl WriteSvg for SizeMeasure {
503    fn write_svg(&self, writer: &mut SvgWriter, attr: &SvgTagAttributes) -> std::io::Result<()> {
504        if let Some(width) = &self.width {
505            width.write_svg(writer, attr)?;
506        }
507        if let Some(height) = &self.height {
508            height.write_svg(writer, attr)?;
509        }
510        Ok(())
511    }
512}
513
514impl WriteSvgMapped for SizeMeasure {}