motion_core/drawing/clip.rs
1//! Coverage masks and geometric clip domains.
2
3use super::*;
4
5/// How a subtree clip combines with its geometric ancestors.
6#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
7#[cfg_attr(
8 feature = "serde",
9 derive(serde::Serialize, serde::Deserialize),
10 serde(rename_all = "snake_case")
11)]
12#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
13pub enum ClipMode {
14 /// Multiply separately rasterized clip coverage, preserving mask semantics.
15 #[default]
16 Coverage,
17 /// Intersect geometry before coverage quantization.
18 Geometric,
19}
20
21/// Values accepted by [`crate::NodeModifiers::clip`] that convert to a [`ShapeClip`].
22///
23/// Shapes convert through their fallible [`Shape::path`]; clips convert directly.
24pub trait IntoShapeClip {
25 /// Captures the clip geometry, reporting invalid shapes.
26 fn into_shape_clip(self) -> Result<ShapeClip, ValidationError>;
27}
28
29impl IntoShapeClip for ShapeClip {
30 fn into_shape_clip(self) -> Result<ShapeClip, ValidationError> {
31 Ok(self)
32 }
33}
34
35impl IntoShapeClip for PathClip {
36 fn into_shape_clip(self) -> Result<ShapeClip, ValidationError> {
37 Ok(ShapeClip {
38 geometry: ClipGeometry::Path(self.path),
39 transform: self.transform,
40 rule: self.rule,
41 })
42 }
43}
44
45impl IntoShapeClip for RectClip {
46 fn into_shape_clip(self) -> Result<ShapeClip, ValidationError> {
47 Ok(ShapeClip {
48 geometry: ClipGeometry::Rectangle(self.bounds),
49 transform: self.transform,
50 rule: FillRule::NonZero,
51 })
52 }
53}
54
55impl<Geometry: Shape> IntoShapeClip for Geometry {
56 fn into_shape_clip(self) -> Result<ShapeClip, ValidationError> {
57 ShapeClip::new(&self)
58 }
59}
60
61/// A checked path used as a coverage mask on an isolated subtree.
62///
63/// Transform and rule remain editable and validate during compilation/preflight.
64/// The transform is node-local in authoring and device-space in a DisplayList.
65#[derive(Clone, Debug, PartialEq)]
66pub struct PathClip {
67 /// Shared geometry; open contours close implicitly for filling.
68 pub path: Arc<Path2D>,
69 /// Mapping from path coordinates into the owning coordinate system.
70 pub transform: Transform,
71 /// Explicit filled-interior rule.
72 pub rule: FillRule,
73}
74
75impl PathClip {
76 /// Creates a nonzero clip with an identity local transform.
77 pub fn new(path: Path2D) -> Self {
78 Self {
79 path: Arc::new(path),
80 transform: Transform::IDENTITY,
81 rule: FillRule::NonZero,
82 }
83 }
84}
85
86impl From<Path2D> for PathClip {
87 fn from(path: Path2D) -> Self {
88 Self::new(path)
89 }
90}
91
92/// A local rectangle whose transformed region intersects ancestor rectangles.
93///
94/// Unlike PathClip, nested rectangles intersect before coverage quantization.
95/// Raw bounds and transforms validate during authoring/DisplayList preflight.
96#[derive(Clone, Copy, Debug, PartialEq)]
97pub struct RectClip {
98 /// Opposite local corners; zero area is an empty clipping region.
99 pub bounds: Rect,
100 /// Local mapping in authoring, device mapping in a DisplayList.
101 pub transform: Transform,
102}
103
104impl RectClip {
105 /// Creates a local rectangle without narrowing or unchecked path conversion.
106 pub const fn new(bounds: Rect) -> Self {
107 Self {
108 bounds,
109 transform: Transform::IDENTITY,
110 }
111 }
112}
113
114impl From<Rect> for RectClip {
115 fn from(bounds: Rect) -> Self {
116 Self::new(bounds)
117 }
118}
119
120/// A snapshotted Shape used for geometric intersection, not coverage multiplication.
121#[derive(Clone, Debug, PartialEq)]
122pub struct ShapeClip {
123 /// Snapshot of the local clipping geometry, not a bounding-box substitute.
124 pub geometry: ClipGeometry,
125 /// Local mapping in authoring, device mapping in a DisplayList.
126 pub transform: Transform,
127 /// Interior interpretation of this shape independently of its ancestors.
128 pub rule: FillRule,
129}
130
131#[derive(Clone, Debug, PartialEq)]
132/// Immutable rectangle or path geometry used to intersect geometric clipping domains.
133pub enum ClipGeometry {
134 /// Raw opposite-corner rectangle, validated before geometric intersection.
135 Rectangle(Rect),
136 /// Shared path whose contour interpretation comes from its owning clip.
137 Path(Arc<Path2D>),
138}
139
140impl ShapeClip {
141 /// Captures a shape once; trait objects are accepted without retaining them.
142 ///
143 /// Exact rectangles retain raw bounds so invalid rectangles fail validation
144 /// with the clip. Other shapes share a checked path and report its errors.
145 ///
146 /// ```
147 /// use motion_core::prelude::*;
148 /// use motion_core::{
149 /// DisplayList, RasterLimits, ResourceStore, ValidatedDisplayList, MAX_PATH_VERBS,
150 /// MAX_STROKE_VERBS, MAX_SVG_PATH_BYTES, sample_offsets,
151 /// };
152 /// let path = Path2D::from_rect(Rect::new(0.0, 0.0, 1.0, 1.0))?;
153 /// let shape: &dyn Shape = &path;
154 /// let mut clip = ShapeClip::new(shape)?;
155 /// clip.rule = FillRule::EvenOdd;
156 /// clip.transform = Transform::translate(0.5, 0.0);
157 /// let frame = Node::from(Rect::new(0.0, 0.0, 2.0, 1.0).fill(Color::WHITE))
158 /// .clip_with(clip, motion_core::ClipMode::Geometric)
159 /// .compile(
160 /// Size::new(2.0, 1.0),
161 /// PixelSize::new(2, 1),
162 /// ValidationLimits::default(),
163 /// )?;
164 /// # Ok::<(), motion_core::ValidationError>(())
165 /// ```
166 pub fn new(shape: &(impl Shape + ?Sized)) -> Result<Self, ValidationError> {
167 let geometry = match shape.as_rect() {
168 Some(bounds) => ClipGeometry::Rectangle(bounds),
169 None => ClipGeometry::Path(Arc::new(shape.path()?)),
170 };
171 Ok(Self {
172 geometry,
173 transform: Transform::IDENTITY,
174 rule: FillRule::NonZero,
175 })
176 }
177
178 /// Checks geometry and transform, returning the path-verb count for structural charging.
179 pub fn validate(&self, path: &str) -> Result<usize, ValidationError> {
180 match &self.geometry {
181 ClipGeometry::Rectangle(bounds) => {
182 crate::display_list::mapped_bounds(*bounds, self.transform, path)?;
183 Ok(0)
184 }
185 ClipGeometry::Path(geometry) => {
186 crate::display_list::validate_path(geometry, self.transform, path)?;
187 Ok(geometry.verbs().len())
188 }
189 }
190 }
191}