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}