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/// 
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}