Skip to main content

femto_g/
path.rs

1use std::{
2    cell::{RefCell, RefMut},
3    f32::consts::PI,
4    slice,
5};
6
7use crate::geometry::{Position, Transform2D, Vector};
8#[cfg(feature = "textlayout")]
9use rustybuzz::ttf_parser;
10
11mod cache;
12pub use cache::{Convexity, PathCache};
13
14// Length proportional to radius of a cubic bezier handle for 90deg arcs.
15const KAPPA90: f32 = 0.552_284_8; // 0.552_284_749_3;
16
17#[inline]
18fn add3(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
19    std::array::from_fn(|i| a[i] + b[i])
20}
21
22#[inline]
23fn sub3(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
24    std::array::from_fn(|i| a[i] - b[i])
25}
26
27#[inline]
28fn scale3(v: [f32; 3], s: f32) -> [f32; 3] {
29    v.map(|c| c * s)
30}
31
32#[inline]
33fn dot3(a: [f32; 3], b: [f32; 3]) -> f32 {
34    a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
35}
36
37fn cross3(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
38    [
39        a[1] * b[2] - a[2] * b[1],
40        a[2] * b[0] - a[0] * b[2],
41        a[0] * b[1] - a[1] * b[0],
42    ]
43}
44
45fn normalize3(v: [f32; 3]) -> [f32; 3] {
46    let len = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
47    if len > 1e-6 {
48        v.map(|c| c / len)
49    } else {
50        [0.0, 0.0, 1.0]
51    }
52}
53
54fn build_basis(n: &[f32; 3]) -> ([f32; 3], [f32; 3]) {
55    let t = if n[0].abs() < 0.9 {
56        [1.0, 0.0, 0.0]
57    } else {
58        [0.0, 1.0, 0.0]
59    };
60    let u = normalize3(cross3(t, *n));
61    let v = cross3(*n, u);
62    (u, v)
63}
64
65/// Specifies whether a shape is solid or a hole when adding it to a path.
66///
67/// The default value is `Solid`.
68#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Default)]
69pub enum Solidity {
70    /// The shape is solid (filled).
71    #[default]
72    Solid = 1,
73    /// The shape is a hole (not filled).
74    Hole = 2,
75}
76
77#[derive(Copy, Clone, Debug, Eq, PartialEq)]
78#[repr(u8)]
79#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
80pub enum PackedVerb {
81    MoveTo,
82    LineTo,
83    BezierTo,
84    Solid,
85    Hole,
86    Close,
87}
88
89/// A verb describes how to interpret one or more points to continue the contour
90/// of a [`Path`].
91#[derive(Copy, Clone, Debug)]
92pub enum Verb<const N: usize = 2> {
93    /// Terminates the current sub-path and defines the new current point.
94    MoveTo([f32; N]),
95    /// Describes that the contour of the path should continue as a line from the
96    /// current point to the given point.
97    LineTo([f32; N]),
98    /// Describes that the contour of the path should continue as a cubic bezier segment from the
99    /// current point via two control points to the endpoint.
100    BezierTo([f32; N], [f32; N], [f32; N]),
101    /// Sets the current sub-path winding to be solid.
102    Solid,
103    /// Sets the current sub-path winding to be hole.
104    Hole,
105    /// Closes the current sub-path.
106    Close,
107}
108
109impl<const N: usize> Verb<N> {
110    fn num_coordinates(&self) -> usize {
111        match *self {
112            Self::MoveTo(..) => 1,
113            Self::LineTo(..) => 1,
114            Self::BezierTo(..) => 3,
115            Self::Solid => 0,
116            Self::Hole => 0,
117            Self::Close => 0,
118        }
119    }
120
121    fn from_packed(packed: &PackedVerb, coords: &[[f32; N]]) -> Self {
122        match *packed {
123            PackedVerb::MoveTo => Self::MoveTo(coords[0]),
124            PackedVerb::LineTo => Self::LineTo(coords[0]),
125            PackedVerb::BezierTo => Self::BezierTo(coords[0], coords[1], coords[2]),
126            PackedVerb::Solid => Self::Solid,
127            PackedVerb::Hole => Self::Hole,
128            PackedVerb::Close => Self::Close,
129        }
130    }
131}
132
133/// A collection of verbs (`move_to()`, `line_to()`, `bezier_to()`, etc.)
134/// describing one or more contours.
135///
136/// The const generic `N` specifies the number of dimensions (default 2).
137/// Use `Path` (or `Path<2>`) for standard 2D drawing, and `Path<3>` for 3D
138/// geometry that can be projected to 2D via [`map()`](Path::map).
139///
140/// # 3D Example
141/// ```
142/// use femto_g::Path;
143///
144/// let mut path3d = Path::<3>::new();
145/// path3d.move_to([0.0, 0.0, 0.0]);
146/// path3d.line_to([1.0, 1.0, 1.0]);
147/// path3d.bezier_to([0.5, 0.0, 0.5], [0.5, 1.0, 0.5], [1.0, 1.0, 0.0]);
148/// path3d.close();
149///
150/// let path2d = path3d.map(|[x, y, z]| {
151///     let scale = 400.0 / (400.0 + z);
152///     [x * scale, y * scale]
153/// });
154/// ```
155#[derive(Clone, Debug)]
156#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
157pub struct Path<const N: usize = 2> {
158    verbs: Vec<PackedVerb>,
159    coords: Vec<[f32; N]>,
160    last_pos: [f32; N],
161    dist_tol: f32,
162    #[cfg_attr(feature = "serde", serde(skip))]
163    pub(crate) cache: RefCell<Option<(u64, PathCache)>>,
164}
165
166impl<const N: usize> Default for Path<N> {
167    fn default() -> Self {
168        Self {
169            verbs: Vec::new(),
170            coords: Vec::new(),
171            last_pos: [0.0; N],
172            dist_tol: 0.01,
173            cache: RefCell::new(None),
174        }
175    }
176}
177
178impl<const N: usize> Path<N> {
179    /// Creates a new empty path with a distance tolerance of 0.01.
180    pub fn new() -> Self {
181        Self::default()
182    }
183
184    /// Returns the memory size in bytes used by the path.
185    pub fn size(&self) -> usize {
186        std::mem::size_of::<PackedVerb>() * self.verbs.len() + std::mem::size_of::<[f32; N]>() * self.coords.len()
187    }
188
189    /// Checks if the path is empty (contains no verbs).
190    pub fn is_empty(&self) -> bool {
191        self.verbs.is_empty()
192    }
193
194    /// Sets the distance tolerance used for path operations.
195    pub fn set_distance_tolerance(&mut self, value: f32) {
196        self.dist_tol = value;
197    }
198
199    /// Returns an iterator over the path's verbs.
200    pub fn verbs(&self) -> PathIter<'_, N> {
201        PathIter {
202            verbs: self.verbs.iter(),
203            coords: &self.coords,
204        }
205    }
206
207    /// Starts a new sub-path with the specified point as the first point.
208    pub fn move_to(&mut self, pos: impl Into<[f32; N]>) {
209        let pos = pos.into();
210        self.append(&[PackedVerb::MoveTo], &[pos]);
211    }
212
213    /// Adds a line segment from the last point in the path to the specified point.
214    pub fn line_to(&mut self, pos: impl Into<[f32; N]>) {
215        let pos = pos.into();
216        self.append(&[PackedVerb::LineTo], &[pos]);
217    }
218
219    /// Adds a cubic bezier segment from the last point in the path via two control points to the specified point.
220    pub fn bezier_to(
221        &mut self,
222        control1: impl Into<[f32; N]>,
223        control2: impl Into<[f32; N]>,
224        pos: impl Into<[f32; N]>,
225    ) {
226        self.append(&[PackedVerb::BezierTo], &[control1.into(), control2.into(), pos.into()]);
227    }
228
229    /// Adds a quadratic bezier segment from the last point in the path via a control point to the specified point.
230    pub fn quad_to(&mut self, control: impl Into<[f32; N]>, pos: impl Into<[f32; N]>) {
231        let control = control.into();
232        let pos = pos.into();
233        let pos0 = self.last_pos;
234        let pos1 = std::array::from_fn(|i| pos0[i] + (control[i] - pos0[i]) * (2.0 / 3.0));
235        let pos2 = std::array::from_fn(|i| pos[i] + (control[i] - pos[i]) * (2.0 / 3.0));
236        self.append(&[PackedVerb::BezierTo], &[pos1, pos2, pos]);
237    }
238
239    /// Closes the current sub-path with a line segment.
240    pub fn close(&mut self) {
241        self.append(&[PackedVerb::Close], &[]);
242    }
243
244    /// Sets the current sub-path winding, see [`Solidity`].
245    pub fn solidity(&mut self, solidity: Solidity) {
246        match solidity {
247            Solidity::Solid => self.append(&[PackedVerb::Solid], &[]),
248            Solidity::Hole => self.append(&[PackedVerb::Hole], &[]),
249        }
250    }
251
252    /// Transforms all coordinates using the given function, producing a path in a
253    /// (potentially different) number of dimensions. This is useful for projecting
254    /// 3D paths to 2D for rendering.
255    ///
256    /// ```
257    /// use femto_g::Path;
258    ///
259    /// let mut path3d = Path::<3>::new();
260    /// path3d.move_to([10.0, 20.0, 30.0]);
261    /// path3d.line_to([40.0, 50.0, 60.0]);
262    ///
263    /// let path2d: Path<2> = path3d.map(|[x, y, _z]| [x, y]);
264    /// ```
265    pub fn map<const M: usize>(&self, f: impl Fn([f32; N]) -> [f32; M]) -> Path<M> {
266        Path {
267            verbs: self.verbs.clone(),
268            coords: self.coords.iter().map(|c| f(*c)).collect(),
269            last_pos: f(self.last_pos),
270            dist_tol: self.dist_tol,
271            cache: RefCell::new(None),
272        }
273    }
274
275    fn append(&mut self, verbs: &[PackedVerb], coords: &[[f32; N]]) {
276        if !coords.is_empty() {
277            self.last_pos = coords[coords.len() - 1];
278        }
279
280        self.verbs.extend_from_slice(verbs);
281        self.coords.extend_from_slice(coords);
282    }
283}
284
285impl Path<2> {
286    pub(crate) fn cache<'a>(&'a self, transform: &Transform2D, tess_tol: f32, dist_tol: f32) -> RefMut<'a, PathCache> {
287        let key = transform.cache_key();
288
289        let needs_rebuild = if let Some((transform_cache_key, _cache)) = &*self.cache.borrow() {
290            key != *transform_cache_key
291        } else {
292            true
293        };
294
295        if needs_rebuild {
296            let path_cache = PathCache::new(self.verbs(), transform, tess_tol, dist_tol);
297            *self.cache.borrow_mut() = Some((key, path_cache));
298        }
299
300        RefMut::map(self.cache.borrow_mut(), |cache| &mut cache.as_mut().unwrap().1)
301    }
302
303    /// Creates new circle arc shaped sub-path. The arc center is at `center`, the arc radius is `r`,
304    /// and the arc is drawn from angle `a0` to `a1`, and swept in direction `dir` (Winding)
305    /// Angles are specified in radians.
306    pub fn arc(&mut self, center: impl Into<[f32; 2]>, r: f32, a0: f32, a1: f32, dir: Solidity) {
307        let [cx, cy] = center.into();
308        let cpos = Position { x: cx, y: cy };
309
310        let mut da = a1 - a0;
311
312        if dir == Solidity::Hole {
313            if da.abs() >= PI * 2.0 {
314                da = PI * 2.0;
315            } else {
316                while da < 0.0 {
317                    da += PI * 2.0
318                }
319            }
320        } else if da.abs() >= PI * 2.0 {
321            da = -PI * 2.0;
322        } else {
323            while da > 0.0 {
324                da -= PI * 2.0
325            }
326        }
327
328        // Split arc into max 90 degree segments.
329        let ndivs = ((da.abs() / (PI * 0.5) + 0.5) as i32).clamp(1, 5);
330        let hda = (da / ndivs as f32) / 2.0;
331        let mut kappa = (4.0 / 3.0 * (1.0 - hda.cos()) / hda.sin()).abs();
332
333        let mut commands = Vec::with_capacity(ndivs as usize);
334
335        if dir == Solidity::Solid {
336            kappa = -kappa;
337        }
338
339        let (mut ppos, mut ptanpos) = (Position { x: 0.0, y: 0.0 }, Vector::zero());
340
341        let mut pos_coords: Vec<Position> = Vec::with_capacity(ndivs as usize);
342
343        for i in 0..=ndivs {
344            let a = a0 + da * (i as f32 / ndivs as f32);
345            let dpos = Vector::from_angle(a);
346            let pos = cpos + dpos * r;
347            let tanpos = -dpos.orthogonal() * r * kappa;
348
349            if i == 0 {
350                let first_move = if self.verbs.is_empty() {
351                    PackedVerb::MoveTo
352                } else {
353                    PackedVerb::LineTo
354                };
355
356                commands.push(first_move);
357                pos_coords.push(pos);
358            } else {
359                commands.push(PackedVerb::BezierTo);
360                pos_coords.extend_from_slice(&[ppos + ptanpos, pos - tanpos, pos]);
361            }
362
363            ppos = pos;
364            ptanpos = tanpos;
365        }
366
367        let coords: Vec<[f32; 2]> = pos_coords.into_iter().map(Into::into).collect();
368        self.append(&commands, &coords);
369    }
370
371    /// Adds an arc segment at the corner defined by the last path point and two specified points.
372    pub fn arc_to(&mut self, pos1: impl Into<[f32; 2]>, pos2: impl Into<[f32; 2]>, radius: f32) {
373        if self.verbs.is_empty() {
374            return;
375        }
376
377        let pos0: Position = self.last_pos.into();
378        let pos1: Position = pos1.into().into();
379        let pos2: Position = pos2.into().into();
380
381        if Position::equals(pos0, pos1, self.dist_tol)
382            || Position::equals(pos1, pos2, self.dist_tol)
383            || Position::segment_distance(pos1, pos0, pos2) < self.dist_tol * self.dist_tol
384            || radius < self.dist_tol
385        {
386            self.line_to(<[f32; 2]>::from(pos1));
387            return;
388        }
389
390        let mut dpos0 = pos0 - pos1;
391        let mut dpos1 = pos2 - pos1;
392
393        dpos0.normalize();
394        dpos1.normalize();
395
396        let a = dpos0.dot(dpos1).acos();
397        let d = radius / (a / 2.0).tan();
398
399        if d > 10000.0 {
400            self.line_to(<[f32; 2]>::from(pos1));
401            return;
402        }
403
404        let (cpos, a0, a1, dir);
405
406        if dpos0.cross(dpos1) > 0.0 {
407            cpos = pos1 + dpos0 * d + dpos0.orthogonal() * radius;
408            a0 = dpos0.angle();
409            a1 = (-dpos1).angle();
410            dir = Solidity::Hole;
411        } else {
412            cpos = pos1 + dpos0 * d - dpos0.orthogonal() * radius;
413            a0 = (-dpos0).angle();
414            a1 = dpos1.angle();
415            dir = Solidity::Solid;
416        }
417
418        self.arc(<[f32; 2]>::from(cpos), radius, a0 + PI / 2.0, a1 + PI / 2.0, dir);
419    }
420
421    /// Creates a new rectangle shaped sub-path.
422    pub fn rect(&mut self, pos: impl Into<[f32; 2]>, size: impl Into<[f32; 2]>) {
423        let [x, y] = pos.into();
424        let [w, h] = size.into();
425        self.append(
426            &[
427                PackedVerb::MoveTo,
428                PackedVerb::LineTo,
429                PackedVerb::LineTo,
430                PackedVerb::LineTo,
431                PackedVerb::Close,
432            ],
433            &[[x, y], [x, y + h], [x + w, y + h], [x + w, y]],
434        );
435    }
436
437    /// Creates a new rounded rectangle shaped sub-path.
438    pub fn rounded_rect(&mut self, pos: impl Into<[f32; 2]>, size: impl Into<[f32; 2]>, r: f32) {
439        let pos = pos.into();
440        let size = size.into();
441        self.rounded_rect_varying(pos, size, r, r, r, r);
442    }
443
444    /// Creates a new rounded rectangle shaped sub-path with varying radii for each corner.
445    pub fn rounded_rect_varying(
446        &mut self,
447        pos: impl Into<[f32; 2]>,
448        size: impl Into<[f32; 2]>,
449        rad_top_left: f32,
450        rad_top_right: f32,
451        rad_bottom_right: f32,
452        rad_bottom_left: f32,
453    ) {
454        let [x, y] = pos.into();
455        let [w, h] = size.into();
456
457        if rad_top_left < 0.1 && rad_top_right < 0.1 && rad_bottom_right < 0.1 && rad_bottom_left < 0.1 {
458            self.rect([x, y], [w, h]);
459        } else {
460            let halfw = w.abs() * 0.5;
461            let halfh = h.abs() * 0.5;
462
463            let rx_bl = rad_bottom_left.min(halfw) * w.signum();
464            let ry_bl = rad_bottom_left.min(halfh) * h.signum();
465
466            let rx_br = rad_bottom_right.min(halfw) * w.signum();
467            let ry_br = rad_bottom_right.min(halfh) * h.signum();
468
469            let rx_tr = rad_top_right.min(halfw) * w.signum();
470            let ry_tr = rad_top_right.min(halfh) * h.signum();
471
472            let rx_tl = rad_top_left.min(halfw) * w.signum();
473            let ry_tl = rad_top_left.min(halfh) * h.signum();
474
475            self.append(
476                &[
477                    PackedVerb::MoveTo,
478                    PackedVerb::LineTo,
479                    PackedVerb::BezierTo,
480                    PackedVerb::LineTo,
481                    PackedVerb::BezierTo,
482                    PackedVerb::LineTo,
483                    PackedVerb::BezierTo,
484                    PackedVerb::LineTo,
485                    PackedVerb::BezierTo,
486                    PackedVerb::Close,
487                ],
488                &[
489                    [x, y + ry_tl],
490                    [x, y + h - ry_bl],
491                    [x, y + h - ry_bl * (1.0 - KAPPA90)],
492                    [x + rx_bl * (1.0 - KAPPA90), y + h],
493                    [x + rx_bl, y + h],
494                    [x + w - rx_br, y + h],
495                    [x + w - rx_br * (1.0 - KAPPA90), y + h],
496                    [x + w, y + h - ry_br * (1.0 - KAPPA90)],
497                    [x + w, y + h - ry_br],
498                    [x + w, y + ry_tr],
499                    [x + w, y + ry_tr * (1.0 - KAPPA90)],
500                    [x + w - rx_tr * (1.0 - KAPPA90), y],
501                    [x + w - rx_tr, y],
502                    [x + rx_tl, y],
503                    [x + rx_tl * (1.0 - KAPPA90), y],
504                    [x, y + ry_tl * (1.0 - KAPPA90)],
505                    [x, y + ry_tl],
506                ],
507            );
508        }
509    }
510
511    /// Creates a new ellipse shaped sub-path.
512    pub fn ellipse(&mut self, center: impl Into<[f32; 2]>, radii: impl Into<[f32; 2]>) {
513        let [cx, cy] = center.into();
514        let [rx, ry] = radii.into();
515        let kx = rx * KAPPA90;
516        let ky = ry * KAPPA90;
517        self.append(
518            &[
519                PackedVerb::MoveTo,
520                PackedVerb::BezierTo,
521                PackedVerb::BezierTo,
522                PackedVerb::BezierTo,
523                PackedVerb::BezierTo,
524                PackedVerb::Close,
525            ],
526            &[
527                [cx - rx, cy],
528                [cx - rx, cy + ky],
529                [cx - kx, cy + ry],
530                [cx, cy + ry],
531                [cx + kx, cy + ry],
532                [cx + rx, cy + ky],
533                [cx + rx, cy],
534                [cx + rx, cy - ky],
535                [cx + kx, cy - ry],
536                [cx, cy - ry],
537                [cx - kx, cy - ry],
538                [cx - rx, cy - ky],
539                [cx - rx, cy],
540            ],
541        );
542    }
543
544    /// Creates a new circle shaped sub-path.
545    pub fn circle(&mut self, center: impl Into<[f32; 2]>, r: f32) {
546        let center = center.into();
547        self.ellipse(center, [r, r]);
548    }
549}
550
551/// Extension trait providing 3D-specific shape construction methods for [`Path<3>`].
552///
553/// These methods require a `normal` parameter to define the plane in which
554/// the shape lies. Use [`PathExt3d`] with `Path<3>` to access them.
555pub trait PathExt3d {
556    /// Creates a new circle shaped sub-path in the plane defined by `normal`.
557    fn circle(&mut self, center: impl Into<[f32; 3]>, normal: impl Into<[f32; 3]>, r: f32);
558
559    /// Creates a new ellipse shaped sub-path in the plane defined by `normal`.
560    /// The ellipse axes are oriented along the basis vectors of the plane.
561    fn ellipse(&mut self, center: impl Into<[f32; 3]>, normal: impl Into<[f32; 3]>, radii: impl Into<[f32; 2]>);
562
563    /// Creates new circle arc shaped sub-path. The arc lies in the plane defined
564    /// by `normal`. Angles are measured from the first basis vector in the plane
565    /// and specified in radians.
566    fn arc(
567        &mut self,
568        center: impl Into<[f32; 3]>,
569        normal: impl Into<[f32; 3]>,
570        r: f32,
571        a0: f32,
572        a1: f32,
573        dir: Solidity,
574    );
575
576    /// Adds an arc segment at the corner defined by the last path point and two
577    /// specified points. The arc plane is determined by the three points.
578    fn arc_to(&mut self, pos1: impl Into<[f32; 3]>, pos2: impl Into<[f32; 3]>, radius: f32);
579}
580
581impl PathExt3d for Path<3> {
582    fn circle(&mut self, center: impl Into<[f32; 3]>, normal: impl Into<[f32; 3]>, r: f32) {
583        self.ellipse(center, normal, [r, r]);
584    }
585
586    fn ellipse(&mut self, center: impl Into<[f32; 3]>, normal: impl Into<[f32; 3]>, radii: impl Into<[f32; 2]>) {
587        let center = center.into();
588        let [rx, ry] = radii.into();
589        let normal = normalize3(normal.into());
590        let (u, v) = build_basis(&normal);
591
592        let kx = rx * KAPPA90;
593        let ky = ry * KAPPA90;
594
595        let p = |xu: f32, yv: f32| -> [f32; 3] { add3(center, add3(scale3(u, xu), scale3(v, yv))) };
596
597        self.append(
598            &[
599                PackedVerb::MoveTo,
600                PackedVerb::BezierTo,
601                PackedVerb::BezierTo,
602                PackedVerb::BezierTo,
603                PackedVerb::BezierTo,
604                PackedVerb::Close,
605            ],
606            &[
607                p(-rx, 0.0),
608                p(-rx, ky),
609                p(-kx, ry),
610                p(0.0, ry),
611                p(kx, ry),
612                p(rx, ky),
613                p(rx, 0.0),
614                p(rx, -ky),
615                p(kx, -ry),
616                p(0.0, -ry),
617                p(-kx, -ry),
618                p(-rx, -ky),
619                p(-rx, 0.0),
620            ],
621        );
622    }
623
624    fn arc(
625        &mut self,
626        center: impl Into<[f32; 3]>,
627        normal: impl Into<[f32; 3]>,
628        r: f32,
629        a0: f32,
630        a1: f32,
631        dir: Solidity,
632    ) {
633        let center = center.into();
634        let normal = normalize3(normal.into());
635        let (u, v) = build_basis(&normal);
636
637        let mut da = a1 - a0;
638
639        if dir == Solidity::Hole {
640            if da.abs() >= PI * 2.0 {
641                da = PI * 2.0;
642            } else {
643                while da < 0.0 {
644                    da += PI * 2.0
645                }
646            }
647        } else if da.abs() >= PI * 2.0 {
648            da = -PI * 2.0;
649        } else {
650            while da > 0.0 {
651                da -= PI * 2.0
652            }
653        }
654
655        let ndivs = ((da.abs() / (PI * 0.5) + 0.5) as i32).clamp(1, 5);
656        let hda = (da / ndivs as f32) / 2.0;
657        let mut kappa = (4.0 / 3.0 * (1.0 - hda.cos()) / hda.sin()).abs();
658
659        if dir == Solidity::Solid {
660            kappa = -kappa;
661        }
662
663        let mut commands = Vec::with_capacity(ndivs as usize);
664        let mut pos_coords: Vec<[f32; 3]> = Vec::with_capacity(ndivs as usize * 3 + 1);
665        let (mut ppos, mut ptanpos) = ([0.0; 3], [0.0; 3]);
666
667        for i in 0..=ndivs {
668            let a = a0 + da * (i as f32 / ndivs as f32);
669            let dpos = add3(scale3(u, a.cos()), scale3(v, a.sin()));
670            let pos = add3(center, scale3(dpos, r));
671            let tanpos = scale3(cross3(normal, dpos), r * kappa);
672
673            if i == 0 {
674                let first_move = if self.verbs.is_empty() {
675                    PackedVerb::MoveTo
676                } else {
677                    PackedVerb::LineTo
678                };
679                commands.push(first_move);
680                pos_coords.push(pos);
681            } else {
682                commands.push(PackedVerb::BezierTo);
683                pos_coords.extend_from_slice(&[add3(ppos, ptanpos), sub3(pos, tanpos), pos]);
684            }
685
686            ppos = pos;
687            ptanpos = tanpos;
688        }
689
690        self.append(&commands, &pos_coords);
691    }
692
693    fn arc_to(&mut self, pos1: impl Into<[f32; 3]>, pos2: impl Into<[f32; 3]>, radius: f32) {
694        if self.verbs.is_empty() {
695            return;
696        }
697
698        let pos0 = self.last_pos;
699        let pos1 = pos1.into();
700        let pos2 = pos2.into();
701
702        let d0 = sub3(pos0, pos1);
703        let d1 = sub3(pos2, pos1);
704
705        if dot3(d0, d0) < self.dist_tol * self.dist_tol || dot3(d1, d1) < self.dist_tol * self.dist_tol {
706            self.line_to(pos1);
707            return;
708        }
709
710        let raw_normal = cross3(d0, d1);
711        if dot3(raw_normal, raw_normal) < 1e-12 {
712            self.line_to(pos1);
713            return;
714        }
715
716        let normal = normalize3(raw_normal);
717        let (u, v) = build_basis(&normal);
718
719        let to_2d = |p: [f32; 3]| -> Position {
720            let d = sub3(p, pos1);
721            Position {
722                x: dot3(d, u),
723                y: dot3(d, v),
724            }
725        };
726
727        let p0 = to_2d(pos0);
728        let p1 = to_2d(pos1);
729        let p2 = to_2d(pos2);
730
731        if Position::equals(p0, p1, self.dist_tol)
732            || Position::equals(p1, p2, self.dist_tol)
733            || Position::segment_distance(p1, p0, p2) < self.dist_tol * self.dist_tol
734            || radius < self.dist_tol
735        {
736            self.line_to(pos1);
737            return;
738        }
739
740        let mut dpos0 = p0 - p1;
741        let mut dpos1 = p2 - p1;
742
743        dpos0.normalize();
744        dpos1.normalize();
745
746        let a = dpos0.dot(dpos1).acos();
747        let d = radius / (a / 2.0).tan();
748
749        if d > 10000.0 {
750            self.line_to(pos1);
751            return;
752        }
753
754        let (cpos, a0, a1, dir);
755
756        if dpos0.cross(dpos1) > 0.0 {
757            cpos = p1 + dpos0 * d + dpos0.orthogonal() * radius;
758            a0 = dpos0.angle();
759            a1 = (-dpos1).angle();
760            dir = Solidity::Hole;
761        } else {
762            cpos = p1 + dpos0 * d - dpos0.orthogonal() * radius;
763            a0 = (-dpos0).angle();
764            a1 = dpos1.angle();
765            dir = Solidity::Solid;
766        }
767
768        let center3 = add3(pos1, add3(scale3(u, cpos.x), scale3(v, cpos.y)));
769
770        self.arc(center3, normal, radius, a0 + PI / 2.0, a1 + PI / 2.0, dir);
771    }
772}
773
774/// An iterator over the verbs and coordinates of a path.
775#[derive(Debug)]
776pub struct PathIter<'a, const N: usize = 2> {
777    verbs: slice::Iter<'a, PackedVerb>,
778    coords: &'a [[f32; N]],
779}
780
781impl<const N: usize> Iterator for PathIter<'_, N> {
782    type Item = Verb<N>;
783
784    fn next(&mut self) -> Option<Self::Item> {
785        if let Some(verb) = self.verbs.next() {
786            let verb = Verb::from_packed(verb, self.coords);
787            let num_coords = verb.num_coordinates();
788            self.coords = &self.coords[num_coords..];
789            Some(verb)
790        } else {
791            None
792        }
793    }
794}
795
796#[cfg(feature = "textlayout")]
797impl ttf_parser::OutlineBuilder for Path {
798    fn move_to(&mut self, x: f32, y: f32) {
799        self.move_to([x, y]);
800    }
801
802    fn line_to(&mut self, x: f32, y: f32) {
803        self.line_to([x, y]);
804    }
805
806    fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
807        self.quad_to([x1, y1], [x, y]);
808    }
809
810    fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
811        self.bezier_to([x1, y1], [x2, y2], [x, y]);
812    }
813
814    fn close(&mut self) {
815        self.close();
816    }
817}