whiskers/
sketch.rs

1use kurbo::Affine;
2use vsvg::{
3    Document, DocumentTrait, IntoBezPathTolerance, LayerID, PageSize, Path, PathMetadata,
4    Transforms, DEFAULT_TOLERANCE,
5};
6
7/// Primary interface for drawing.
8///
9/// The [`Sketch`] type is the primary interface for drawing. To that effect, it implements the key
10/// traits [`vsvg::Draw`] and [`Transforms`].
11///
12/// # Drawing
13///
14/// The [`vsvg::Draw`] trait provide a wealth of chainable drawing functions, such as
15/// [`vsvg::Draw::circle`]:
16///
17/// ```rust
18/// # use whiskers::prelude::*;
19/// # let mut sketch = Sketch::new();
20/// sketch.circle(0.0, 0.0, 10.0).rect(10.0, 20.0, 20.0, 20.0);
21/// ```
22///
23/// In addition to the basic, primitive draw calls, the [`vsvg::Draw`] trait also provides a more
24/// flexible [`vsvg::Draw::add_path`] function that accepts any type which implements the
25/// [`IntoBezPathTolerance`] trait. This currently includes many types from the [`::kurbo`] and
26/// [`vsvg::exports::geo`] crates.
27///
28/// # Transformations
29///
30/// The [`Sketch`] type implements the [`Transforms`] trait, which provides a number of chainable
31/// affine transform functions such as [`Sketch::translate`] and [`Sketch::scale`]. In the context
32/// of a sketch, these functions modify the current transform matrix, which affects _subsequent_
33/// draw calls:
34///
35/// ```rust
36/// # use whiskers::prelude::*;
37/// # let mut sketch = Sketch::new();
38/// sketch
39///     .circle(0.0, 0.0, 10.0)  // not translated
40///     .translate(5.0, 5.0)
41///     .circle(0.0, 0.0, 10.0); // translated
42/// ```
43///
44/// The [`Sketch`] type also maintains a stack of transform matrices, to make it easy to save and
45/// restore current transform matrix with the [`Sketch::push_matrix`] and [`Sketch::pop_matrix`]:
46///
47/// ```rust
48/// # use whiskers::prelude::*;
49/// # let mut sketch = Sketch::new();
50/// sketch
51///     .circle(0.0, 0.0, 10.0)  // not translated
52///     .push_matrix()
53///     .translate(5.0, 5.0)
54///     .circle(0.0, 0.0, 10.0)  // translated
55///     .pop_matrix()
56///     .circle(0.0, 0.0, 5.0);  // not translated
57/// ```
58///
59/// The [`Sketch::push_matrix_and`] function is a convenience method to automatically save and
60/// restore the current transform matrix around some draw calls:
61///
62/// ```rust
63/// # use whiskers::prelude::*;
64/// # let mut sketch = Sketch::new();
65/// sketch
66///     .circle(0.0, 0.0, 10.0)        // not translated
67///     .push_matrix_and(|sketch| {
68///         sketch.translate(5.0, 5.0)
69///         .circle(0.0, 0.0, 10.0);   // translated
70///     })
71///     .circle(0.0, 0.0, 5.0);        // not translated
72/// ```
73///
74/// # Runner use
75///
76/// In interactive sketches, the [`Sketch`] instance is constructed by the [`crate::Runner`] and
77/// passed to the [`crate::App::update`] function. The runner takes care of configuring the page
78/// size according to the UI.
79///
80/// # Standalone use
81///
82/// Alternatively, the [`Sketch`] type can be used as standalone object to build, display, and/or
83/// export drawings to SVG:
84///
85/// ```no_run
86/// use whiskers::prelude::*;
87///
88/// fn main() -> Result {
89///    let mut sketch = Sketch::new();
90///
91///     sketch
92///         .scale(2.0 * Unit::Cm)
93///         .translate(10.0, 10.0)
94///         .circle(0.0, 0.0, 3.0)
95///         .rotate(Angle::from_deg(45.0))
96///         .rect(0.0, 0.0, 6.5, 0.5)
97///         .show()?
98///         .save("circle.svg")?;
99///
100///     Ok(())
101/// }
102/// ```
103///
104/// This results in the following SVG file:
105///
106/// ![result](https://github.com/abey79/vsvg/assets/49431240/09d88775-0ad3-4776-be4b-b3872ba467b0)
107///
108/// Note that here the page size is not set. If needed, it must be set manually using the
109/// [`Sketch::page_size`] function.
110
111pub struct Sketch {
112    document: Document,
113    transform_stack: Vec<Affine>,
114    target_layer: LayerID,
115    tolerance: f64,
116    path_metadata: PathMetadata,
117}
118
119impl Default for Sketch {
120    fn default() -> Self {
121        Self::new()
122    }
123}
124impl Sketch {
125    /// Create a new, empty [`Sketch`].
126    pub fn new() -> Self {
127        Self::with_document(Document::default())
128    }
129
130    /// Create a [`Sketch`] from an existing [`Document`].
131    pub fn with_document(mut document: Document) -> Self {
132        let target_layer = 0;
133        document.ensure_exists(target_layer);
134
135        Self {
136            document,
137            tolerance: DEFAULT_TOLERANCE,
138            transform_stack: vec![Affine::default()],
139            target_layer,
140            path_metadata: PathMetadata::default(),
141        }
142    }
143
144    /// Sets the target layer for subsequent draw calls.
145    pub fn set_layer(&mut self, layer_id: LayerID) -> &mut Self {
146        self.document.ensure_exists(layer_id);
147        self.target_layer = layer_id;
148        self
149    }
150
151    /// Returns the sketch's width in pixels.
152    ///
153    /// If the page size is not set, it defaults to 400px.
154    pub fn width(&self) -> f64 {
155        self.document.metadata().page_size.map_or(400.0, |p| p.w())
156    }
157
158    /// Returns the sketch's height in pixels.
159    ///
160    /// If the page size is not set, it defaults to 400px.
161    pub fn height(&self) -> f64 {
162        self.document.metadata().page_size.map_or(400.0, |p| p.h())
163    }
164
165    /// Sets the [`Sketch`]'s page size.
166    pub fn page_size(&mut self, page_size: PageSize) -> &mut Self {
167        self.document.metadata_mut().page_size = Some(page_size);
168        self
169    }
170
171    /// Sets the path color for subsequent draw calls.
172    pub fn color(&mut self, color: impl Into<vsvg::Color>) -> &mut Self {
173        self.path_metadata.color = color.into();
174        self
175    }
176
177    /// Sets the path stroke width for subsequent draw calls.
178    pub fn stroke_width(&mut self, width: impl Into<f64>) -> &mut Self {
179        self.path_metadata.stroke_width = width.into();
180        self
181    }
182
183    /// Push the current matrix onto the stack.
184    ///
185    /// A copy of the current transform matrix is pushed onto the stack. Use this before applying
186    /// temporary transforms that you want to revert later with [`Sketch::pop_matrix`].
187    pub fn push_matrix(&mut self) -> &mut Self {
188        self.transform_stack
189            .push(self.transform_stack.last().copied().unwrap_or_default());
190        self
191    }
192
193    /// Push the identity matrix onto the stack.
194    ///
195    /// Use this if you want to temporarily reset the transform matrix and later revert to the
196    /// current matrix with [`Sketch::pop_matrix`].
197    pub fn push_matrix_reset(&mut self) -> &mut Self {
198        self.transform_stack.push(Affine::default());
199        self
200    }
201
202    /// Pop the current transform matrix from the stack, restoring the previously pushed matrix.
203    pub fn pop_matrix(&mut self) -> &mut Self {
204        if self.transform_stack.len() == 1 {
205            log::warn!("pop_matrix: stack underflow");
206            return self;
207        }
208
209        self.transform_stack.pop();
210        self
211    }
212
213    /// Push the current matrix onto the stack, apply the given function, then pop the matrix.
214    ///
215    /// This is a convenience method for draw code that require a temporary change of the current
216    /// transform matrix.
217    pub fn push_matrix_and(&mut self, f: impl FnOnce(&mut Self)) -> &mut Self {
218        self.push_matrix();
219        f(self);
220        self.pop_matrix();
221        self
222    }
223
224    /// Centers the content of the sketch on the page, if the page size is set.
225    ///
226    /// **Note**: contrary to most other functions, this function is applied on the _existing_
227    /// sketch content, not on subsequent draw calls.
228    pub fn center(&mut self) -> &mut Self {
229        self.document_mut().center_content();
230        self
231    }
232
233    /// Returns a reference to the underlying [`Document`].
234    pub fn document(&self) -> &Document {
235        &self.document
236    }
237
238    /// Returns a mutable reference to the underlying [`Document`].
239    pub fn document_mut(&mut self) -> &mut Document {
240        &mut self.document
241    }
242
243    /// Consume the [`Sketch`] and return the underlying [`Document`].
244    pub fn into_document(self) -> Document {
245        self.document
246    }
247
248    /// Opens the `vsvg` viewer with the sketch content.
249    ///
250    /// Requires the `viewer` feature to be enabled.
251    #[cfg(feature = "viewer")]
252    #[cfg(not(target_arch = "wasm32"))]
253    #[allow(clippy::missing_panics_doc)]
254    pub fn show(&mut self) -> anyhow::Result<&mut Self> {
255        use std::mem::{replace, take};
256        use std::sync::Arc;
257
258        let document = Arc::new(take(&mut self.document));
259        vsvg_viewer::show(document.clone())?;
260
261        let _ = replace(
262            &mut self.document,
263            Arc::into_inner(document)
264                .expect("vsvg_viewer::show does not keep references to the document"),
265        );
266        Ok(self)
267    }
268
269    /// Saves the sketch content to an SVG file.
270    pub fn save(&self, path: impl AsRef<std::path::Path>) -> anyhow::Result<()> {
271        let file = std::io::BufWriter::new(std::fs::File::create(path)?);
272        self.document.to_svg(file)?;
273        Ok(())
274    }
275}
276
277impl Transforms for Sketch {
278    fn transform(&mut self, affine: &Affine) -> &mut Self {
279        if let Some(matrix) = self.transform_stack.last_mut() {
280            *matrix *= *affine;
281        } else {
282            log::warn!("transform: no matrix on the stack");
283        }
284
285        self
286    }
287}
288
289impl vsvg::Draw for Sketch {
290    fn add_path<T: IntoBezPathTolerance>(&mut self, path: T) -> &mut Self {
291        let mut path: Path =
292            Path::from_tolerance_metadata(path, self.tolerance, self.path_metadata.clone());
293
294        if let Some(&matrix) = self.transform_stack.last() {
295            path.apply_transform(matrix);
296        } else {
297            log::warn!("add_path: no matrix on the stack");
298        }
299
300        self.document.push_path(self.target_layer, path);
301        self
302    }
303}