1use std::{
2 cell::{RefCell, RefMut},
3 f32::consts::PI,
4 slice,
5};
6
7use crate::geometry::{Position, Transform2D, Vector};
8use rustybuzz::ttf_parser;
9
10mod cache;
11pub use cache::{Convexity, PathCache};
12
13// Length proportional to radius of a cubic bezier handle for 90deg arcs.
14const KAPPA90: f32 = 0.552_284_8; // 0.552_284_749_3;
15
16/// Specifies whether a shape is solid or a hole when adding it to a path.
17///
18/// The default value is `Solid`.
19#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Default)]
20pub enum Solidity {
21 /// The shape is solid (filled).
22 #[default]
23 Solid = 1,
24 /// The shape is a hole (not filled).
25 Hole = 2,
26}
27
28#[derive(Copy, Clone, Debug, Eq, PartialEq)]
29#[repr(u8)]
30#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
31pub enum PackedVerb {
32 MoveTo,
33 LineTo,
34 BezierTo,
35 Solid,
36 Hole,
37 Close,
38}
39
40/// A verb describes how to interpret one or more points to continue the countour
41/// of a [`Path`].
42#[derive(Copy, Clone, Debug)]
43pub enum Verb {
44 /// Terminates the current sub-path and defines the new current point by the
45 /// given x/y f32 coordinates.
46 MoveTo(f32, f32),
47 /// Describes that the contour of the path should continue as a line from the
48 /// current point to the given x/y f32 coordinates.
49 LineTo(f32, f32),
50 /// Describes that the contour of the path should continue as a cubie bezier segment from the
51 /// current point via two control points (as f32 pairs) to the point in the last f32 pair.
52 BezierTo(f32, f32, f32, f32, f32, f32),
53 /// Sets the current sub-path winding to be solid.
54 Solid,
55 /// Sets the current sub-path winding to be hole.
56 Hole,
57 /// Closes the current sub-path.
58 Close,
59}
60
61impl Verb {
62 fn num_coordinates(&self) -> usize {
63 match *self {
64 Self::MoveTo(..) => 1,
65 Self::LineTo(..) => 1,
66 Self::BezierTo(..) => 3,
67 Self::Solid => 0,
68 Self::Hole => 0,
69 Self::Close => 0,
70 }
71 }
72
73 fn from_packed(packed: &PackedVerb, coords: &[Position]) -> Self {
74 match *packed {
75 PackedVerb::MoveTo => Self::MoveTo(coords[0].x, coords[0].y),
76 PackedVerb::LineTo => Self::LineTo(coords[0].x, coords[0].y),
77 PackedVerb::BezierTo => Self::BezierTo(
78 coords[0].x,
79 coords[0].y,
80 coords[1].x,
81 coords[1].y,
82 coords[2].x,
83 coords[2].y,
84 ),
85 PackedVerb::Solid => Self::Solid,
86 PackedVerb::Hole => Self::Hole,
87 PackedVerb::Close => Self::Close,
88 }
89 }
90}
91
92/// A collection of verbs (`move_to()`, `line_to()`, `bezier_to()`, etc.)
93/// describing one or more contours.
94#[derive(Default, Clone, Debug)]
95#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
96pub struct Path {
97 verbs: Vec<PackedVerb>,
98 coords: Vec<Position>,
99 last_pos: Position,
100 dist_tol: f32,
101 #[cfg_attr(feature = "serde", serde(skip))]
102 pub(crate) cache: RefCell<Option<(u64, PathCache)>>,
103}
104
105impl Path {
106 /// Creates a new empty path with a distance tolerance of 0.01.
107 pub fn new() -> Self {
108 Self {
109 dist_tol: 0.01,
110 ..Default::default()
111 }
112 }
113
114 /// Returns the memory size in bytes used by the path.
115 pub fn size(&self) -> usize {
116 std::mem::size_of::<PackedVerb>() * self.verbs.len() + std::mem::size_of::<f32>() * self.coords.len()
117 }
118
119 /// Checks if the path is empty (contains no verbs).
120 pub fn is_empty(&self) -> bool {
121 self.verbs.is_empty()
122 }
123
124 /// Sets the distance tolerance used for path operations.
125 pub fn set_distance_tolerance(&mut self, value: f32) {
126 self.dist_tol = value;
127 }
128
129 /// Returns an iterator over the path's verbs.
130 pub fn verbs(&self) -> PathIter<'_> {
131 PathIter {
132 verbs: self.verbs.iter(),
133 coords: &self.coords,
134 }
135 }
136
137 pub(crate) fn cache<'a>(&'a self, transform: &Transform2D, tess_tol: f32, dist_tol: f32) -> RefMut<'a, PathCache> {
138 // The path cache saves a flattened and transformed version of the path. If client code calls
139 // (fill|stroke)_path repeatedly with the same Path under the same transform circumstances then it will be
140 // retrieved from cache. I'm not sure if transform.cache_key() is actually good enough for this
141 // and if it will produce the correct cache keys under different float edge cases.
142
143 let key = transform.cache_key();
144
145 // this shouldn't need a bool once non lexic lifetimes are stable
146 let mut needs_rebuild = true;
147
148 if let Some((transform_cache_key, _cache)) = &*self.cache.borrow() {
149 needs_rebuild = key != *transform_cache_key;
150 }
151
152 if needs_rebuild {
153 let path_cache = PathCache::new(self.verbs(), transform, tess_tol, dist_tol);
154 *self.cache.borrow_mut() = Some((key, path_cache));
155 }
156
157 RefMut::map(self.cache.borrow_mut(), |cache| &mut cache.as_mut().unwrap().1)
158 }
159
160 // Path funcs
161
162 /// Starts a new sub-path with the specified point as the first point.
163 pub fn move_to(&mut self, x: f32, y: f32) {
164 self.append(&[PackedVerb::MoveTo], &[Position { x, y }]);
165 }
166
167 /// Adds a line segment from the last point in the path to the specified point.
168 pub fn line_to(&mut self, x: f32, y: f32) {
169 self.append(&[PackedVerb::LineTo], &[Position { x, y }]);
170 }
171
172 /// Adds a cubic bezier segment from the last point in the path via two control points to the specified point.
173 pub fn bezier_to(&mut self, c1x: f32, c1y: f32, c2x: f32, c2y: f32, x: f32, y: f32) {
174 self.append(
175 &[PackedVerb::BezierTo],
176 &[
177 Position { x: c1x, y: c1y },
178 Position { x: c2x, y: c2y },
179 Position { x, y },
180 ],
181 );
182 }
183
184 /// Adds a quadratic bezier segment from the last point in the path via a control point to the specified point.
185 pub fn quad_to(&mut self, cx: f32, cy: f32, x: f32, y: f32) {
186 let pos0 = self.last_pos;
187 let cpos = Position { x: cx, y: cy };
188 let pos = Position { x, y };
189 let pos1 = pos0 + (cpos - pos0) * (2.0 / 3.0);
190 let pos2 = pos + (cpos - pos) * (2.0 / 3.0);
191
192 self.append(&[PackedVerb::BezierTo], &[pos1, pos2, pos]);
193 }
194
195 /// Closes the current sub-path with a line segment.
196 pub fn close(&mut self) {
197 self.append(&[PackedVerb::Close], &[]);
198 }
199
200 /// Sets the current sub-path winding, see [`Solidity`].
201 pub fn solidity(&mut self, solidity: Solidity) {
202 match solidity {
203 Solidity::Solid => self.append(&[PackedVerb::Solid], &[]),
204 Solidity::Hole => self.append(&[PackedVerb::Hole], &[]),
205 }
206 }
207
208 /// Creates new circle arc shaped sub-path. The arc center is at `cx`,`cy`, the arc radius is `r`,
209 /// and the arc is drawn from angle `a0` to `a1`, and swept in direction `dir` (Winding)
210 /// Angles are specified in radians.
211 pub fn arc(&mut self, cx: f32, cy: f32, r: f32, a0: f32, a1: f32, dir: Solidity) {
212 let cpos = Position { x: cx, y: cy };
213
214 let mut da = a1 - a0;
215
216 if dir == Solidity::Hole {
217 if da.abs() >= PI * 2.0 {
218 da = PI * 2.0;
219 } else {
220 while da < 0.0 {
221 da += PI * 2.0
222 }
223 }
224 } else if da.abs() >= PI * 2.0 {
225 da = -PI * 2.0;
226 } else {
227 while da > 0.0 {
228 da -= PI * 2.0
229 }
230 }
231
232 // Split arc into max 90 degree segments.
233 let ndivs = ((da.abs() / (PI * 0.5) + 0.5) as i32).clamp(1, 5);
234 let hda = (da / ndivs as f32) / 2.0;
235 let mut kappa = (4.0 / 3.0 * (1.0 - hda.cos()) / hda.sin()).abs();
236
237 let mut commands = Vec::with_capacity(ndivs as usize);
238 let mut coords = Vec::with_capacity(ndivs as usize);
239
240 if dir == Solidity::Solid {
241 kappa = -kappa;
242 }
243
244 let (mut ppos, mut ptanpos) = (Position { x: 0.0, y: 0.0 }, Vector::zero());
245
246 for i in 0..=ndivs {
247 let a = a0 + da * (i as f32 / ndivs as f32);
248 let dpos = Vector::from_angle(a);
249 let pos = cpos + dpos * r;
250 let tanpos = -dpos.orthogonal() * r * kappa;
251
252 if i == 0 {
253 let first_move = if self.verbs.is_empty() {
254 PackedVerb::MoveTo
255 } else {
256 PackedVerb::LineTo
257 };
258
259 commands.push(first_move);
260 coords.extend_from_slice(&[pos]);
261 } else {
262 commands.push(PackedVerb::BezierTo);
263 let pos1 = ppos + ptanpos;
264 let pos2 = pos - tanpos;
265 coords.extend_from_slice(&[pos1, pos2, pos]);
266 }
267
268 ppos = pos;
269 ptanpos = tanpos;
270 }
271
272 self.append(&commands, &coords);
273 }
274
275 /// Adds an arc segment at the corner defined by the last path point and two specified points.
276 pub fn arc_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, radius: f32) {
277 if self.verbs.is_empty() {
278 return;
279 }
280
281 let pos0 = self.last_pos;
282 let pos1 = Position { x: x1, y: y1 };
283 let pos2 = Position { x: x2, y: y2 };
284
285 // Handle degenerate cases.
286 if Position::equals(pos0, pos1, self.dist_tol)
287 || Position::equals(pos1, pos2, self.dist_tol)
288 || Position::segment_distance(pos1, pos0, pos2) < self.dist_tol * self.dist_tol
289 || radius < self.dist_tol
290 {
291 self.line_to(pos1.x, pos1.y);
292 }
293
294 let mut dpos0 = pos0 - pos1;
295 let mut dpos1 = pos2 - pos1;
296
297 dpos0.normalize();
298 dpos1.normalize();
299
300 let a = dpos0.dot(dpos1).acos();
301 let d = radius / (a / 2.0).tan();
302
303 if d > 10000.0 {
304 return self.line_to(pos1.x, pos1.y);
305 }
306
307 let (cpos, a0, a1, dir);
308
309 if dpos0.cross(dpos1) > 0.0 {
310 cpos = pos1 + dpos0 * d + dpos0.orthogonal() * radius;
311 a0 = dpos0.angle();
312 a1 = (-dpos1).angle();
313 dir = Solidity::Hole;
314 } else {
315 cpos = pos1 + dpos0 * d - dpos0.orthogonal() * radius;
316 a0 = (-dpos0).angle();
317 a1 = dpos1.angle();
318 dir = Solidity::Solid;
319 }
320
321 self.arc(cpos.x, cpos.y, radius, a0 + PI / 2.0, a1 + PI / 2.0, dir);
322 }
323
324 /// Creates a new rectangle shaped sub-path.
325 pub fn rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
326 self.append(
327 &[
328 PackedVerb::MoveTo,
329 PackedVerb::LineTo,
330 PackedVerb::LineTo,
331 PackedVerb::LineTo,
332 PackedVerb::Close,
333 ],
334 &{
335 let hoffset = Vector::x(w);
336 let voffset = Vector::y(h);
337
338 let tl = Position { x, y };
339 let tr = tl + hoffset;
340 let br = tr + voffset;
341 let bl = tl + voffset;
342
343 [tl, bl, br, tr]
344 },
345 );
346 }
347
348 /// Creates a new rounded rectangle shaped sub-path.
349 pub fn rounded_rect(&mut self, x: f32, y: f32, w: f32, h: f32, r: f32) {
350 self.rounded_rect_varying(x, y, w, h, r, r, r, r);
351 }
352
353 /// Creates a new rounded rectangle shaped sub-path with varying radii for each corner.
354 pub fn rounded_rect_varying(
355 &mut self,
356 x: f32,
357 y: f32,
358 w: f32,
359 h: f32,
360 rad_top_left: f32,
361 rad_top_right: f32,
362 rad_bottom_right: f32,
363 rad_bottom_left: f32,
364 ) {
365 if rad_top_left < 0.1 && rad_top_right < 0.1 && rad_bottom_right < 0.1 && rad_bottom_left < 0.1 {
366 self.rect(x, y, w, h);
367 } else {
368 let halfw = w.abs() * 0.5;
369 let halfh = h.abs() * 0.5;
370
371 let rx_bl = rad_bottom_left.min(halfw) * w.signum();
372 let ry_bl = rad_bottom_left.min(halfh) * h.signum();
373
374 let rx_br = rad_bottom_right.min(halfw) * w.signum();
375 let ry_br = rad_bottom_right.min(halfh) * h.signum();
376
377 let rx_tr = rad_top_right.min(halfw) * w.signum();
378 let ry_tr = rad_top_right.min(halfh) * h.signum();
379
380 let rx_tl = rad_top_left.min(halfw) * w.signum();
381 let ry_tl = rad_top_left.min(halfh) * h.signum();
382
383 self.append(
384 &[
385 PackedVerb::MoveTo,
386 PackedVerb::LineTo,
387 PackedVerb::BezierTo,
388 PackedVerb::LineTo,
389 PackedVerb::BezierTo,
390 PackedVerb::LineTo,
391 PackedVerb::BezierTo,
392 PackedVerb::LineTo,
393 PackedVerb::BezierTo,
394 PackedVerb::Close,
395 ],
396 &[
397 Position { x, y: y + ry_tl },
398 Position { x, y: y + h - ry_bl },
399 //
400 Position {
401 x,
402 y: y + h - ry_bl * (1.0 - KAPPA90),
403 },
404 Position {
405 x: x + rx_bl * (1.0 - KAPPA90),
406 y: y + h,
407 },
408 Position { x: x + rx_bl, y: y + h },
409 //
410 Position {
411 x: x + w - rx_br,
412 y: y + h,
413 },
414 //
415 Position {
416 x: x + w - rx_br * (1.0 - KAPPA90),
417 y: y + h,
418 },
419 Position {
420 x: x + w,
421 y: y + h - ry_br * (1.0 - KAPPA90),
422 },
423 Position {
424 x: x + w,
425 y: y + h - ry_br,
426 },
427 //
428 Position { x: x + w, y: y + ry_tr },
429 //
430 Position {
431 x: x + w,
432 y: y + ry_tr * (1.0 - KAPPA90),
433 },
434 Position {
435 x: x + w - rx_tr * (1.0 - KAPPA90),
436 y,
437 },
438 Position { x: x + w - rx_tr, y },
439 //
440 Position { x: x + rx_tl, y },
441 //
442 Position {
443 x: x + rx_tl * (1.0 - KAPPA90),
444 y,
445 },
446 Position {
447 x,
448 y: y + ry_tl * (1.0 - KAPPA90),
449 },
450 Position { x, y: y + ry_tl },
451 ],
452 );
453 }
454 }
455
456 /// Creates a new ellipse shaped sub-path.
457 pub fn ellipse(&mut self, cx: f32, cy: f32, rx: f32, ry: f32) {
458 self.append(
459 &[
460 PackedVerb::MoveTo,
461 PackedVerb::BezierTo,
462 PackedVerb::BezierTo,
463 PackedVerb::BezierTo,
464 PackedVerb::BezierTo,
465 PackedVerb::Close,
466 ],
467 &{
468 let cpos = Position { x: cx, y: cy };
469 let hoffset = Vector::x(rx);
470 let voffset = Vector::y(ry);
471 [
472 cpos - hoffset,
473 cpos - hoffset + voffset * KAPPA90,
474 cpos - hoffset * KAPPA90 + voffset,
475 cpos + voffset,
476 cpos + hoffset * KAPPA90 + voffset,
477 cpos + hoffset + voffset * KAPPA90,
478 cpos + hoffset,
479 cpos + hoffset - voffset * KAPPA90,
480 cpos + hoffset * KAPPA90 - voffset,
481 cpos - voffset,
482 cpos - hoffset * KAPPA90 - voffset,
483 cpos - hoffset - voffset * KAPPA90,
484 cpos - hoffset,
485 ]
486 },
487 );
488 }
489
490 /// Creates a new circle shaped sub-path.
491 pub fn circle(&mut self, cx: f32, cy: f32, r: f32) {
492 self.ellipse(cx, cy, r, r);
493 }
494
495 /// Appends a slice of verbs and coordinates to the path.
496 fn append(&mut self, verbs: &[PackedVerb], coords: &[Position]) {
497 if !coords.is_empty() {
498 self.last_pos = coords[coords.len() - 1];
499 }
500
501 self.verbs.extend_from_slice(verbs);
502 self.coords.extend_from_slice(coords);
503 }
504}
505
506/// An iterator over the verbs and coordinates of a path.
507pub struct PathIter<'a> {
508 verbs: slice::Iter<'a, PackedVerb>,
509 coords: &'a [Position],
510}
511
512impl<'a> Iterator for PathIter<'a> {
513 type Item = Verb;
514
515 fn next(&mut self) -> Option<Self::Item> {
516 if let Some(verb: &'a PackedVerb) = self.verbs.next() {
517 let verb: Verb = Verb::from_packed(packed:verb, self.coords);
518 let num_coords: usize = verb.num_coordinates();
519 self.coords = &self.coords[num_coords..];
520 Some(verb)
521 } else {
522 None
523 }
524 }
525}
526
527impl ttf_parser::OutlineBuilder for Path {
528 fn move_to(&mut self, x: f32, y: f32) {
529 self.move_to(x, y);
530 }
531
532 fn line_to(&mut self, x: f32, y: f32) {
533 self.line_to(x, y);
534 }
535
536 fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
537 self.quad_to(cx:x1, cy:y1, x, y);
538 }
539
540 fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
541 self.bezier_to(c1x:x1, c1y:y1, c2x:x2, c2y:y2, x, y);
542 }
543
544 fn close(&mut self) {
545 self.close();
546 }
547}
548