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