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/// Both queries report invalid geometry, such as a raw rectangle with nonfinite
14/// edges, as a [`ValidationError`] instead of panicking.
15pub trait Shape: Send + Sync {
16    /// Returns a checked path snapshot without f32 narrowing.
17    fn path(&self) -> Result<Path2D, ValidationError>;
18
19    /// Geometric bounds; defaults to the path's cached curve bounds.
20    fn bounding_box(&self) -> Result<Rect, ValidationError> {
21        self.path().map(|path| path.bounding_box())
22    }
23
24    /// Exact rectangle equivalent, when available, for a rule-independent fast path.
25    fn as_rect(&self) -> Option<Rect> {
26        None
27    }
28
29    /// Adds floating fill paint, using the nonzero rule by default.
30    fn fill(self, paint: impl Into<Paint>) -> Styled<Self>
31    where
32        Self: Sized,
33    {
34        Styled {
35            shape: self,
36            fill: Some(paint.into()),
37            stroke: None,
38            stroke_style: StrokeStyle::default(),
39            rule: FillRule::default(),
40            trim: None,
41            dash: None,
42        }
43    }
44
45    /// Adds a bounded curve/path stroke without an implicit fill.
46    fn stroke(self, width: f64, paint: impl Into<Paint>) -> Styled<Self>
47    where
48        Self: Sized,
49    {
50        Styled {
51            shape: self,
52            fill: None,
53            stroke: Some(paint.into()),
54            stroke_style: StrokeStyle {
55                width,
56                ..StrokeStyle::default()
57            },
58            rule: FillRule::default(),
59            trim: None,
60            dash: None,
61        }
62    }
63}
64
65impl Shape for Path2D {
66    fn path(&self) -> Result<Path2D, ValidationError> {
67        Ok(self.clone())
68    }
69
70    fn bounding_box(&self) -> Result<Rect, ValidationError> {
71        Ok(Path2D::bounding_box(self))
72    }
73}
74
75impl Shape for Rect {
76    fn path(&self) -> Result<Path2D, ValidationError> {
77        Path2D::from_rect(*self)
78    }
79
80    fn bounding_box(&self) -> Result<Rect, ValidationError> {
81        Ok(*self)
82    }
83
84    fn as_rect(&self) -> Option<Rect> {
85        Some(*self)
86    }
87}
88
89/// geometry with optional fill/stroke paint and an explicit fill rule.
90///
91/// Fill is drawn before stroke; Node opacity applies once to their combined
92/// result. Bounds conservatively include the stroke, not just its centerline.
93#[derive(Clone, Debug)]
94pub struct Styled<Geometry> {
95    shape: Geometry,
96    fill: Option<Paint>,
97    stroke: Option<Paint>,
98    stroke_style: StrokeStyle,
99    rule: FillRule,
100    trim: Option<(f64, f64)>,
101    dash: Option<(std::sync::Arc<[f64]>, f64)>,
102}
103
104impl<Geometry: Shape> Styled<Geometry> {
105    /// Clips source geometry by normalized arc distance before dashing.
106    /// Bounds remain conservative and stable during growth animation.
107    pub fn trim(mut self, start: f64, end: f64) -> Self {
108        self.trim = Some((start, end));
109        self
110    }
111
112    /// Replaces the dash pattern/phase. Invalid or more than 256 entries fail
113    /// during drawing, even on hidden or zero-width artwork.
114    pub fn dash(mut self, pattern: &[f64], offset: f64) -> Self {
115        self.dash = Some((std::sync::Arc::from(pattern), offset));
116        self
117    }
118
119    /// Replaces the floating fill paint without changing geometry or rule.
120    pub fn fill(mut self, paint: impl Into<Paint>) -> Self {
121        self.fill = Some(paint.into());
122        self
123    }
124
125    /// Adds or replaces stroke paint/width, retaining cap and join selection.
126    pub fn stroke(mut self, width: f64, paint: impl Into<Paint>) -> Self {
127        self.stroke = Some(paint.into());
128        self.stroke_style.width = width;
129        self
130    }
131
132    /// Replaces stroke geometry style without enabling a missing stroke paint.
133    pub fn stroke_style(mut self, style: StrokeStyle) -> Self {
134        self.stroke_style = style;
135        self
136    }
137
138    /// Replaces the contour rule; the default is [`FillRule::default`].
139    pub fn fill_rule(mut self, rule: FillRule) -> Self {
140        self.rule = rule;
141        self
142    }
143}
144
145impl<Geometry: Shape> Drawable for Styled<Geometry> {
146    fn bounds(&self, _: &DrawContext) -> Result<Rect, ValidationError> {
147        let bounds = self.shape.bounding_box()?;
148        if self.stroke.is_some() && validate_rect(bounds, "stroke", "bounds").is_ok() {
149            self.stroke_style.validate("stroke")?;
150            Ok(self.stroke_style.bounds(bounds))
151        } else {
152            Ok(bounds)
153        }
154    }
155
156    fn draw(&self, _: &DrawContext, canvas: &mut Canvas<'_>) -> Result<(), ValidationError> {
157        if self.trim.is_some() || self.dash.is_some() {
158            let mut geometry = self.shape.path()?;
159            if let Some((start, end)) = self.trim {
160                geometry = geometry.try_trim(start, end)?;
161            }
162            if let Some((pattern, offset)) = &self.dash {
163                geometry = geometry.dashed(pattern, *offset)?;
164            }
165            if let Some(paint) = &self.fill {
166                canvas.fill(&geometry, paint.clone(), self.rule)?;
167            }
168            if let Some(paint) = &self.stroke {
169                canvas.stroke(&geometry, &self.stroke_style, paint.clone())?;
170            }
171            return Ok(());
172        }
173        if let Some(paint) = &self.fill {
174            canvas.fill(&self.shape, paint.clone(), self.rule)?;
175        }
176        if let Some(paint) = &self.stroke {
177            canvas.stroke(&self.shape, &self.stroke_style, paint.clone())?;
178        }
179        Ok(())
180    }
181}