microcad_export/svg/
canvas.rs

1// Copyright © 2025 The µcad authors <info@ucad.xyz>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4//! Canvas to draw geometry.
5
6use geo::MultiPolygon;
7use microcad_core::{
8    Bounds2D, Circle, Geometries2D, Geometry2D, Line, LineString, MultiLineString, Point, Polygon,
9    Rect, Scalar, Size2, Vec2, geo2d,
10};
11
12use crate::svg::CenteredText;
13
14/// A canvas coordinate system.
15#[derive(Clone, Debug)]
16pub struct Canvas {
17    /// The canvas rect.
18    pub rect: geo2d::Rect,
19    /// The content rect.
20    pub content_rect: geo2d::Rect,
21    /// Size2.
22    pub size: Size2,
23}
24
25impl Canvas {
26    /// Create a new canvas with a size and center the content.
27    pub fn new_centered_content(
28        size: Size2,
29        content_rect: geo2d::Rect,
30        scale: Option<Scalar>,
31    ) -> Self {
32        // Compute scale to fit content inside canvas (preserving aspect ratio)
33        let scale = match scale {
34            Some(scale) => scale,
35            None => {
36                let scale_x = size.width / content_rect.width();
37                let scale_y = size.height / content_rect.height();
38                scale_x.min(scale_y)
39            }
40        };
41
42        // New content size after scaling
43        let width = content_rect.width() * scale;
44        let height = content_rect.height() * scale;
45
46        // Center the content within the canvas
47        let min = Point::new((size.width - width) / 2.0, (size.height - height) / 2.0);
48
49        // Build the new canvas rect centered with content
50        let rect = geo2d::Rect::new(min, min + geo2d::Point::new(width, height));
51
52        Canvas {
53            rect,
54            content_rect,
55            size,
56        }
57    }
58
59    /// Return the ratio between canvas rect and content rect size.
60    pub fn scale(&self) -> Scalar {
61        (self.rect.width() / self.content_rect.width())
62            .min(self.rect.height() / self.content_rect.height())
63    }
64}
65
66/// Map something into a canvas coordinates.
67pub trait MapToCanvas: Sized {
68    /// Return mapped version.
69    fn map_to_canvas(&self, canvas: &Canvas) -> Self;
70}
71
72/// Scale scalar value.
73impl MapToCanvas for Scalar {
74    fn map_to_canvas(&self, canvas: &Canvas) -> Self {
75        self * canvas.scale()
76    }
77}
78
79impl MapToCanvas for (Scalar, Scalar) {
80    fn map_to_canvas(&self, canvas: &Canvas) -> Self {
81        let scale = canvas.scale();
82        let new_width = canvas.rect.width() / scale;
83        let new_height = canvas.rect.height() / scale;
84
85        let x = self.0 - canvas.content_rect.min().x;
86        let y = canvas.content_rect.max().y - self.1; // Flip Y
87        let x = x / new_width * canvas.rect.width() + canvas.rect.min().x;
88        let y = y / new_height * canvas.rect.height() + canvas.rect.min().y;
89        (x, y)
90    }
91}
92
93impl MapToCanvas for Point {
94    fn map_to_canvas(&self, canvas: &Canvas) -> Self {
95        Point::from(self.x_y().map_to_canvas(canvas))
96    }
97}
98
99impl MapToCanvas for Vec2 {
100    fn map_to_canvas(&self, canvas: &Canvas) -> Self {
101        Vec2::from((self.x, self.y).map_to_canvas(canvas))
102    }
103}
104
105impl MapToCanvas for Line {
106    fn map_to_canvas(&self, canvas: &Canvas) -> Self {
107        Self(self.0.map_to_canvas(canvas), self.1.map_to_canvas(canvas))
108    }
109}
110
111impl MapToCanvas for Rect {
112    fn map_to_canvas(&self, canvas: &Canvas) -> Self {
113        Self::new(
114            Point::from(self.min()).map_to_canvas(canvas),
115            Point::from(self.max()).map_to_canvas(canvas),
116        )
117    }
118}
119
120impl MapToCanvas for Bounds2D {
121    fn map_to_canvas(&self, canvas: &Canvas) -> Self {
122        match self.rect() {
123            Some(rect) => Self::new(
124                rect.min().x_y().map_to_canvas(canvas).into(),
125                rect.max().x_y().map_to_canvas(canvas).into(),
126            ),
127            None => Self::default(),
128        }
129    }
130}
131
132impl MapToCanvas for Circle {
133    fn map_to_canvas(&self, canvas: &Canvas) -> Self {
134        Self {
135            radius: self.radius.map_to_canvas(canvas),
136            offset: self.offset.map_to_canvas(canvas),
137        }
138    }
139}
140
141impl MapToCanvas for LineString {
142    fn map_to_canvas(&self, canvas: &Canvas) -> Self {
143        Self(
144            self.0
145                .iter()
146                .map(|p| p.x_y().map_to_canvas(canvas).into())
147                .collect(),
148        )
149    }
150}
151
152impl MapToCanvas for MultiLineString {
153    fn map_to_canvas(&self, canvas: &Canvas) -> Self {
154        self.iter()
155            .map(|line_string| line_string.map_to_canvas(canvas))
156            .collect()
157    }
158}
159
160impl MapToCanvas for Polygon {
161    fn map_to_canvas(&self, canvas: &Canvas) -> Self {
162        Self::new(
163            self.exterior().map_to_canvas(canvas),
164            self.interiors()
165                .iter()
166                .map(|line_string| line_string.map_to_canvas(canvas))
167                .collect(),
168        )
169    }
170}
171
172impl MapToCanvas for MultiPolygon {
173    fn map_to_canvas(&self, canvas: &Canvas) -> Self {
174        self.0
175            .iter()
176            .map(|polygon| polygon.map_to_canvas(canvas))
177            .collect()
178    }
179}
180
181impl MapToCanvas for Geometries2D {
182    fn map_to_canvas(&self, canvas: &Canvas) -> Self {
183        Geometries2D::new(self.iter().map(|geo| geo.map_to_canvas(canvas)).collect())
184    }
185}
186
187impl MapToCanvas for Geometry2D {
188    fn map_to_canvas(&self, canvas: &Canvas) -> Self {
189        match self {
190            Geometry2D::LineString(line_string) => {
191                Geometry2D::LineString(line_string.map_to_canvas(canvas))
192            }
193            Geometry2D::MultiLineString(multi_line_string) => {
194                Geometry2D::MultiLineString(multi_line_string.map_to_canvas(canvas))
195            }
196            Geometry2D::Polygon(polygon) => Geometry2D::Polygon(polygon.map_to_canvas(canvas)),
197            Geometry2D::MultiPolygon(multi_polygon) => {
198                Geometry2D::MultiPolygon(multi_polygon.map_to_canvas(canvas))
199            }
200            Geometry2D::Rect(rect) => Geometry2D::Rect(rect.map_to_canvas(canvas)),
201            Geometry2D::Circle(circle) => Geometry2D::Circle(circle.map_to_canvas(canvas)),
202            Geometry2D::Line(edge) => Geometry2D::Line(edge.map_to_canvas(canvas)),
203            Geometry2D::Collection(collection) => {
204                Geometry2D::Collection(collection.map_to_canvas(canvas))
205            }
206        }
207    }
208}
209
210impl MapToCanvas for CenteredText {
211    fn map_to_canvas(&self, canvas: &Canvas) -> Self {
212        CenteredText {
213            text: self.text.clone(),
214            rect: self.rect.map_to_canvas(canvas),
215            font_size: self.font_size,
216        }
217    }
218}