Skip to main content

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::GeometryOutput};
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        let geometry = &output.geometry;
158        let node_attr = match output.local_matrix {
159            Some(matrix) => node_attr
160                .clone()
161                .insert(SvgTagAttribute::Transform(mat4_to_mat3(&matrix))),
162            None => node_attr.clone(),
163        };
164
165        match geometry {
166            Some(GeometryOutput::Geometry2D(geometry)) => {
167                writer.begin_group(&node_attr)?;
168                geometry.write_svg_mapped(writer, attr)?;
169                writer.end_group()
170            }
171            None => self_
172                .children()
173                .try_for_each(|model| model.write_svg(writer, attr)),
174            _ => Ok(()),
175        }
176    }
177}
178
179/// A struct for drawing a centered text.
180pub struct CenteredText {
181    /// The actual text.
182    pub text: String,
183    /// Bounding rectangle
184    pub rect: Rect,
185    /// Font size in mm.
186    pub font_size: Scalar,
187}
188
189impl WriteSvg for CenteredText {
190    fn write_svg(&self, writer: &mut SvgWriter, attr: &SvgTagAttributes) -> std::io::Result<()> {
191        let (x, y) = self.rect.center().x_y();
192        writer.open_tag(
193            format!(r#"text x="{x}" y="{y}" dominant-baseline="middle" text-anchor="middle""#,)
194                .as_str(),
195            attr,
196        )?;
197        writer.with_indent(&self.text)?;
198        writer.close_tag("text")
199    }
200}
201
202impl WriteSvgMapped for CenteredText {}
203
204/// A struct for drawing a grid.
205pub struct Grid {
206    /// Grid bounds.
207    pub bounds: Bounds2D,
208
209    /// Grid cell size.
210    pub cell_size: Size2,
211}
212
213impl Default for Grid {
214    fn default() -> Self {
215        Self {
216            bounds: Bounds2D::default(),
217            cell_size: Size2 {
218                width: 10.0,
219                height: 10.0,
220            },
221        }
222    }
223}
224
225impl WriteSvg for Grid {
226    fn write_svg(&self, writer: &mut SvgWriter, attr: &SvgTagAttributes) -> std::io::Result<()> {
227        let rect = self.bounds.rect().unwrap_or(writer.canvas().rect);
228        writer.begin_group(&attr.clone().insert(SvgTagAttribute::class("grid-stroke")))?;
229
230        rect.write_svg(writer, &SvgTagAttributes::default())?;
231
232        let mut left = rect.min().x;
233        let right = rect.max().x;
234        while left <= right {
235            Line(
236                geo::Point::new(left, rect.min().y),
237                geo::Point::new(left, rect.max().y),
238            )
239            .write_svg(writer, &SvgTagAttributes::default())?;
240            left += self.cell_size.width.map_to_canvas(writer.canvas());
241        }
242
243        let mut bottom = rect.min().y;
244        let top = rect.max().y;
245        while bottom <= top {
246            Line(
247                geo::Point::new(rect.min().x, bottom),
248                geo::Point::new(rect.max().x, bottom),
249            )
250            .write_svg(writer, &SvgTagAttributes::default())?;
251            bottom += self.cell_size.height.map_to_canvas(writer.canvas());
252        }
253
254        writer.end_group()?;
255
256        Ok(())
257    }
258}
259
260/// A struct for drawing a background.
261pub struct Background;
262
263impl WriteSvg for Background {
264    fn write_svg(&self, writer: &mut SvgWriter, attr: &SvgTagAttributes) -> std::io::Result<()> {
265        let x = 0;
266        let y = 0;
267        let width = writer.canvas().size.width;
268        let height = writer.canvas().size.height;
269
270        writer.tag(
271            &format!("rect class=\"background-fill\" x=\"{x}\" y=\"{y}\" width=\"{width}\" height=\"{height}\""),
272            attr,
273        )
274    }
275}
276
277/// A measure to measure a length of an edge.
278pub struct EdgeLengthMeasure {
279    // Optional name for this measure.
280    name: Option<String>,
281    // Original Length
282    length: Scalar,
283    // Edge.
284    edge: Line,
285    // Offset (default = 10mm).
286    offset: Scalar,
287}
288
289impl EdgeLengthMeasure {
290    /// Height measure of a rect.
291    pub fn height(rect: &Rect, offset: Scalar, name: Option<&str>) -> Self {
292        let edge = Line(
293            geo::Point::new(rect.min().x, rect.min().y),
294            geo::Point::new(rect.min().x, rect.max().y),
295        );
296        Self {
297            name: name.map(|s| s.into()),
298            length: edge.vec().magnitude(),
299            edge,
300            offset: -offset,
301        }
302    }
303
304    /// Width measure of a rect.
305    pub fn width(rect: &Rect, offset: Scalar, name: Option<&str>) -> Self {
306        let edge = Line(
307            geo::Point::new(rect.min().x, rect.min().y),
308            geo::Point::new(rect.max().x, rect.min().y),
309        );
310
311        Self {
312            name: name.map(|s| s.into()),
313            length: edge.vec().magnitude(),
314            edge,
315            offset,
316        }
317    }
318}
319
320impl MapToCanvas for EdgeLengthMeasure {
321    fn map_to_canvas(&self, canvas: &Canvas) -> Self {
322        Self {
323            name: self.name.clone(),
324            length: self.length,
325            edge: self.edge.map_to_canvas(canvas),
326            offset: self.offset.map_to_canvas(canvas),
327        }
328    }
329}
330
331impl WriteSvg for EdgeLengthMeasure {
332    fn write_svg(&self, writer: &mut SvgWriter, attr: &SvgTagAttributes) -> std::io::Result<()> {
333        let edge_length = self.edge.vec().magnitude();
334
335        use attributes::SvgTagAttribute::*;
336
337        writer.begin_group(&attr.clone().insert(Transform(self.edge.matrix())))?;
338
339        let center = self.offset / 2.0;
340        let bottom_left = Point::new(0.0, 0.0);
341        let bottom_right = Point::new(edge_length, 0.0);
342        let top_left = Point::new(0.0, center);
343        let top_right = Point::new(edge_length, center);
344
345        writer.begin_group(&attr.clone().insert(SvgTagAttribute::class("measure")))?;
346        Line(bottom_left, Point::new(0.0, center * 1.5)).write_svg(writer, attr)?;
347        Line(bottom_right, Point::new(edge_length, center * 1.5)).write_svg(writer, attr)?;
348        Line(top_left, top_right).shorter(1.5).write_svg(
349            writer,
350            &attr
351                .clone()
352                .insert(MarkerStart("arrow".into()))
353                .insert(MarkerEnd("arrow".into())),
354        )?;
355        writer.end_group()?;
356
357        CenteredText {
358            text: format!(
359                "{name}{length:.2}mm",
360                name = match &self.name {
361                    Some(name) => format!("{name} = "),
362                    None => String::new(),
363                },
364                length = self.length
365            ),
366            rect: Rect::new(bottom_left, top_right).translate(0.0, center),
367            font_size: 2.0,
368        }
369        .write_svg(writer, &SvgTagAttribute::class("measure-fill").into())?;
370
371        writer.end_group()
372    }
373}
374
375impl WriteSvgMapped for EdgeLengthMeasure {}
376
377/// A radius measure with an offset.
378pub struct RadiusMeasure {
379    /// Circle to measure.
380    pub circle: Circle,
381    /// Original radius to measure.
382    pub radius: Scalar,
383    /// Name of this measurement.
384    pub name: Option<String>,
385    /// Angle of the measurement.
386    pub angle: Deg<Scalar>,
387}
388
389impl RadiusMeasure {
390    /// Create new radius measure.
391    pub fn new(circle: Circle, name: Option<String>, angle: Option<Deg<Scalar>>) -> Self {
392        Self {
393            radius: circle.radius,
394            circle,
395            name,
396            angle: angle.unwrap_or(Deg(-45.0)),
397        }
398    }
399}
400
401impl MapToCanvas for RadiusMeasure {
402    fn map_to_canvas(&self, canvas: &Canvas) -> Self {
403        Self {
404            radius: self.radius,
405            circle: self.circle.map_to_canvas(canvas),
406            name: self.name.clone(),
407            angle: self.angle,
408        }
409    }
410}
411
412impl WriteSvg for RadiusMeasure {
413    fn write_svg(&self, writer: &mut SvgWriter, attr: &SvgTagAttributes) -> std::io::Result<()> {
414        writer.begin_group(attr)?;
415
416        let edge = Line::radius_edge(&self.circle, &self.angle.into());
417        edge.shorter(1.5).write_svg(
418            writer,
419            &attr
420                .clone()
421                .insert(SvgTagAttribute::MarkerEnd("arrow".into()))
422                .insert(SvgTagAttribute::class("measure")),
423        )?;
424        let center = edge.center();
425
426        CenteredText {
427            text: format!(
428                "{name}{radius:.2}mm",
429                name = match &self.name {
430                    Some(name) => format!("{name} = "),
431                    None => String::new(),
432                },
433                radius = self.radius,
434            ),
435            rect: Rect::new(center, center),
436            font_size: 2.0,
437        }
438        .write_svg(writer, &SvgTagAttribute::class("measure-fill").into())?;
439
440        writer.end_group()?;
441
442        Ok(())
443    }
444}
445
446impl WriteSvgMapped for RadiusMeasure {}
447
448/// Size measure of a bounds.
449pub struct SizeMeasure {
450    bounds: Bounds2D,
451    /// Width measure
452    width: Option<EdgeLengthMeasure>,
453    /// Height measure
454    height: Option<EdgeLengthMeasure>,
455}
456
457impl SizeMeasure {
458    /// Size measure for something that has bounds.
459    pub fn bounds<T: CalcBounds2D>(bounds: &T) -> Self {
460        let bounds = bounds.calc_bounds_2d();
461
462        if let Some(rect) = bounds.rect() {
463            Self {
464                bounds: bounds.clone(),
465                width: Some(EdgeLengthMeasure::width(&rect, 7.0, None)),
466                height: Some(EdgeLengthMeasure::height(&rect, 7.0, None)),
467            }
468        } else {
469            Self {
470                bounds: bounds.clone(),
471                width: None,
472                height: None,
473            }
474        }
475    }
476}
477
478impl MapToCanvas for SizeMeasure {
479    fn map_to_canvas(&self, canvas: &Canvas) -> Self {
480        Self {
481            bounds: self.bounds.map_to_canvas(canvas),
482            width: self.width.as_ref().map(|width| width.map_to_canvas(canvas)),
483            height: self
484                .height
485                .as_ref()
486                .map(|height| height.map_to_canvas(canvas)),
487        }
488    }
489}
490
491impl WriteSvg for SizeMeasure {
492    fn write_svg(&self, writer: &mut SvgWriter, attr: &SvgTagAttributes) -> std::io::Result<()> {
493        if let Some(width) = &self.width {
494            width.write_svg(writer, attr)?;
495        }
496        if let Some(height) = &self.height {
497            height.write_svg(writer, attr)?;
498        }
499        Ok(())
500    }
501}
502
503impl WriteSvgMapped for SizeMeasure {}