Skip to main content

motion_core/
shape.rs

1//! Logical shapes and styled geometry.
2
3use crate::drawing::*;
4use crate::{Path2D, Rect, ValidationError};
5
6mod primitives;
7
8pub use primitives::*;
9
10/// Logical geometry independent of paint and node placement.
11///
12/// Custom geometry must keep path and bounds consistent and deterministic.
13/// Direct path conversion requires valid geometry; invalid raw rectangles panic
14/// there, while recording their filled nodes reports validation errors instead.
15pub trait Shape: Send + Sync {
16    /// Reports retained authoring errors before measuring or drawing.
17    fn validate(&self) -> Result<(), ValidationError> {
18        Ok(())
19    }
20
21    /// Returns a checked path snapshot without f32 narrowing.
22    fn path(&self) -> Path2D;
23
24    /// Geometric bounds; defaults to the path's cached curve bounds.
25    fn bounding_box(&self) -> Rect {
26        self.path().bounding_box()
27    }
28
29    /// Exact rectangle equivalent, when available, for a rule-independent fast path.
30    fn as_rect(&self) -> Option<Rect> {
31        None
32    }
33
34    /// Adds floating fill paint, using the nonzero rule by default.
35    fn fill(self, paint: impl Into<Paint>) -> Styled<Self>
36    where
37        Self: Sized,
38    {
39        Styled {
40            shape: self,
41            fill: Some(paint.into()),
42            stroke: None,
43            stroke_style: StrokeStyle::default(),
44            rule: FillRule::default(),
45            trim: None,
46            dash: None,
47        }
48    }
49
50    /// Adds a bounded curve/path stroke without an implicit fill.
51    fn stroke(self, width: f64, paint: impl Into<Paint>) -> Styled<Self>
52    where
53        Self: Sized,
54    {
55        Styled {
56            shape: self,
57            fill: None,
58            stroke: Some(paint.into()),
59            stroke_style: StrokeStyle {
60                width,
61                ..StrokeStyle::default()
62            },
63            rule: FillRule::default(),
64            trim: None,
65            dash: None,
66        }
67    }
68}
69
70impl Shape for Path2D {
71    fn validate(&self) -> Result<(), ValidationError> {
72        Path2D::validate(self)
73    }
74
75    fn path(&self) -> Path2D {
76        self.clone()
77    }
78
79    fn bounding_box(&self) -> Rect {
80        Path2D::bounding_box(self)
81    }
82}
83
84impl Shape for Rect {
85    fn path(&self) -> Path2D {
86        Path2D::from_rect(*self).expect("Shape::path requires valid rectangle geometry")
87    }
88
89    fn bounding_box(&self) -> Rect {
90        *self
91    }
92
93    fn as_rect(&self) -> Option<Rect> {
94        Some(*self)
95    }
96}
97
98/// geometry with optional fill/stroke paint and an explicit fill rule.
99///
100/// Fill is drawn before stroke; Node opacity applies once to their combined
101/// result. Bounds conservatively include the stroke, not just its centerline.
102#[derive(Clone, Debug)]
103pub struct Styled<Geometry> {
104    shape: Geometry,
105    fill: Option<Paint>,
106    stroke: Option<Paint>,
107    stroke_style: StrokeStyle,
108    rule: FillRule,
109    trim: Option<(f64, f64)>,
110    dash: Option<(std::sync::Arc<[f64]>, f64)>,
111}
112
113impl<Geometry: Shape> Styled<Geometry> {
114    /// Clips source geometry by normalized arc distance before dashing.
115    /// Bounds remain conservative and stable during growth animation.
116    pub fn trim(mut self, start: f64, end: f64) -> Self {
117        self.trim = Some((start, end));
118        self
119    }
120
121    /// Replaces the dash pattern/phase. Invalid or more than 256 entries fail
122    /// during drawing, even on hidden or zero-width artwork.
123    pub fn dash(mut self, pattern: &[f64], offset: f64) -> Self {
124        self.dash = Some((std::sync::Arc::from(pattern), offset));
125        self
126    }
127
128    /// Replaces the floating fill paint without changing geometry or rule.
129    pub fn fill(mut self, paint: impl Into<Paint>) -> Self {
130        self.fill = Some(paint.into());
131        self
132    }
133
134    /// Adds or replaces stroke paint/width, retaining cap and join selection.
135    pub fn stroke(mut self, width: f64, paint: impl Into<Paint>) -> Self {
136        self.stroke = Some(paint.into());
137        self.stroke_style.width = width;
138        self
139    }
140
141    /// Replaces stroke geometry style without enabling a missing stroke paint.
142    pub fn stroke_style(mut self, style: StrokeStyle) -> Self {
143        self.stroke_style = style;
144        self
145    }
146
147    /// Replaces the contour rule; the default is [`FillRule::default`].
148    pub fn fill_rule(mut self, rule: FillRule) -> Self {
149        self.rule = rule;
150        self
151    }
152}
153
154impl Rect {
155    /// Styles this rectangle without declaring a separate layout bound.
156    ///
157    /// The geometry itself supplies Drawable::bounds.
158    pub fn fill(self, paint: impl Into<Paint>) -> Styled<Self> {
159        Shape::fill(self, paint)
160    }
161
162    /// Styles this rectangle with a outline and no implicit fill.
163    pub fn stroke(self, width: f64, paint: impl Into<Paint>) -> Styled<Self> {
164        Shape::stroke(self, width, paint)
165    }
166}
167
168impl<Geometry: Shape> Drawable for Styled<Geometry> {
169    fn try_bounds(&self, context: &DrawContext) -> Result<Rect, ValidationError> {
170        self.shape.validate()?;
171        Ok(self.bounds(context))
172    }
173
174    fn bounds(&self, _: &DrawContext) -> Rect {
175        let bounds = self.shape.bounding_box();
176        if self.stroke.is_some() && validate_rect(bounds, "stroke", "bounds").is_ok() {
177            if self.stroke_style.validate("stroke").is_err() {
178                return Rect::new(f64::NAN, 0.0, 0.0, 0.0);
179            }
180            self.stroke_style.bounds(bounds)
181        } else {
182            bounds
183        }
184    }
185
186    fn draw(&self, _: &DrawContext, canvas: &mut Canvas<'_>) -> Result<(), ValidationError> {
187        self.shape.validate()?;
188        if self.trim.is_some() || self.dash.is_some() {
189            let mut geometry = self.shape.path();
190            if let Some((start, end)) = self.trim {
191                geometry = geometry.try_trim(start, end)?;
192            }
193            if let Some((pattern, offset)) = &self.dash {
194                geometry = geometry.dashed(pattern, *offset)?;
195            }
196            if let Some(paint) = &self.fill {
197                canvas.fill(&geometry, paint.clone(), self.rule)?;
198            }
199            if let Some(paint) = &self.stroke {
200                canvas.stroke(&geometry, &self.stroke_style, paint.clone())?;
201            }
202            return Ok(());
203        }
204        if let Some(paint) = &self.fill {
205            canvas.fill(&self.shape, paint.clone(), self.rule)?;
206        }
207        if let Some(paint) = &self.stroke {
208            canvas.stroke(&self.shape, &self.stroke_style, paint.clone())?;
209        }
210        Ok(())
211    }
212}