1 | use std::{ |
2 | cell::{RefCell, RefMut}, |
3 | f32::consts::PI, |
4 | slice, |
5 | }; |
6 | |
7 | use crate::geometry::{Position, Transform2D, Vector}; |
8 | use rustybuzz::ttf_parser; |
9 | |
10 | mod cache; |
11 | pub use cache::{Convexity, PathCache}; |
12 | |
13 | // Length proportional to radius of a cubic bezier handle for 90deg arcs. |
14 | const 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)] |
20 | pub 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))] |
31 | pub 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)] |
43 | pub 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 | |
61 | impl 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))] |
96 | pub 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 | |
105 | impl 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. |
507 | pub struct PathIter<'a> { |
508 | verbs: slice::Iter<'a, PackedVerb>, |
509 | coords: &'a [Position], |
510 | } |
511 | |
512 | impl<'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 | |
527 | impl 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 | |