Skip to main content

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}