microcad_export/svg/
exporter.rs

1// Copyright © 2024-2025 The µcad authors <info@ucad.xyz>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4//! Scalable Vector Graphics (SVG) export
5
6use microcad_core::{Color, Scalar};
7use microcad_lang::{Id, builtin::*, model::*, parameter, render::RenderError, value::*};
8
9/// SVG Exporter.
10pub struct SvgExporter;
11
12/// A theme for SVG export.
13#[derive(Clone, Debug, PartialEq)]
14pub struct Theme {
15    /// Background color of the drawing canvas.
16    pub background: Color,
17    /// Color used for grid lines.
18    pub grid: Color,
19    /// Color used for selected entities.
20    pub selection: Color,
21    /// Color used for highlighting hovered entities.
22    pub highlight: Color,
23    /// Default color for entities.
24    pub entity: Color,
25    /// Default color for entity outlines.
26    pub outline: Color,
27    /// Color used for active construction lines.
28    pub active: Color,
29    /// Color used for inactive construction lines.
30    pub inactive: Color,
31    /// Color for dimensions and annotations.
32    pub measure: Color,
33    /// Color for snapping indicators.
34    pub snap_indicator: Color,
35    /// Color for guidelines (e.g. inference lines).
36    pub guide: Color,
37}
38
39impl Default for Theme {
40    fn default() -> Self {
41        Self {
42            background: Color::rgb(1.0, 1.0, 1.0),
43            grid: Color::rgb(0.85, 0.85, 0.85),
44            selection: Color::rgb(0.0, 0.4, 0.8),
45            highlight: Color::rgb(1.0, 0.6, 0.0),
46            entity: Color::rgba(0.7, 0.7, 0.7, 0.7),
47            outline: Color::rgb(0.1, 0.1, 0.1),
48            active: Color::rgb(0.2, 0.2, 0.2),
49            inactive: Color::rgb(0.8, 0.8, 0.8),
50            measure: Color::rgb(0.0, 0.8, 0.8),
51            snap_indicator: Color::rgb(0.0, 0.8, 0.8),
52            guide: Color::rgb(0.6, 0.6, 0.6),
53        }
54    }
55}
56
57/// Settings for this exporter.
58pub struct SvgExporterSettings {
59    /// Relative padding (e.g. 0.05 = 5% = padding on each side).
60    padding_factor: Scalar,
61}
62
63impl Default for SvgExporterSettings {
64    fn default() -> Self {
65        Self {
66            padding_factor: 0.05, // 5% padding on each side.
67        }
68    }
69}
70
71impl SvgExporter {
72    /// Generate SVG style string from theme.
73    pub fn theme_to_svg_style(theme: &Theme) -> String {
74        fn fill_stroke_style(
75            class_name: &str,
76            fill_color: Color,
77            stroke_color: Color,
78            stroke_width: Scalar,
79        ) -> String {
80            format!(
81                r#" 
82        .{class_name} {{
83            fill: {fill_color};
84            stroke: {stroke_color};
85            stroke-width: {stroke_width};
86        }}
87        "#,
88                fill_color = fill_color.to_svg_color(),
89                stroke_color = stroke_color.to_svg_color()
90            )
91        }
92
93        fn fill_style(class_name: &str, fill: Color) -> String {
94            format!(
95                r#" 
96        .{class_name}-fill {{
97            fill: {fill};
98            stroke: none;
99        }}
100        "#,
101                fill = fill.to_svg_color()
102            )
103        }
104
105        fn stroke_style(class_name: &str, stroke: Color, stroke_width: Scalar) -> String {
106            format!(
107                r#" 
108        .{class_name}-stroke {{
109            fill: none;
110            stroke: {stroke};
111            stroke-width: {stroke_width};
112        }}
113        "#,
114                stroke = stroke.to_svg_color()
115            )
116        }
117
118        let mut style = [
119            ("background", theme.background, None),
120            ("grid", theme.grid, Some(0.2)),
121            ("measure", theme.measure, Some(0.2)),
122            ("highlight", theme.highlight, Some(0.2)),
123        ]
124        .into_iter()
125        .fold(String::new(), |mut style, item| {
126            if let Some(stroke) = item.2 {
127                style += &fill_stroke_style(item.0, item.1, item.1, stroke);
128                style += &stroke_style(item.0, item.1, stroke)
129            }
130            style += &fill_style(item.0, item.1);
131            style
132        });
133
134        style += &fill_stroke_style("entity", theme.entity, theme.outline, 0.4);
135
136        style += r#"
137            .active { fill-opacity: 1.0; stroke-opacity: 1.0; }
138            .inactive { fill-opacity: 0.3; stroke-opacity: 0.3; }
139        "#;
140
141        style
142    }
143}
144
145impl Exporter for SvgExporter {
146    fn model_parameters(&self) -> microcad_lang::value::ParameterValueList {
147        [
148            parameter!(style: String = String::new()),
149            parameter!(fill: String = String::new()),
150        ]
151        .into_iter()
152        .collect()
153    }
154
155    fn export(&self, model: &Model, filename: &std::path::Path) -> Result<Value, ExportError> {
156        use crate::svg::*;
157        use microcad_core::CalcBounds2D;
158        let settings = SvgExporterSettings::default();
159        let bounds = model.calc_bounds_2d();
160
161        if bounds.is_valid() {
162            let content_rect = bounds
163                .enlarge(2.0 * settings.padding_factor)
164                .rect()
165                .expect("Rect");
166            log::debug!("Exporting into SVG file {filename:?}");
167            let f = std::fs::File::create(filename)?;
168            let mut writer = SvgWriter::new_canvas(
169                Box::new(std::io::BufWriter::new(f)),
170                model.get_size(),
171                content_rect,
172                None,
173            )?;
174            writer.style(&SvgExporter::theme_to_svg_style(&Theme::default()))?;
175
176            model.write_svg(&mut writer, &SvgTagAttributes::default())?;
177            Ok(Value::None)
178        } else {
179            Err(ExportError::RenderError(RenderError::NothingToRender))
180        }
181    }
182
183    fn output_type(&self) -> OutputType {
184        OutputType::Geometry2D
185    }
186}
187
188impl FileIoInterface for SvgExporter {
189    fn id(&self) -> Id {
190        Id::new("svg")
191    }
192}