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 | /// Used to specify Solid/Hole when adding shapes to a path. |
17 | #[derive (Copy, Clone, Debug, Eq, PartialEq, PartialOrd)] |
18 | pub enum Solidity { |
19 | Solid = 1, |
20 | Hole = 2, |
21 | } |
22 | |
23 | impl 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))] |
32 | pub enum PackedVerb { |
33 | MoveTo, |
34 | LineTo, |
35 | BezierTo, |
36 | Solid, |
37 | Hole, |
38 | Close, |
39 | } |
40 | |
41 | #[derive (Copy, Clone, Debug)] |
42 | pub 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 | |
51 | impl 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))] |
86 | pub 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 | |
95 | impl 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 | |
494 | pub struct PathIter<'a> { |
495 | verbs: slice::Iter<'a, PackedVerb>, |
496 | coords: &'a [Position], |
497 | } |
498 | |
499 | impl<'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 | |
514 | impl 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 | |