Skip to main content

ferroui_vello/
geometry_impl.rs

1use crate::combined_geometry_impl::CombinedGeometryImpl;
2use crate::helpers::path_measure::PathMeasure;
3use crate::helpers::{path_helper, pen_helper};
4use crate::simple_geometry_impl::SimpleGeometryImpl;
5use crate::transformed_geometry_impl::TransformedGeometryImpl;
6use crate::vello_extensions::{closed_path, to_kurbo_point, to_point, to_rect};
7use ferroui_base::media::{FillRule, GeometryCombineMode, IPen, IntersectionResult};
8use ferroui_base::platform::{IGeometryImpl, ITransformedGeometryImpl};
9use ferroui_base::{Matrix, Point, Rect};
10use kurbo::{BezPath, Shape};
11use std::sync::{Arc, Mutex, Weak};
12
13/// A path of a geometry with the rule it is filled by.
14///
15/// A path of kurbo is a list of elements; the fill rule, which a path of
16/// Skia carries itself, travels beside it. The elements are shared: a copy
17/// of the value is a counted reference.
18#[derive(Clone, Debug)]
19pub struct VelloPath {
20    elements: Arc<BezPath>,
21    fill_rule: FillRule,
22}
23
24impl VelloPath {
25    /// Wraps a path that is filled by `fill_rule`.
26    pub fn new(path: BezPath, fill_rule: FillRule) -> Self {
27        Self { elements: Arc::new(path), fill_rule }
28    }
29
30    /// An empty path with the fill rule of a new stream geometry.
31    pub fn empty() -> Self {
32        Self::new(BezPath::new(), FillRule::EvenOdd)
33    }
34
35    /// The elements of the path.
36    pub fn path(&self) -> &BezPath {
37        &self.elements
38    }
39
40    /// The rule the path is filled by.
41    pub fn fill_rule(&self) -> FillRule {
42        self.fill_rule
43    }
44
45    /// The same elements with another fill rule.
46    pub fn with_fill_rule(&self, fill_rule: FillRule) -> Self {
47        Self { elements: self.elements.clone(), fill_rule }
48    }
49
50    /// The path with a transform applied.
51    pub fn transformed(&self, transform: kurbo::Affine) -> Self {
52        let mut path = (*self.elements).clone();
53        path.apply_affine(transform);
54        Self::new(path, self.fill_rule)
55    }
56
57    /// The tight bounds of the path: of its curves, not of their control
58    /// points, and of the points of its moves. A path without an element
59    /// has empty bounds at the origin.
60    ///
61    /// The point of a move counts whether or not a segment follows it, as
62    /// in the tight bounds of the Skia backend (`SkPath::computeTightBounds`
63    /// and its `tight_bounds` for a path of lines), which are upstream's.
64    /// Upstream's render tests have paths that end in moves to give a shape
65    /// its extent (`M 10,190 L 190,10 M0,0M200,200`).
66    pub fn tight_bounds(&self) -> Rect {
67        let mut bounds: Option<kurbo::Rect> = None;
68        let mut add = |rect: kurbo::Rect| bounds = Some(bounds.map_or(rect, |bounds| bounds.union(rect)));
69
70        for element in self.elements.elements() {
71            if let kurbo::PathEl::MoveTo(point) = element {
72                add(kurbo::Rect::from_points(*point, *point));
73            }
74        }
75        for segment in self.elements.segments() {
76            add(kurbo::Shape::bounding_box(&segment));
77        }
78
79        bounds.map(to_rect).unwrap_or_default()
80    }
81
82    /// Whether `other` is this very path: the same elements, not equal ones.
83    pub fn is_same(&self, other: &VelloPath) -> bool {
84        Arc::ptr_eq(&self.elements, &other.elements) && self.fill_rule == other.fill_rule
85    }
86
87    /// Whether the fill of the path contains the point. Open figures are
88    /// filled as if they were closed.
89    pub fn contains(&self, point: Point) -> bool {
90        let winding = closed_path(&self.elements).winding(to_kurbo_point(point));
91        if self.fill_rule == FillRule::EvenOdd {
92            winding & 1 != 0
93        } else {
94            winding != 0
95        }
96    }
97}
98
99/// The fill path of a geometry in relation to its stroke path.
100///
101/// Many geometries fill exactly what they stroke; keeping that fact explicit
102/// (rather than comparing paths) lets combined and transformed geometries
103/// reuse one path for both.
104#[derive(Clone, Debug, Default)]
105pub enum FillPath {
106    /// The geometry has no fill.
107    #[default]
108    None,
109    /// The fill path is the stroke path.
110    SameAsStroke,
111    /// The fill path differs from the stroke path.
112    Separate(VelloPath),
113}
114
115/// A geometry of this backend: something with a stroke path and a fill path.
116///
117/// The members of the platform contract are implemented once, by the
118/// functions of this module, on top of the two paths; every geometry type
119/// forwards to them through [`impl_geometry_impl`].
120pub trait GeometryImpl: IGeometryImpl {
121    /// The caches shared by all geometry kinds.
122    fn base(&self) -> &GeometryImplBase;
123
124    /// The path that is stroked.
125    fn stroke_path(&self) -> Option<VelloPath>;
126
127    /// The fill of the geometry in relation to its stroke path.
128    fn fill(&self) -> FillPath;
129
130    /// The path that is filled.
131    fn fill_path(&self) -> Option<VelloPath> {
132        match self.fill() {
133            FillPath::None => None,
134            FillPath::SameAsStroke => self.stroke_path(),
135            FillPath::Separate(path) => Some(path),
136        }
137    }
138
139    /// Whether the fill path is the very same path as the stroke path.
140    fn is_fill_same_as_stroke(&self) -> bool {
141        matches!(self.fill(), FillPath::SameAsStroke) && self.stroke_path().is_some()
142    }
143}
144
145/// State every geometry kind has: the stroke cache, the path measure and a
146/// handle to the geometry itself.
147#[derive(Default)]
148pub struct GeometryImplBase {
149    path_cache: Mutex<PathCache>,
150    cached_path_measure: Mutex<Option<PathMeasure>>,
151    weak_self: Mutex<Option<Weak<dyn GeometryImpl>>>,
152}
153
154/// A value of a geometry that the UI thread and the render thread both read:
155/// readers take a copy under a lock; a copy of a path shares its elements.
156#[derive(Default)]
157pub struct Shared<T>(Mutex<T>);
158
159impl<T: Clone> Shared<T> {
160    /// Wraps `value`.
161    pub fn new(value: T) -> Self {
162        Self(Mutex::new(value))
163    }
164
165    /// A copy of the value.
166    pub fn get(&self) -> T {
167        self.0.lock().unwrap().clone()
168    }
169
170    /// Replaces the value.
171    pub fn set(&self, value: T) {
172        *self.0.lock().unwrap() = value;
173    }
174}
175
176impl GeometryImplBase {
177    /// Creates the state of a new geometry.
178    pub fn new() -> Self {
179        Self::default()
180    }
181
182    /// Invalidates all caches. Call after the paths of the geometry changed.
183    pub fn invalidate_caches(&self) {
184        *self.path_cache.lock().unwrap() = PathCache::default();
185        *self.cached_path_measure.lock().unwrap() = None;
186    }
187
188    /// The handle of the geometry this state belongs to.
189    fn to_arc(&self) -> Arc<dyn GeometryImpl> {
190        self.weak_self
191            .lock()
192            .unwrap()
193            .as_ref()
194            .and_then(Weak::upgrade)
195            .expect("geometries of the backend are created behind a handle")
196    }
197}
198
199/// Puts a new geometry behind a handle and lets it find that handle again
200/// (a transformed geometry keeps its source alive).
201pub(crate) fn register<T: GeometryImpl>(geometry: T) -> Arc<T> {
202    let geometry = Arc::new(geometry);
203    let weak: Weak<dyn GeometryImpl> = Arc::downgrade(&(geometry.clone() as Arc<dyn GeometryImpl>));
204    *geometry.base().weak_self.lock().unwrap() = Some(weak);
205    geometry
206}
207
208/// Recovers the backend geometry behind a platform geometry.
209///
210/// Returns `None` for geometries created by another backend.
211pub fn try_get_geometry_impl(geometry: &dyn IGeometryImpl) -> Option<&dyn GeometryImpl> {
212    use crate::ellipse_geometry_impl::EllipseGeometryImpl;
213    use crate::geometry_group_impl::GeometryGroupImpl;
214    use crate::line_geometry_impl::LineGeometryImpl;
215    use crate::rectangle_geometry_impl::RectangleGeometryImpl;
216
217    let any = geometry.as_any();
218
219    if let Some(geometry) = any.downcast_ref::<SimpleGeometryImpl>() {
220        return Some(geometry);
221    }
222    if let Some(geometry) = any.downcast_ref::<TransformedGeometryImpl>() {
223        return Some(geometry);
224    }
225    if let Some(geometry) = any.downcast_ref::<RectangleGeometryImpl>() {
226        return Some(geometry);
227    }
228    if let Some(geometry) = any.downcast_ref::<EllipseGeometryImpl>() {
229        return Some(geometry);
230    }
231    if let Some(geometry) = any.downcast_ref::<LineGeometryImpl>() {
232        return Some(geometry);
233    }
234    if let Some(geometry) = any.downcast_ref::<GeometryGroupImpl>() {
235        return Some(geometry);
236    }
237    if let Some(geometry) = any.downcast_ref::<CombinedGeometryImpl>() {
238        return Some(geometry);
239    }
240
241    None
242}
243
244fn with_path_measure<R>(geometry: &dyn GeometryImpl, f: impl FnOnce(&PathMeasure) -> R) -> Option<R> {
245    let stroke_path = geometry.stroke_path()?;
246    let mut measure = geometry.base().cached_path_measure.lock().unwrap();
247    let measure = measure.get_or_insert_with(|| PathMeasure::new(stroke_path.path()));
248    Some(f(measure))
249}
250
251pub(crate) fn contour_length(geometry: &dyn GeometryImpl) -> f64 {
252    with_path_measure(geometry, |measure| measure.length()).unwrap_or(0.0)
253}
254
255pub(crate) fn fill_contains(geometry: &dyn GeometryImpl, point: Point) -> bool {
256    geometry.fill_path().is_some_and(|path| path.contains(point))
257}
258
259pub(crate) fn stroke_contains(geometry: &dyn GeometryImpl, pen: Option<&dyn IPen>, point: Point) -> bool {
260    let mut cache = geometry.base().path_cache.lock().unwrap();
261    cache.update_if_needed(geometry.stroke_path().as_ref(), pen);
262    cache.expanded_path().is_some_and(|path| path.contains(point))
263}
264
265pub(crate) fn intersect(geometry: &dyn GeometryImpl, other: &dyn IGeometryImpl) -> Option<Arc<dyn IGeometryImpl>> {
266    let other = try_get_geometry_impl(other)?;
267    CombinedGeometryImpl::try_create(GeometryCombineMode::Intersect, geometry, other)
268        .map(|geometry| geometry as Arc<dyn IGeometryImpl>)
269}
270
271pub(crate) fn get_render_bounds(geometry: &dyn GeometryImpl, pen: Option<&dyn IPen>) -> Rect {
272    let mut cache = geometry.base().path_cache.lock().unwrap();
273    cache.update_if_needed(geometry.stroke_path().as_ref(), pen);
274    let mut bounds = cache.render_bounds();
275
276    if let FillPath::Separate(fill_path) = geometry.fill() {
277        bounds = bounds.union(fill_path.tight_bounds());
278    }
279
280    bounds
281}
282
283pub(crate) fn get_widened_geometry(geometry: &dyn GeometryImpl, pen: &dyn IPen) -> Arc<dyn IGeometryImpl> {
284    if let Some(stroke_path) = geometry.stroke_path() {
285        if let Some(path) = path_helper::create_stroked_path(stroke_path.path(), pen) {
286            // The outline of a stroke is filled by the non-zero rule: where
287            // the stroke crosses itself its figures overlap.
288            return SimpleGeometryImpl::new(
289                VelloPath::new(path, FillRule::NonZero),
290                FillPath::SameAsStroke,
291                None,
292            );
293        }
294    }
295
296    SimpleGeometryImpl::new(VelloPath::empty(), FillPath::None, None)
297}
298
299pub(crate) fn with_transform(geometry: &dyn GeometryImpl, transform: Matrix) -> Arc<dyn ITransformedGeometryImpl> {
300    TransformedGeometryImpl::new(geometry.base().to_arc(), transform)
301}
302
303pub(crate) fn try_get_point_at_distance(geometry: &dyn GeometryImpl, distance: f64) -> Option<Point> {
304    try_get_point_and_tangent_at_distance(geometry, distance).map(|(point, _)| point)
305}
306
307pub(crate) fn try_get_point_and_tangent_at_distance(
308    geometry: &dyn GeometryImpl,
309    distance: f64,
310) -> Option<(Point, Point)> {
311    let (point, tangent) = with_path_measure(geometry, |measure| measure.pos_tan(distance))??;
312    Some((to_point(point), Point::new(tangent.x, tangent.y)))
313}
314
315pub(crate) fn try_get_segment(
316    geometry: &dyn GeometryImpl,
317    start_distance: f64,
318    stop_distance: f64,
319    start_on_begin_figure: bool,
320) -> Option<Arc<dyn IGeometryImpl>> {
321    let segment = with_path_measure(geometry, |measure| {
322        measure.get_segment(start_distance, stop_distance, start_on_begin_figure)
323    })??;
324
325    Some(SimpleGeometryImpl::new(VelloPath::new(segment, FillRule::EvenOdd), FillPath::None, None))
326}
327
328pub(crate) fn get_fill_intersection_result(
329    geometry: &dyn GeometryImpl,
330    other: &dyn IGeometryImpl,
331) -> IntersectionResult {
332    let Some(other) = try_get_geometry_impl(other) else {
333        return IntersectionResult::Empty;
334    };
335
336    hit_test_path(geometry.fill_path().as_ref(), other.fill_path().as_ref())
337}
338
339/// The relation of the areas two paths fill.
340///
341/// The Skia backend compares the regions of the two paths, which are the
342/// pixels they cover; here the areas themselves are compared: they
343/// intersect when their intersection has an area, and one contains the
344/// other when nothing of the other is left outside it.
345fn hit_test_path(path1: Option<&VelloPath>, path2: Option<&VelloPath>) -> IntersectionResult {
346    use crate::combined_geometry_impl::{combine_paths, path_area};
347
348    let (Some(path1), Some(path2)) = (path1, path2) else {
349        return IntersectionResult::Empty;
350    };
351
352    /// Areas below this are the noise of the boolean operations.
353    const MINIMUM_AREA: f64 = 1e-6;
354
355    let has_area = |mode| {
356        combine_paths(mode, path1, path2).is_some_and(|path| path_area(path.path()) > MINIMUM_AREA)
357    };
358
359    if has_area(GeometryCombineMode::Intersect) {
360        let other_outside = combine_paths(GeometryCombineMode::Exclude, path2, path1)
361            .is_some_and(|path| path_area(path.path()) > MINIMUM_AREA);
362        if !other_outside {
363            return IntersectionResult::FullyInside;
364        }
365
366        if !has_area(GeometryCombineMode::Exclude) {
367            return IntersectionResult::FullyContains;
368        }
369
370        return IntersectionResult::Intersects;
371    }
372
373    IntersectionResult::Empty
374}
375
376/// The stroke of a geometry expanded with a pen, kept for hit testing and
377/// render bounds until the pen or the stroke path changes.
378#[derive(Default)]
379struct PathCache {
380    pen_hash: u64,
381    path: Option<VelloPath>,
382    /// The stroke path the cache was computed for.
383    cached_for: Option<VelloPath>,
384    render_bounds: Option<Rect>,
385}
386
387impl PathCache {
388    fn render_bounds(&mut self) -> Rect {
389        if let Some(bounds) = self.render_bounds {
390            return bounds;
391        }
392
393        let bounds = match (&self.path, &self.cached_for) {
394            (Some(path), _) => path.tight_bounds(),
395            (None, Some(cached_for)) => cached_for.tight_bounds(),
396            (None, None) => Rect::default(),
397        };
398        self.render_bounds = Some(bounds);
399        bounds
400    }
401
402    fn expanded_path(&self) -> Option<&VelloPath> {
403        self.path.as_ref()
404    }
405
406    fn update_if_needed(&mut self, stroke_path: Option<&VelloPath>, pen: Option<&dyn IPen>) {
407        let pen_hash = pen_helper::get_hash_code(pen, false);
408
409        let same_path = match (stroke_path, &self.cached_for) {
410            (Some(stroke_path), Some(cached_for)) => stroke_path.is_same(cached_for),
411            (None, None) => true,
412            _ => false,
413        };
414
415        if pen_hash == self.pen_hash && same_path {
416            // We are up to date.
417            return;
418        }
419
420        self.render_bounds = None;
421        self.cached_for = stroke_path.cloned();
422        self.pen_hash = pen_hash;
423
424        self.path = match (stroke_path, pen) {
425            (Some(stroke_path), Some(pen)) => path_helper::create_stroked_path(stroke_path.path(), pen)
426                .map(|path| VelloPath::new(path, FillRule::NonZero)),
427            _ => None,
428        };
429    }
430}
431
432/// Implements the platform geometry contract for a [`GeometryImpl`] type by
433/// forwarding to the shared implementation.
434///
435/// The second form adds overrides of the contract's default members (the
436/// `as_*` views).
437macro_rules! impl_geometry_impl {
438    ($ty:ty) => {
439        $crate::geometry_impl::impl_geometry_impl!($ty, {});
440    };
441    ($ty:ty, { $($extra:tt)* }) => {
442        impl ferroui_base::platform::IGeometryImpl for $ty {
443            fn bounds(&self) -> ferroui_base::Rect {
444                self.geometry_bounds()
445            }
446
447            fn as_any(&self) -> &dyn std::any::Any {
448                self
449            }
450
451            fn contour_length(&self) -> f64 {
452                $crate::geometry_impl::contour_length(self)
453            }
454
455            fn get_render_bounds(&self, pen: Option<&dyn ferroui_base::media::IPen>) -> ferroui_base::Rect {
456                $crate::geometry_impl::get_render_bounds(self, pen)
457            }
458
459            fn get_widened_geometry(
460                &self,
461                pen: &dyn ferroui_base::media::IPen,
462            ) -> std::sync::Arc<dyn ferroui_base::platform::IGeometryImpl> {
463                $crate::geometry_impl::get_widened_geometry(self, pen)
464            }
465
466            fn fill_contains(&self, point: ferroui_base::Point) -> bool {
467                $crate::geometry_impl::fill_contains(self, point)
468            }
469
470            fn get_fill_intersection_result(
471                &self,
472                geometry: &dyn ferroui_base::platform::IGeometryImpl,
473            ) -> ferroui_base::media::IntersectionResult {
474                $crate::geometry_impl::get_fill_intersection_result(self, geometry)
475            }
476
477            fn intersect(
478                &self,
479                geometry: &dyn ferroui_base::platform::IGeometryImpl,
480            ) -> Option<std::sync::Arc<dyn ferroui_base::platform::IGeometryImpl>> {
481                $crate::geometry_impl::intersect(self, geometry)
482            }
483
484            fn stroke_contains(
485                &self,
486                pen: Option<&dyn ferroui_base::media::IPen>,
487                point: ferroui_base::Point,
488            ) -> bool {
489                $crate::geometry_impl::stroke_contains(self, pen, point)
490            }
491
492            fn with_transform(
493                &self,
494                transform: ferroui_base::Matrix,
495            ) -> std::sync::Arc<dyn ferroui_base::platform::ITransformedGeometryImpl> {
496                $crate::geometry_impl::with_transform(self, transform)
497            }
498
499            fn try_get_point_at_distance(&self, distance: f64) -> Option<ferroui_base::Point> {
500                $crate::geometry_impl::try_get_point_at_distance(self, distance)
501            }
502
503            fn try_get_point_and_tangent_at_distance(
504                &self,
505                distance: f64,
506            ) -> Option<(ferroui_base::Point, ferroui_base::Point)> {
507                $crate::geometry_impl::try_get_point_and_tangent_at_distance(self, distance)
508            }
509
510            fn try_get_segment(
511                &self,
512                start_distance: f64,
513                stop_distance: f64,
514                start_on_begin_figure: bool,
515            ) -> Option<std::sync::Arc<dyn ferroui_base::platform::IGeometryImpl>> {
516                $crate::geometry_impl::try_get_segment(self, start_distance, stop_distance, start_on_begin_figure)
517            }
518
519            $($extra)*
520        }
521    };
522}
523
524pub(crate) use impl_geometry_impl;