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