incular_rendering/
paths.rs1use crate::geometry::CornerRadii;
2use crate::paint::FillRule;
3use incular_core::{Offset, Rect, Size, Transform};
4use kurbo::{BezPath, Ellipse, Point, RoundedRect, RoundedRectRadii, Shape, Vec2};
5use std::sync::{
6 Arc,
7 atomic::{AtomicU64, Ordering},
8};
9
10static NEXT_PATH_ID: AtomicU64 = AtomicU64::new(1);
11
12fn next_path_id() -> PathId {
13 PathId(
14 NEXT_PATH_ID
15 .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |id| id.checked_add(1))
16 .expect("path identity space exhausted"),
17 )
18}
19
20#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
23pub struct PathId(u64);
24#[derive(Clone, Debug, PartialEq)]
25pub struct Path {
26 id: PathId,
27 path: Arc<BezPath>,
28 bounds: Option<Rect>,
29}
30impl Path {
31 #[must_use]
32 pub fn builder() -> PathBuilder {
33 PathBuilder::default()
34 }
35 #[must_use]
36 pub fn bez_path(&self) -> &BezPath {
37 &self.path
38 }
39 #[must_use]
40 pub fn bounds(&self) -> Option<Rect> {
41 self.bounds
42 }
43 #[must_use]
44 pub const fn id(&self) -> PathId {
45 self.id
46 }
47 #[must_use]
51 pub fn contains(&self, point: Offset, rule: FillRule) -> bool {
52 let winding = self
53 .path
54 .winding(Point::new(f64::from(point.x), f64::from(point.y)));
55 match rule {
56 FillRule::NonZero => winding != 0,
57 FillRule::EvenOdd => winding.unsigned_abs() % 2 == 1,
58 }
59 }
60
61 pub(crate) fn from_bez_path(path: BezPath) -> Self {
65 let bounds = (!path.is_empty() && path.is_finite()).then(|| {
66 let bounds = path.bounding_box();
67 Rect::from_origin_size(
68 Offset::new(bounds.x0 as f32, bounds.y0 as f32),
69 incular_core::Size::new(
70 (bounds.x1 - bounds.x0) as f32,
71 (bounds.y1 - bounds.y0) as f32,
72 ),
73 )
74 });
75 Self {
76 id: next_path_id(),
77 path: Arc::new(path),
78 bounds,
79 }
80 }
81
82 #[must_use]
88 pub fn transformed(&self, transform: Transform) -> Self {
89 let mut path = (*self.path).clone();
90 path.apply_affine(transform.to_kurbo());
91 let bounds = (!path.is_empty() && path.is_finite()).then(|| {
92 let bounds = path.bounding_box();
93 Rect::from_origin_size(
94 Offset::new(bounds.x0 as f32, bounds.y0 as f32),
95 Size::new(
96 (bounds.x1 - bounds.x0) as f32,
97 (bounds.y1 - bounds.y0) as f32,
98 ),
99 )
100 });
101 Self {
102 id: next_path_id(),
103 path: Arc::new(path),
104 bounds,
105 }
106 }
107}
108impl Default for Path {
109 fn default() -> Self {
110 PathBuilder::default().build()
111 }
112}
113#[derive(Default)]
114pub struct PathBuilder {
115 path: BezPath,
116}
117impl PathBuilder {
118 pub fn move_to(&mut self, p: Offset) -> &mut Self {
119 self.path.move_to(point(p));
120 self
121 }
122 pub fn line_to(&mut self, p: Offset) -> &mut Self {
123 self.path.line_to(point(p));
124 self
125 }
126 pub fn quadratic_to(&mut self, c: Offset, p: Offset) -> &mut Self {
127 self.path.quad_to(point(c), point(p));
128 self
129 }
130 pub fn cubic_to(&mut self, a: Offset, b: Offset, p: Offset) -> &mut Self {
131 self.path.curve_to(point(a), point(b), point(p));
132 self
133 }
134 pub fn close(&mut self) -> &mut Self {
135 if !self.path.elements().is_empty() {
136 self.path.close_path();
137 }
138 self
139 }
140 #[must_use]
141 pub fn build(self) -> Path {
142 Path::from_bez_path(self.path)
143 }
144}
145
146fn point(offset: Offset) -> Point {
147 Point::new(f64::from(offset.x), f64::from(offset.y))
148}
149
150const CLIP_PATH_WORLD_TOLERANCE: f64 = 0.1;
153
154pub(crate) fn fallback_tolerance(world: Transform) -> f64 {
163 (CLIP_PATH_WORLD_TOLERANCE / f64::from(world.magnification_bound())).clamp(1e-3, 1.0)
164}
165
166pub(crate) fn rect_as_path(rect: Rect) -> Path {
169 let mut builder = PathBuilder::default();
170 builder
171 .move_to(rect.origin)
172 .line_to(Offset::new(rect.origin.x + rect.size.width, rect.origin.y))
173 .line_to(Offset::new(
174 rect.origin.x + rect.size.width,
175 rect.origin.y + rect.size.height,
176 ))
177 .line_to(Offset::new(rect.origin.x, rect.origin.y + rect.size.height))
178 .close();
179 builder.build()
180}
181
182pub(crate) fn rrect_as_path(rect: Rect, radii: CornerRadii, tolerance: f64) -> Path {
187 let shape = RoundedRect::new(
188 f64::from(rect.origin.x),
189 f64::from(rect.origin.y),
190 f64::from(rect.origin.x + rect.size.width),
191 f64::from(rect.origin.y + rect.size.height),
192 RoundedRectRadii::new(
193 f64::from(radii.top_left),
194 f64::from(radii.top_right),
195 f64::from(radii.bottom_right),
196 f64::from(radii.bottom_left),
197 ),
198 );
199 Path::from_bez_path(shape.to_path(tolerance))
200}
201
202pub(crate) fn ellipse_as_path(rect: Rect, tolerance: f64) -> Path {
206 let shape = Ellipse::new(
207 Point::new(
208 f64::from(rect.origin.x) + f64::from(rect.size.width) * 0.5,
209 f64::from(rect.origin.y) + f64::from(rect.size.height) * 0.5,
210 ),
211 Vec2::new(
212 f64::from(rect.size.width) * 0.5,
213 f64::from(rect.size.height) * 0.5,
214 ),
215 0.0,
216 );
217 Path::from_bez_path(shape.to_path(tolerance))
218}