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