Skip to main content

kui_core/
path.rs

1//! Paths: what a `path` node draws (`docs/adr/0040-a-path-is-a-mask-in-the-atlas.md`).
2//!
3//! A path is a list of [`PathOp`]s — SVG's `d` with every coordinate
4//! absolute and `H`, `V`, `S`, `T` and the relative forms expanded by
5//! [`Path::parse`], the one parser every binding goes through — filled by
6//! a rule and, optionally, stroked. The core flattens it once per frame
7//! for the hit outline, rasterizes it through zeno (swash's rasterizer,
8//! already in the tree for glyphs) into an alpha mask at the node's
9//! physical scale, keeps the mask in the glyph atlas keyed on a hash of
10//! the ops, and draws it as a `GlyphMask` quad tinted by the fill or the
11//! stroke colour. The ops live in a per-frame list beside the tree, as a
12//! line's points do, and a node refers to its run by [`PathId`].
13
14use crate::geom::{Rect, Vec2};
15use crate::retain::Kept;
16
17/// Index into the frame's path list.
18#[derive(Clone, Copy, Debug, PartialEq, Eq)]
19pub struct PathId(pub u32);
20
21/// One drawing command, every coordinate absolute, in the space the
22/// path was declared in.
23#[derive(Clone, Copy, Debug, PartialEq)]
24pub enum PathOp {
25    /// Begins a subpath at the point.
26    MoveTo(Vec2),
27    /// A straight line to the point.
28    LineTo(Vec2),
29    /// A quadratic curve through one control point to the end point.
30    QuadTo(Vec2, Vec2),
31    /// A cubic curve through two control points to the end point.
32    CubicTo(Vec2, Vec2, Vec2),
33    /// An elliptical arc as SVG spells it: the radii, the ellipse's
34    /// rotation in degrees, the large-arc and sweep flags, the end point.
35    ArcTo {
36        rx: f32,
37        ry: f32,
38        rotation: f32,
39        large: bool,
40        sweep: bool,
41        to: Vec2,
42    },
43    /// Closes the subpath back to its start.
44    Close,
45}
46
47/// The op codes of the flat wire form: a code, then its operands.
48pub const OP_MOVE: f32 = 0.0;
49pub const OP_LINE: f32 = 1.0;
50pub const OP_QUAD: f32 = 2.0;
51pub const OP_CUBIC: f32 = 3.0;
52pub const OP_ARC: f32 = 4.0;
53pub const OP_CLOSE: f32 = 5.0;
54
55impl PathOp {
56    /// How many floats follow the code of op `code` on the wire; `None`
57    /// for a code that is not one.
58    pub fn operands(code: f32) -> Option<usize> {
59        // A whole number or nothing: `as` would read 0.9, -0.5 and a NaN
60        // as 0, a move.
61        if code.fract() != 0.0 {
62            return None;
63        }
64        match code as i32 {
65            0 | 1 => Some(2),
66            2 => Some(4),
67            3 => Some(6),
68            4 => Some(7),
69            5 => Some(0),
70            _ => None,
71        }
72    }
73
74    /// Whether every number of the op is one: no NaN, no infinity.
75    pub fn is_finite(&self) -> bool {
76        let ok = |p: Vec2| p.x.is_finite() && p.y.is_finite();
77        match *self {
78            PathOp::MoveTo(p) | PathOp::LineTo(p) => ok(p),
79            PathOp::QuadTo(c, p) => ok(c) && ok(p),
80            PathOp::CubicTo(a, b, p) => ok(a) && ok(b) && ok(p),
81            PathOp::ArcTo {
82                rx,
83                ry,
84                rotation,
85                to,
86                ..
87            } => rx.is_finite() && ry.is_finite() && rotation.is_finite() && ok(to),
88            PathOp::Close => true,
89        }
90    }
91
92    /// Appends the op's wire form: the code and its operands.
93    pub fn write(&self, out: &mut Vec<f32>) {
94        match *self {
95            PathOp::MoveTo(p) => out.extend_from_slice(&[OP_MOVE, p.x, p.y]),
96            PathOp::LineTo(p) => out.extend_from_slice(&[OP_LINE, p.x, p.y]),
97            PathOp::QuadTo(c, p) => out.extend_from_slice(&[OP_QUAD, c.x, c.y, p.x, p.y]),
98            PathOp::CubicTo(a, b, p) => {
99                out.extend_from_slice(&[OP_CUBIC, a.x, a.y, b.x, b.y, p.x, p.y])
100            }
101            PathOp::ArcTo {
102                rx,
103                ry,
104                rotation,
105                large,
106                sweep,
107                to,
108            } => out.extend_from_slice(&[
109                OP_ARC,
110                rx,
111                ry,
112                rotation,
113                f32::from(large),
114                f32::from(sweep),
115                to.x,
116                to.y,
117            ]),
118            PathOp::Close => out.push(OP_CLOSE),
119        }
120    }
121
122    /// The op with every point moved by `d`.
123    fn shifted(&self, d: Vec2) -> PathOp {
124        let s = |p: Vec2| Vec2::new(p.x + d.x, p.y + d.y);
125        match *self {
126            PathOp::MoveTo(p) => PathOp::MoveTo(s(p)),
127            PathOp::LineTo(p) => PathOp::LineTo(s(p)),
128            PathOp::QuadTo(c, p) => PathOp::QuadTo(s(c), s(p)),
129            PathOp::CubicTo(a, b, p) => PathOp::CubicTo(s(a), s(b), s(p)),
130            PathOp::ArcTo {
131                rx,
132                ry,
133                rotation,
134                large,
135                sweep,
136                to,
137            } => PathOp::ArcTo {
138                rx,
139                ry,
140                rotation,
141                large,
142                sweep,
143                to: s(to),
144            },
145            PathOp::Close => PathOp::Close,
146        }
147    }
148}
149
150/// How a path's inside is decided: SVG's default, or the polygon's rule.
151#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
152pub enum FillRule {
153    /// A point is inside when the outline winds around it a net nonzero
154    /// number of times: a self-intersecting outline fills its overlaps.
155    #[default]
156    NonZero,
157    /// A point is inside when an odd number of edges cross a ray from it:
158    /// overlaps are unfilled, which is how a ring is a ring.
159    EvenOdd,
160}
161
162impl FillRule {
163    /// The names the bindings spell: `nonzero`, `evenodd`.
164    pub const ALL: &[&str] = &["nonzero", "evenodd"];
165
166    pub fn from_index(i: usize) -> Self {
167        match i {
168            1 => FillRule::EvenOdd,
169            _ => FillRule::NonZero,
170        }
171    }
172
173    pub fn index(self) -> usize {
174        match self {
175            FillRule::NonZero => 0,
176            FillRule::EvenOdd => 1,
177        }
178    }
179
180    pub fn parse(s: &str) -> Option<Self> {
181        match s {
182            "nonzero" => Some(FillRule::NonZero),
183            "evenodd" => Some(FillRule::EvenOdd),
184            _ => None,
185        }
186    }
187}
188
189/// A path's turn (`docs/adr/0041-a-mask-turns-about-its-centre.md`): how
190/// far it is turned, in turns — clockwise with y down, as
191/// [`Path::sector`] counts them — and the point it turns about, in the
192/// path's own coordinates; `None` is the centre of the outline's box. A
193/// path with a turn is boxed by the square the turn sweeps, so its mask
194/// is one mask at every angle and the quad that draws it carries the
195/// angle.
196#[derive(Clone, Copy, Debug, Default, PartialEq)]
197pub struct Turn {
198    pub turns: f32,
199    pub pivot: Option<Vec2>,
200}
201
202impl Turn {
203    /// The angle in radians, clockwise with y down.
204    pub fn radians(self) -> f32 {
205        self.turns * std::f32::consts::TAU
206    }
207}
208
209/// `p` turned by `angle` radians (clockwise, y down) about `c`.
210pub fn turned(p: Vec2, c: Vec2, angle: f32) -> Vec2 {
211    let (sin, cos) = angle.sin_cos();
212    let (x, y) = (p.x - c.x, p.y - c.y);
213    Vec2::new(c.x + x * cos - y * sin, c.y + x * sin + y * cos)
214}
215
216/// Where a `d` string went wrong: the byte offset and what was expected.
217#[derive(Clone, Debug, PartialEq, Eq)]
218pub struct PathError {
219    pub at: usize,
220    pub what: &'static str,
221}
222
223impl std::fmt::Display for PathError {
224    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
225        write!(f, "{} at byte {}", self.what, self.at)
226    }
227}
228
229impl std::error::Error for PathError {}
230
231/// A path as an app builds one: the ops, the fill rule, and an optional
232/// stroke. The fill colour is the node's `bg`; the stroke's width and
233/// colour are the [`crate::line::Stroke`]'s (`curve` is ignored).
234#[derive(Clone, Debug, PartialEq)]
235pub struct Path {
236    ops: Vec<PathOp>,
237    rule: FillRule,
238    stroke: Option<crate::line::Stroke>,
239    turn: Option<Turn>,
240}
241
242impl Default for Path {
243    fn default() -> Self {
244        Self::new()
245    }
246}
247
248impl Path {
249    pub fn new() -> Self {
250        Self {
251            ops: Vec::new(),
252            rule: FillRule::NonZero,
253            stroke: None,
254            turn: None,
255        }
256    }
257
258    /// A path over ops already built.
259    pub fn from_ops(ops: Vec<PathOp>) -> Self {
260        Self {
261            ops,
262            rule: FillRule::NonZero,
263            stroke: None,
264            turn: None,
265        }
266    }
267
268    /// A path from its flat wire form: a code, then its operands, per op.
269    pub fn from_floats(f: &[f32]) -> Result<Self, PathError> {
270        let mut ops = Vec::new();
271        let mut i = 0;
272        while i < f.len() {
273            let code = f[i];
274            let Some(n) = PathOp::operands(code) else {
275                return Err(PathError {
276                    at: i,
277                    what: "an op code 0..=5",
278                });
279            };
280            let Some(a) = f.get(i + 1..i + 1 + n) else {
281                return Err(PathError {
282                    at: i,
283                    what: "the op's operands",
284                });
285            };
286            ops.push(match code as i32 {
287                0 => PathOp::MoveTo(Vec2::new(a[0], a[1])),
288                1 => PathOp::LineTo(Vec2::new(a[0], a[1])),
289                2 => PathOp::QuadTo(Vec2::new(a[0], a[1]), Vec2::new(a[2], a[3])),
290                3 => PathOp::CubicTo(
291                    Vec2::new(a[0], a[1]),
292                    Vec2::new(a[2], a[3]),
293                    Vec2::new(a[4], a[5]),
294                ),
295                4 => PathOp::ArcTo {
296                    rx: a[0],
297                    ry: a[1],
298                    rotation: a[2],
299                    large: a[3] != 0.0,
300                    sweep: a[4] != 0.0,
301                    to: Vec2::new(a[5], a[6]),
302                },
303                _ => PathOp::Close,
304            });
305            i += 1 + n;
306        }
307        Ok(Self::from_ops(ops))
308    }
309
310    /// The flat wire form.
311    pub fn to_floats(&self) -> Vec<f32> {
312        let mut out = Vec::with_capacity(self.ops.len() * 3);
313        for op in &self.ops {
314            op.write(&mut out);
315        }
316        out
317    }
318
319    pub fn ops(&self) -> &[PathOp] {
320        &self.ops
321    }
322
323    pub fn into_ops(self) -> Vec<PathOp> {
324        self.ops
325    }
326
327    pub fn rule(&self) -> FillRule {
328        self.rule
329    }
330
331    pub fn stroke(&self) -> Option<crate::line::Stroke> {
332        self.stroke
333    }
334
335    pub fn turn(&self) -> Option<Turn> {
336        self.turn
337    }
338
339    /// Turns the path by `turns` (clockwise, y down) about its pivot —
340    /// the centre of its outline's box unless [`Self::pivot`] names one.
341    /// The turn is the quad's and not the mask's: one raster, whatever
342    /// the angle.
343    pub fn rotated(mut self, turns: f32) -> Self {
344        self.turn.get_or_insert_default().turns = turns;
345        self
346    }
347
348    /// The point the path turns about, in the path's own coordinates. A
349    /// spinner's arc names its circle's centre, which is not its own
350    /// box's.
351    pub fn pivot(mut self, x: f32, y: f32) -> Self {
352        self.turn.get_or_insert_default().pivot = Some(Vec2::new(x, y));
353        self
354    }
355
356    pub fn fill_rule(mut self, rule: FillRule) -> Self {
357        self.rule = rule;
358        self
359    }
360
361    /// Strokes the outline `stroke.width` wide in `stroke.color`, over
362    /// the fill. Round joins and caps.
363    pub fn stroked(mut self, stroke: crate::line::Stroke) -> Self {
364        self.stroke = Some(stroke);
365        self
366    }
367
368    pub fn move_to(mut self, x: f32, y: f32) -> Self {
369        self.ops.push(PathOp::MoveTo(Vec2::new(x, y)));
370        self
371    }
372
373    pub fn line_to(mut self, x: f32, y: f32) -> Self {
374        self.ops.push(PathOp::LineTo(Vec2::new(x, y)));
375        self
376    }
377
378    pub fn quad_to(mut self, cx: f32, cy: f32, x: f32, y: f32) -> Self {
379        self.ops
380            .push(PathOp::QuadTo(Vec2::new(cx, cy), Vec2::new(x, y)));
381        self
382    }
383
384    pub fn cubic_to(mut self, c1x: f32, c1y: f32, c2x: f32, c2y: f32, x: f32, y: f32) -> Self {
385        self.ops.push(PathOp::CubicTo(
386            Vec2::new(c1x, c1y),
387            Vec2::new(c2x, c2y),
388            Vec2::new(x, y),
389        ));
390        self
391    }
392
393    /// An elliptical arc to `(x, y)`, as SVG's `A`: radii, the ellipse's
394    /// rotation in degrees, whether the larger of the two arcs is taken,
395    /// whether it sweeps clockwise (y down).
396    #[allow(clippy::too_many_arguments)]
397    pub fn arc_to(
398        mut self,
399        rx: f32,
400        ry: f32,
401        rotation: f32,
402        large: bool,
403        sweep: bool,
404        x: f32,
405        y: f32,
406    ) -> Self {
407        self.ops.push(PathOp::ArcTo {
408            rx,
409            ry,
410            rotation,
411            large,
412            sweep,
413            to: Vec2::new(x, y),
414        });
415        self
416    }
417
418    pub fn close(mut self) -> Self {
419        self.ops.push(PathOp::Close);
420        self
421    }
422
423    /// A sector of an annulus centred on `(cx, cy)`: from `from` turns
424    /// (0 is east, turns run clockwise with y down) sweeping `sweep` turns,
425    /// between `inner` and `outer` radii. A pie wedge with `inner` 0, a
426    /// donut's segment otherwise, a ring with `sweep` 1.
427    pub fn sector(cx: f32, cy: f32, outer: f32, inner: f32, from: f32, sweep: f32) -> Self {
428        let tau = std::f32::consts::TAU;
429        let sweep = sweep.clamp(-1.0, 1.0);
430        let (a0, a1) = (from * tau, (from + sweep) * tau);
431        let at = |r: f32, a: f32| (cx + r * a.cos(), cy + r * a.sin());
432        let large = sweep.abs() > 0.5;
433        let cw = sweep >= 0.0;
434        if sweep.abs() >= 1.0 {
435            // A full turn is two half arcs, since one arc cannot return to
436            // its start.
437            let mid = a0 + tau * 0.5;
438            let (ox, oy) = at(outer, a0);
439            let (mx, my) = at(outer, mid);
440            let mut p = Path::new()
441                .move_to(ox, oy)
442                .arc_to(outer, outer, 0.0, false, cw, mx, my)
443                .arc_to(outer, outer, 0.0, false, cw, ox, oy)
444                .close();
445            if inner > 0.0 {
446                let (ix, iy) = at(inner, a0);
447                let (jx, jy) = at(inner, mid);
448                p = p
449                    .move_to(ix, iy)
450                    .arc_to(inner, inner, 0.0, false, !cw, jx, jy)
451                    .arc_to(inner, inner, 0.0, false, !cw, ix, iy)
452                    .close();
453            }
454            return p;
455        }
456        let (ox0, oy0) = at(outer, a0);
457        let (ox1, oy1) = at(outer, a1);
458        let mut p = Path::new()
459            .move_to(ox0, oy0)
460            .arc_to(outer, outer, 0.0, large, cw, ox1, oy1);
461        if inner > 0.0 {
462            let (ix1, iy1) = at(inner, a1);
463            let (ix0, iy0) = at(inner, a0);
464            p = p
465                .line_to(ix1, iy1)
466                .arc_to(inner, inner, 0.0, large, !cw, ix0, iy0);
467        } else {
468            p = p.line_to(cx, cy);
469        }
470        p.close()
471    }
472
473    /// Parses SVG path data: `M L H V C S Q T A Z` and their relative
474    /// forms, implicit repeats, numbers run together as SVG allows. Every
475    /// relative command, `H`, `V`, `S` and `T` is expanded into the six
476    /// ops, so what comes out is the same in every binding.
477    pub fn parse(d: &str) -> Result<Self, PathError> {
478        let b = d.as_bytes();
479        let mut i = 0;
480        let mut ops = Vec::new();
481        let mut cur = Vec2::ZERO;
482        let mut start = Vec2::ZERO;
483        // The last control point, for `S` / `T` reflection, and which
484        // kind of curve left it.
485        let mut last_cubic: Option<Vec2> = None;
486        let mut last_quad: Option<Vec2> = None;
487        let mut cmd: Option<u8> = None;
488
489        fn skip_ws(b: &[u8], i: &mut usize) {
490            while *i < b.len() && (b[*i].is_ascii_whitespace() || b[*i] == b',') {
491                *i += 1;
492            }
493        }
494        fn number(b: &[u8], i: &mut usize) -> Result<f32, PathError> {
495            skip_ws(b, i);
496            let at = *i;
497            let mut j = at;
498            if j < b.len() && (b[j] == b'-' || b[j] == b'+') {
499                j += 1;
500            }
501            let digits = j;
502            while j < b.len() && b[j].is_ascii_digit() {
503                j += 1;
504            }
505            if j < b.len() && b[j] == b'.' {
506                j += 1;
507                while j < b.len() && b[j].is_ascii_digit() {
508                    j += 1;
509                }
510            }
511            if j == digits || (j == digits + 1 && b[digits] == b'.') {
512                return Err(PathError {
513                    at,
514                    what: "a number",
515                });
516            }
517            if j < b.len() && (b[j] == b'e' || b[j] == b'E') {
518                let mut k = j + 1;
519                if k < b.len() && (b[k] == b'-' || b[k] == b'+') {
520                    k += 1;
521                }
522                let e = k;
523                while k < b.len() && b[k].is_ascii_digit() {
524                    k += 1;
525                }
526                if k > e {
527                    j = k;
528                }
529            }
530            let s = std::str::from_utf8(&b[at..j]).map_err(|_| PathError {
531                at,
532                what: "a number",
533            })?;
534            *i = j;
535            s.parse::<f32>().map_err(|_| PathError {
536                at,
537                what: "a number",
538            })
539        }
540        fn flag(b: &[u8], i: &mut usize) -> Result<bool, PathError> {
541            skip_ws(b, i);
542            match b.get(*i) {
543                Some(b'0') => {
544                    *i += 1;
545                    Ok(false)
546                }
547                Some(b'1') => {
548                    *i += 1;
549                    Ok(true)
550                }
551                _ => Err(PathError {
552                    at: *i,
553                    what: "an arc flag (0 or 1)",
554                }),
555            }
556        }
557
558        loop {
559            skip_ws(b, &mut i);
560            if i >= b.len() {
561                break;
562            }
563            let c = b[i];
564            if c.is_ascii_alphabetic() {
565                cmd = Some(c);
566                i += 1;
567                if c == b'Z' || c == b'z' {
568                    ops.push(PathOp::Close);
569                    cur = start;
570                    last_cubic = None;
571                    last_quad = None;
572                    continue;
573                }
574            } else if cmd.is_none() {
575                return Err(PathError {
576                    at: i,
577                    what: "a command letter",
578                });
579            }
580            let Some(c) = cmd else { break };
581            let rel = c.is_ascii_lowercase();
582            let base = if rel { cur } else { Vec2::ZERO };
583            let point = |b: &[u8], i: &mut usize| -> Result<Vec2, PathError> {
584                let x = number(b, i)?;
585                let y = number(b, i)?;
586                Ok(Vec2::new(base.x + x, base.y + y))
587            };
588            match c.to_ascii_uppercase() {
589                b'M' => {
590                    let p = point(b, &mut i)?;
591                    ops.push(PathOp::MoveTo(p));
592                    cur = p;
593                    start = p;
594                    // Further pairs are implicit line-tos.
595                    cmd = Some(if rel { b'l' } else { b'L' });
596                    last_cubic = None;
597                    last_quad = None;
598                }
599                b'L' => {
600                    let p = point(b, &mut i)?;
601                    ops.push(PathOp::LineTo(p));
602                    cur = p;
603                    last_cubic = None;
604                    last_quad = None;
605                }
606                b'H' => {
607                    let x = number(b, &mut i)?;
608                    let p = Vec2::new(base.x + x, cur.y);
609                    ops.push(PathOp::LineTo(p));
610                    cur = p;
611                    last_cubic = None;
612                    last_quad = None;
613                }
614                b'V' => {
615                    let y = number(b, &mut i)?;
616                    let p = Vec2::new(cur.x, base.y + y);
617                    ops.push(PathOp::LineTo(p));
618                    cur = p;
619                    last_cubic = None;
620                    last_quad = None;
621                }
622                b'C' => {
623                    let c1 = point(b, &mut i)?;
624                    let c2 = point(b, &mut i)?;
625                    let p = point(b, &mut i)?;
626                    ops.push(PathOp::CubicTo(c1, c2, p));
627                    last_cubic = Some(c2);
628                    last_quad = None;
629                    cur = p;
630                }
631                b'S' => {
632                    let c2 = point(b, &mut i)?;
633                    let p = point(b, &mut i)?;
634                    let c1 = match last_cubic {
635                        Some(l) => Vec2::new(2.0 * cur.x - l.x, 2.0 * cur.y - l.y),
636                        None => cur,
637                    };
638                    ops.push(PathOp::CubicTo(c1, c2, p));
639                    last_cubic = Some(c2);
640                    last_quad = None;
641                    cur = p;
642                }
643                b'Q' => {
644                    let c1 = point(b, &mut i)?;
645                    let p = point(b, &mut i)?;
646                    ops.push(PathOp::QuadTo(c1, p));
647                    last_quad = Some(c1);
648                    last_cubic = None;
649                    cur = p;
650                }
651                b'T' => {
652                    let p = point(b, &mut i)?;
653                    let c1 = match last_quad {
654                        Some(l) => Vec2::new(2.0 * cur.x - l.x, 2.0 * cur.y - l.y),
655                        None => cur,
656                    };
657                    ops.push(PathOp::QuadTo(c1, p));
658                    last_quad = Some(c1);
659                    last_cubic = None;
660                    cur = p;
661                }
662                b'A' => {
663                    let rx = number(b, &mut i)?;
664                    let ry = number(b, &mut i)?;
665                    let rotation = number(b, &mut i)?;
666                    let large = flag(b, &mut i)?;
667                    let sweep = flag(b, &mut i)?;
668                    let p = point(b, &mut i)?;
669                    ops.push(PathOp::ArcTo {
670                        rx: rx.abs(),
671                        ry: ry.abs(),
672                        rotation,
673                        large,
674                        sweep,
675                        to: p,
676                    });
677                    last_cubic = None;
678                    last_quad = None;
679                    cur = p;
680                }
681                _ => {
682                    return Err(PathError {
683                        at: i - 1,
684                        what: "one of M L H V C S Q T A Z",
685                    });
686                }
687            }
688        }
689        Ok(Self::from_ops(ops))
690    }
691}
692
693/// Separates two contours in a flattened outline: a point that is not a
694/// point. `in_path` reads it as the end of one closed contour and the
695/// start of the next.
696pub const CONTOUR_BREAK: Vec2 = Vec2 {
697    x: f32::NAN,
698    y: f32::NAN,
699};
700
701/// How far a flattened curve may stray from the true one, logical px.
702/// The hit outline's tolerance; the rasterizer flattens on its own.
703pub const FLATTEN_TOLERANCE: f32 = 0.2;
704
705/// The most pieces one curve or arc is cut into.
706const MAX_PIECES: usize = 128;
707
708/// Flattens `ops` into closed contours in `out`, each contour's points
709/// followed by [`CONTOUR_BREAK`]. Every subpath is closed for the
710/// purpose of a fill, as SVG closes it. A move with nothing after it
711/// adds nothing.
712pub fn flatten(ops: &[PathOp], out: &mut Vec<Vec2>) {
713    flatten_as(ops, out, false);
714}
715
716/// Flattens `ops` into the polylines a stroke draws, each followed by
717/// [`CONTOUR_BREAK`]: a subpath its `Z` closed ends back on its start, an
718/// open one ends where it was left, so a stroke's hit pieces are the
719/// pieces it paints and no chord across an open curve is among them.
720pub fn flatten_stroke(ops: &[PathOp], out: &mut Vec<Vec2>) {
721    flatten_as(ops, out, true);
722}
723
724fn flatten_as(ops: &[PathOp], out: &mut Vec<Vec2>, stroke: bool) {
725    let mut cur = Vec2::ZERO;
726    let mut open = false;
727    let mut contour_start = out.len();
728    let close = |out: &mut Vec<Vec2>, open: &mut bool, contour_start: usize, closed: bool| {
729        if *open {
730            // A contour of one point is nothing.
731            if out.len() - contour_start >= 2 {
732                if stroke && closed {
733                    out.push(out[contour_start]);
734                }
735                out.push(CONTOUR_BREAK);
736            } else {
737                out.truncate(contour_start);
738            }
739            *open = false;
740        }
741    };
742    for op in ops {
743        match *op {
744            PathOp::MoveTo(p) => {
745                close(out, &mut open, contour_start, false);
746                contour_start = out.len();
747                out.push(p);
748                cur = p;
749                open = true;
750            }
751            PathOp::Close => {
752                close(out, &mut open, contour_start, true);
753                // A draw after a close starts where the subpath began.
754                if let Some(&s) = out.get(contour_start) {
755                    cur = s;
756                }
757                contour_start = out.len();
758            }
759            _ => {
760                if !open {
761                    contour_start = out.len();
762                    out.push(cur);
763                    open = true;
764                }
765                match *op {
766                    PathOp::LineTo(p) => {
767                        out.push(p);
768                        cur = p;
769                    }
770                    PathOp::QuadTo(c, p) => {
771                        flatten_quad(cur, c, p, out);
772                        cur = p;
773                    }
774                    PathOp::CubicTo(a, b, p) => {
775                        flatten_cubic(cur, a, b, p, out);
776                        cur = p;
777                    }
778                    PathOp::ArcTo {
779                        rx,
780                        ry,
781                        rotation,
782                        large,
783                        sweep,
784                        to,
785                    } => {
786                        flatten_arc(cur, rx, ry, rotation, large, sweep, to, out);
787                        cur = to;
788                    }
789                    PathOp::MoveTo(_) | PathOp::Close => unreachable!(),
790                }
791            }
792        }
793    }
794    close(out, &mut open, contour_start, false);
795}
796
797fn pieces(dd: f32, k: f32) -> usize {
798    ((dd * k / FLATTEN_TOLERANCE).sqrt().ceil() as usize).clamp(1, MAX_PIECES)
799}
800
801fn flatten_quad(p0: Vec2, c: Vec2, p1: Vec2, out: &mut Vec<Vec2>) {
802    let dd = Vec2::new(p0.x - 2.0 * c.x + p1.x, p0.y - 2.0 * c.y + p1.y);
803    let n = pieces((dd.x * dd.x + dd.y * dd.y).sqrt(), 0.25);
804    for i in 1..=n {
805        let t = i as f32 / n as f32;
806        let u = 1.0 - t;
807        out.push(Vec2::new(
808            u * u * p0.x + 2.0 * u * t * c.x + t * t * p1.x,
809            u * u * p0.y + 2.0 * u * t * c.y + t * t * p1.y,
810        ));
811    }
812}
813
814fn flatten_cubic(p0: Vec2, a: Vec2, b: Vec2, p1: Vec2, out: &mut Vec<Vec2>) {
815    let d1 = Vec2::new(p0.x - 2.0 * a.x + b.x, p0.y - 2.0 * a.y + b.y);
816    let d2 = Vec2::new(a.x - 2.0 * b.x + p1.x, a.y - 2.0 * b.y + p1.y);
817    let dd = (d1.x * d1.x + d1.y * d1.y)
818        .max(d2.x * d2.x + d2.y * d2.y)
819        .sqrt();
820    let n = pieces(dd, 0.75);
821    for i in 1..=n {
822        let t = i as f32 / n as f32;
823        let u = 1.0 - t;
824        let (uu, tt) = (u * u, t * t);
825        out.push(Vec2::new(
826            uu * u * p0.x + 3.0 * uu * t * a.x + 3.0 * u * tt * b.x + tt * t * p1.x,
827            uu * u * p0.y + 3.0 * uu * t * a.y + 3.0 * u * tt * b.y + tt * t * p1.y,
828        ));
829    }
830}
831
832/// An SVG arc in centre form: the centre, the radii as scaled up when the
833/// endpoints are too far apart, the start angle and the sweep in radians.
834/// `None` for an arc whose endpoints coincide or whose radii are zero,
835/// which SVG draws as a line (or nothing).
836#[allow(clippy::too_many_arguments)]
837fn arc_center(
838    from: Vec2,
839    rx: f32,
840    ry: f32,
841    rotation: f32,
842    large: bool,
843    sweep: bool,
844    to: Vec2,
845) -> Option<(Vec2, f32, f32, f32, f32, f32)> {
846    if (from.x - to.x).abs() < 1e-6 && (from.y - to.y).abs() < 1e-6 {
847        return None;
848    }
849    let (mut rx, mut ry) = (rx.abs(), ry.abs());
850    if rx < 1e-6 || ry < 1e-6 {
851        return None;
852    }
853    let phi = rotation.to_radians();
854    let (sin_phi, cos_phi) = phi.sin_cos();
855    let dx = (from.x - to.x) * 0.5;
856    let dy = (from.y - to.y) * 0.5;
857    let x1 = cos_phi * dx + sin_phi * dy;
858    let y1 = -sin_phi * dx + cos_phi * dy;
859    let lambda = (x1 * x1) / (rx * rx) + (y1 * y1) / (ry * ry);
860    if lambda > 1.0 {
861        let s = lambda.sqrt();
862        rx *= s;
863        ry *= s;
864    }
865    let num = (rx * rx * ry * ry - rx * rx * y1 * y1 - ry * ry * x1 * x1).max(0.0);
866    let den = rx * rx * y1 * y1 + ry * ry * x1 * x1;
867    let mut coef = if den > 0.0 { (num / den).sqrt() } else { 0.0 };
868    if large == sweep {
869        coef = -coef;
870    }
871    let cx1 = coef * rx * y1 / ry;
872    let cy1 = -coef * ry * x1 / rx;
873    let cx = cos_phi * cx1 - sin_phi * cy1 + (from.x + to.x) * 0.5;
874    let cy = sin_phi * cx1 + cos_phi * cy1 + (from.y + to.y) * 0.5;
875    let ux = (x1 - cx1) / rx;
876    let uy = (y1 - cy1) / ry;
877    let vx = (-x1 - cx1) / rx;
878    let vy = (-y1 - cy1) / ry;
879    let angle = |ax: f32, ay: f32, bx: f32, by: f32| -> f32 {
880        let dot = ax * bx + ay * by;
881        let len = ((ax * ax + ay * ay) * (bx * bx + by * by)).sqrt();
882        let mut a = (dot / len).clamp(-1.0, 1.0).acos();
883        if ax * by - ay * bx < 0.0 {
884            a = -a;
885        }
886        a
887    };
888    let theta = angle(1.0, 0.0, ux, uy);
889    let mut delta = angle(ux, uy, vx, vy);
890    let tau = std::f32::consts::TAU;
891    if !sweep && delta > 0.0 {
892        delta -= tau;
893    } else if sweep && delta < 0.0 {
894        delta += tau;
895    }
896    Some((Vec2::new(cx, cy), rx, ry, phi, theta, delta))
897}
898
899#[allow(clippy::too_many_arguments)]
900fn flatten_arc(
901    from: Vec2,
902    rx: f32,
903    ry: f32,
904    rotation: f32,
905    large: bool,
906    sweep: bool,
907    to: Vec2,
908    out: &mut Vec<Vec2>,
909) {
910    let Some((c, rx, ry, phi, theta, delta)) = arc_center(from, rx, ry, rotation, large, sweep, to)
911    else {
912        out.push(to);
913        return;
914    };
915    let r = rx.max(ry);
916    // The chord of one piece strays `r (1 - cos(step / 2))` from the arc.
917    let step = 2.0 * (1.0 - FLATTEN_TOLERANCE / r).clamp(-1.0, 1.0).acos();
918    let n = if step > 0.0 {
919        ((delta.abs() / step).ceil() as usize).clamp(1, MAX_PIECES)
920    } else {
921        1
922    };
923    let (sin_phi, cos_phi) = phi.sin_cos();
924    for i in 1..=n {
925        let a = theta + delta * i as f32 / n as f32;
926        let (sa, ca) = a.sin_cos();
927        let x = rx * ca;
928        let y = ry * sa;
929        out.push(Vec2::new(
930            c.x + cos_phi * x - sin_phi * y,
931            c.y + sin_phi * x + cos_phi * y,
932        ));
933    }
934    // The last point is the declared end, to the bit.
935    if let Some(last) = out.last_mut() {
936        *last = to;
937    }
938}
939
940/// Whether `p` is inside the flattened contours `pts` (as [`flatten`]
941/// lays them out) by `rule`. A point on an edge counts as inside on one
942/// side and outside on the other, so of two wedges sharing an edge
943/// exactly one takes it.
944pub fn in_path(p: Vec2, pts: &[Vec2], rule: FillRule) -> bool {
945    let mut winding = 0i32;
946    let mut crossings = 0u32;
947    for contour in pts.split(|v| v.x.is_nan()) {
948        let n = contour.len();
949        if n < 3 {
950            continue;
951        }
952        let mut j = n - 1;
953        for i in 0..n {
954            let (a, b) = (contour[i], contour[j]);
955            if (a.y > p.y) != (b.y > p.y) {
956                let x = a.x + (p.y - a.y) / (b.y - a.y) * (b.x - a.x);
957                if p.x < x {
958                    crossings += 1;
959                    winding += if b.y > a.y { 1 } else { -1 };
960                }
961            }
962            j = i;
963        }
964    }
965    match rule {
966        FillRule::NonZero => winding != 0,
967        FillRule::EvenOdd => crossings % 2 == 1,
968    }
969}
970
971/// The bounding box of a flattened outline, ignoring contour breaks;
972/// `None` for no points.
973pub fn bounds(pts: &[Vec2]) -> Option<Rect> {
974    let mut it = pts.iter().filter(|p| !p.x.is_nan());
975    let first = *it.next()?;
976    let (mut min, mut max) = (first, first);
977    for p in it {
978        min.x = min.x.min(p.x);
979        min.y = min.y.min(p.y);
980        max.x = max.x.max(p.x);
981        max.y = max.y.max(p.y);
982    }
983    Some(Rect::new(min.x, min.y, max.x - min.x, max.y - min.y))
984}
985
986/// FNV-1a over the ops' bits: what a mask is keyed on. The same ops are
987/// the same hash in every binding, since the floats crossed as floats.
988pub fn hash_ops(ops: &[PathOp]) -> u64 {
989    const OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
990    const PRIME: u64 = 0x0000_0100_0000_01b3;
991    let mut h = OFFSET;
992    let mut mix = |v: u32| {
993        for byte in v.to_le_bytes() {
994            h ^= u64::from(byte);
995            h = h.wrapping_mul(PRIME);
996        }
997    };
998    let mut scratch = Vec::with_capacity(8);
999    for op in ops {
1000        scratch.clear();
1001        op.write(&mut scratch);
1002        for v in &scratch {
1003            mix(v.to_bits());
1004        }
1005    }
1006    h
1007}
1008
1009/// What one mask paints: the fill by its rule, bled half a pixel so two
1010/// fills sharing an edge meet without the background showing, or the
1011/// outline stroked `width` physical px wide — whole, or cut into the
1012/// marks of a dash pattern (backlog V2), its lengths physical too.
1013#[derive(Clone, Copy, Debug, PartialEq)]
1014pub enum MaskPaint {
1015    Fill(FillRule),
1016    Stroke(f32),
1017    Dashed(f32, DashCut),
1018}
1019
1020/// A dash pattern as the rasterizer takes it: the centre-line lengths of
1021/// a mark, a gap, a mark and a gap, and how far into them the stroke
1022/// starts. [`crate::line::Dash`] is what an app declares.
1023pub type DashCut = crate::line::Cut;
1024
1025/// Rasterizes `ops` (in logical px, relative to the node's box) into a
1026/// `w × h` alpha mask at `scale`, the node's box shifted by `bin`
1027/// quarter-pixels so a node at a fractional position lands on the pixel
1028/// grid as it would be drawn. `w * h` bytes, row after row.
1029pub fn rasterize(
1030    ops: &[PathOp],
1031    scale: f32,
1032    bin: (u8, u8),
1033    w: u32,
1034    h: u32,
1035    paint: MaskPaint,
1036) -> Vec<u8> {
1037    let off = Vec2::new(f32::from(bin.0) * 0.25, f32::from(bin.1) * 0.25);
1038    rasterize_at(ops, scale, off, w, h, paint)
1039}
1040
1041/// [`rasterize`] with the box's origin `off` physical px into the mask:
1042/// what a turning path's mask is drawn with, its pivot at the mask's
1043/// centre.
1044pub fn rasterize_at(
1045    ops: &[PathOp],
1046    scale: f32,
1047    off: Vec2,
1048    w: u32,
1049    h: u32,
1050    paint: MaskPaint,
1051) -> Vec<u8> {
1052    use swash::zeno::{Cap, Command, Fill, Join, Mask, PathBuilder, Point, Stroke};
1053    let len = (w as usize) * (h as usize);
1054    let mut buf = vec![0u8; len];
1055    if len == 0 || ops.is_empty() {
1056        return buf;
1057    }
1058    let at = |p: Vec2| Point::new(p.x * scale + off.x, p.y * scale + off.y);
1059    let mut cmds: Vec<Command> = Vec::with_capacity(ops.len() + 1);
1060    let mut cur = Vec2::ZERO;
1061    let mut start = Vec2::ZERO;
1062    let mut open = false;
1063    for op in ops {
1064        match *op {
1065            PathOp::MoveTo(p) => {
1066                cmds.move_to(at(p));
1067                cur = p;
1068                start = p;
1069                open = true;
1070            }
1071            PathOp::Close => {
1072                if open {
1073                    cmds.close();
1074                }
1075                // A draw after a close starts where the subpath began,
1076                // as `flatten` has it.
1077                cur = start;
1078                open = false;
1079            }
1080            _ => {
1081                if !open {
1082                    cmds.move_to(at(cur));
1083                    start = cur;
1084                    open = true;
1085                }
1086                match *op {
1087                    PathOp::LineTo(p) => {
1088                        cmds.line_to(at(p));
1089                        cur = p;
1090                    }
1091                    PathOp::QuadTo(c, p) => {
1092                        cmds.quad_to(at(c), at(p));
1093                        cur = p;
1094                    }
1095                    PathOp::CubicTo(a, b, p) => {
1096                        cmds.curve_to(at(a), at(b), at(p));
1097                        cur = p;
1098                    }
1099                    PathOp::ArcTo {
1100                        rx,
1101                        ry,
1102                        rotation,
1103                        large,
1104                        sweep,
1105                        to,
1106                    } => {
1107                        use swash::zeno::{Angle, ArcSize, ArcSweep};
1108                        cmds.arc_to(
1109                            rx * scale,
1110                            ry * scale,
1111                            Angle::from_degrees(rotation),
1112                            if large {
1113                                ArcSize::Large
1114                            } else {
1115                                ArcSize::Small
1116                            },
1117                            if sweep {
1118                                ArcSweep::Positive
1119                            } else {
1120                                ArcSweep::Negative
1121                            },
1122                            at(to),
1123                        );
1124                        cur = to;
1125                    }
1126                    PathOp::MoveTo(_) | PathOp::Close => unreachable!(),
1127                }
1128            }
1129        }
1130    }
1131    match paint {
1132        MaskPaint::Fill(rule) => {
1133            let fill = match rule {
1134                FillRule::NonZero => Fill::NonZero,
1135                FillRule::EvenOdd => Fill::EvenOdd,
1136            };
1137            Mask::new(&cmds[..])
1138                .style(fill)
1139                .size(w, h)
1140                .render_into(&mut buf, None);
1141            // A fill that covers nothing - a ring's sector at a sweep of
1142            // 0, out along a radius and back - has no edge to bleed: the
1143            // outline alone would paint it as a hairline.
1144            if buf.iter().all(|&a| a == 0) {
1145                return buf;
1146            }
1147            // The bleed: the outline a pixel wide, in the same mask by
1148            // max, so two fills sharing an edge overlap by the ramp.
1149            let mut edge = vec![0u8; len];
1150            let mut stroke = Stroke::new(1.0);
1151            stroke.join(Join::Round).cap(Cap::Round);
1152            Mask::new(&cmds[..])
1153                .style(stroke)
1154                .size(w, h)
1155                .render_into(&mut edge, None);
1156            for (a, e) in buf.iter_mut().zip(edge) {
1157                *a = (*a).max(e);
1158            }
1159        }
1160        MaskPaint::Stroke(width) => {
1161            let mut stroke = Stroke::new(width.max(0.0));
1162            stroke.join(Join::Round).cap(Cap::Round);
1163            Mask::new(&cmds[..])
1164                .style(stroke)
1165                .size(w, h)
1166                .render_into(&mut buf, None);
1167        }
1168        MaskPaint::Dashed(width, cut) => {
1169            let mut stroke = Stroke::new(width.max(0.0));
1170            stroke.join(Join::Round).cap(Cap::Round);
1171            // A mark and its gap under a pixel are not a pattern any
1172            // more, as on a line: the stroke whole.
1173            if !cut.finer_than(1.0) {
1174                stroke.dash(&cut.lens, cut.offset);
1175            }
1176            Mask::new(&cmds[..])
1177                .style(stroke)
1178                .size(w, h)
1179                .render_into(&mut buf, None);
1180        }
1181    }
1182    buf
1183}
1184
1185/// The texels a mask may take of the atlas before it goes to a texture
1186/// of its own: a quarter of the biggest page, since four of them would
1187/// empty it every frame.
1188pub const MAX_ATLAS_MASK_TEXELS: u64 = 2048 * 2048;
1189
1190/// The widest or tallest a mask may be and still be drawn from a texture
1191/// of its own: a texture every device kui runs on can hold — `kui-wgpu`
1192/// opens its device with wgpu's default limits, whose
1193/// `max_texture_dimension_2d` is this number whatever the adapter could
1194/// do, so the core's limit and the renderer's are one. Past it the node
1195/// draws nothing, with `path-too-large`. A host that draws the list
1196/// itself on a device that holds less refuses the texture on its side.
1197pub const MAX_MASK_SIDE: u32 = 8192;
1198
1199/// A mask drawn from a texture of its own rather than the atlas (ADR
1200/// 0040, decisions 7 and 8): the handle the backend caches it under, and
1201/// the pixels it uploads from.
1202pub struct PathTexture {
1203    pub id: crate::resources::ImageId,
1204    pub w: u32,
1205    pub h: u32,
1206    pub rgba: std::sync::Arc<Vec<u8>>,
1207    last_used: u64,
1208}
1209
1210/// The texture-backed masks of one core, by the mask key the atlas would
1211/// have held them under. One a frame does not draw is dropped at the
1212/// next frame's start, its handle handed to the display list for the
1213/// backend to free; an animating path, whose key is new each frame, thus
1214/// uploads one texture a frame and frees one. The ones a core still
1215/// holds when it goes go the way a removed image does: to the session,
1216/// for the next display list any of its windows builds (RG112).
1217pub struct PathTextures {
1218    by_key: rustc_hash::FxHashMap<u64, PathTexture>,
1219    session: crate::session::Session,
1220}
1221
1222impl Drop for PathTextures {
1223    fn drop(&mut self) {
1224        for t in self.by_key.values() {
1225            crate::resources::unmint_image(t.id);
1226        }
1227        // Held only if the core is dropped from inside a borrow of its
1228        // session, which nothing does; then the handles are forgotten
1229        // and the backend keeps the textures, as before.
1230        if let Some(mut sess) = self.session.try_state() {
1231            sess.dropped
1232                .images
1233                .extend(self.by_key.values().map(|t| t.id));
1234        }
1235    }
1236}
1237
1238impl PathTextures {
1239    pub(crate) fn new(session: crate::session::Session) -> Self {
1240        Self {
1241            by_key: Default::default(),
1242            session,
1243        }
1244    }
1245
1246    /// The pixels of the texture minted as `id`, for a host that draws
1247    /// the list itself and asks by handle.
1248    pub(crate) fn pixels(
1249        &self,
1250        id: crate::resources::ImageId,
1251    ) -> Option<(u32, u32, std::sync::Arc<Vec<u8>>)> {
1252        self.by_key
1253            .values()
1254            .find(|t| t.id == id)
1255            .map(|t| (t.w, t.h, t.rgba.clone()))
1256    }
1257
1258    /// The texture for `key`, made from `coverage` (`w * h` alpha bytes)
1259    /// on a miss.
1260    pub(crate) fn get_or_make(
1261        &mut self,
1262        key: u64,
1263        w: u32,
1264        h: u32,
1265        frame: u64,
1266        session: crate::resources::SessionId,
1267        coverage: impl FnOnce() -> Vec<u8>,
1268    ) -> &PathTexture {
1269        if let Some(tex) = self.by_key.get(&key) {
1270            debug_assert!(tex.w == w && tex.h == h, "a mask key names one size");
1271        }
1272        let tex = self.by_key.entry(key).or_insert_with(|| {
1273            let mask = coverage();
1274            let mut rgba = Vec::with_capacity(mask.len() * 4);
1275            for &a in &mask {
1276                rgba.extend_from_slice(&[255, 255, 255, a]);
1277            }
1278            PathTexture {
1279                id: crate::resources::mint_image(session),
1280                w,
1281                h,
1282                rgba: std::sync::Arc::new(rgba),
1283                last_used: frame,
1284            }
1285        });
1286        tex.last_used = frame;
1287        tex
1288    }
1289
1290    /// Drops every texture no frame since `frame - 1` drew, and returns
1291    /// their handles for the display list's `dropped_textures`.
1292    pub(crate) fn sweep(&mut self, frame: u64) -> Vec<crate::resources::ImageId> {
1293        let mut gone = Vec::new();
1294        self.by_key.retain(|_, t| {
1295            if t.last_used + 1 < frame {
1296                crate::resources::unmint_image(t.id);
1297                gone.push(t.id);
1298                false
1299            } else {
1300                true
1301            }
1302        });
1303        gone
1304    }
1305
1306    /// How many masks are texture-backed right now.
1307    pub fn len(&self) -> usize {
1308        self.by_key.len()
1309    }
1310
1311    pub fn is_empty(&self) -> bool {
1312        self.by_key.is_empty()
1313    }
1314}
1315
1316/// The mask key the atlas and the textures hold a path's mask under: its
1317/// ops' hash with the scale, the quarter-pixel bin and the paint mixed in.
1318/// The bin a turning path's masks are keyed under: none of the sixteen,
1319/// since its mask is centred on its pivot and not binned.
1320pub(crate) const TURNED_BIN: (u8, u8) = (0xff, 0xff);
1321
1322pub(crate) fn mask_key(hash: u64, scale: f32, bin: (u8, u8), paint: MaskPaint) -> u64 {
1323    const PRIME: u64 = 0x0000_0100_0000_01b3;
1324    let mut h = hash;
1325    let mut mix = |v: u64| {
1326        h ^= v;
1327        h = h.wrapping_mul(PRIME);
1328    };
1329    mix(u64::from(scale.to_bits()));
1330    mix(u64::from(bin.0) | (u64::from(bin.1) << 8));
1331    match paint {
1332        MaskPaint::Fill(rule) => mix(0x1000 | rule.index() as u64),
1333        MaskPaint::Stroke(w) => mix(0x2000_0000_0000 | u64::from(w.to_bits())),
1334        MaskPaint::Dashed(w, cut) => {
1335            mix(0x3000_0000_0000 | u64::from(w.to_bits()));
1336            mix(cut.hash());
1337        }
1338    }
1339    h
1340}
1341
1342/// How many frames apart two changes of one key's ops may be and still
1343/// be an animation: a shape driven at a quarter of
1344/// the frame rate, or one whose changes have a frame between them that
1345/// something else asked for, moves as surely as one that changes every
1346/// frame, and each of its shapes would be a slot the atlas never reuses.
1347pub const ANIMATING_WINDOW: u64 = 8;
1348
1349/// How many frames an animating key's shape stays the same before it is
1350/// a still shape again, and its mask the atlas's: a second at 120 Hz. A
1351/// spinner that pauses for a frame or ten stays out; one that stopped
1352/// stops costing a texture and a draw of its own.
1353pub const SETTLED_AFTER: u64 = 120;
1354
1355/// What the core remembers of one `path` key between frames, to tell a
1356/// new shape from a moving one.
1357#[derive(Clone, Copy, Debug)]
1358pub(crate) struct Motion {
1359    /// The ops' hash the key last declared, and the frame it did.
1360    pub hash: u64,
1361    pub seen: u64,
1362    /// The frame the hash last differed from the one before; 0 for never.
1363    pub changed: u64,
1364    /// Two changes within [`ANIMATING_WINDOW`], until the shape has held
1365    /// for [`SETTLED_AFTER`].
1366    pub animating: bool,
1367}
1368
1369/// A `d` string's ops as the core last parsed them under one key, so a
1370/// binding that hands the string over every frame pays the parse once
1371/// per string rather than once per frame.
1372pub(crate) struct Parsed {
1373    /// `key::hash_bulk` of the string, and its length.
1374    pub hash: u64,
1375    pub len: usize,
1376    pub seen: u64,
1377    pub ops: Vec<PathOp>,
1378}
1379
1380/// One path's run in the frame's op list. The ops are stored relative to
1381/// the node's box, so a node that eases or slides carries them along.
1382#[derive(Clone, Copy, Debug, PartialEq)]
1383pub(crate) struct Run {
1384    pub first: u32,
1385    pub len: u32,
1386    pub rule: FillRule,
1387    /// The stroke width in logical px; 0 for no stroke.
1388    pub stroke_w: f32,
1389    /// What cuts the stroke into marks; None for a solid one.
1390    pub dash: Option<crate::line::Cut>,
1391    /// [`hash_ops`] of the ops as stored.
1392    pub hash: u64,
1393    /// The turn in radians when the path declared one: its box is then
1394    /// the square about its pivot, and the angle is the quad's.
1395    pub angle: Option<f32>,
1396    /// The key's ops changed twice within [`ANIMATING_WINDOW`] frames:
1397    /// the mask goes to a texture of its own until the shape has held
1398    /// for [`SETTLED_AFTER`].
1399    pub animating: bool,
1400}
1401
1402/// The frame's paths, and the previous frame's while an `exit` needs it.
1403#[derive(Default)]
1404pub struct PathStore {
1405    runs: Kept<Run>,
1406    ops: Kept<PathOp>,
1407    scratch: Vec<Vec2>,
1408}
1409
1410impl PathStore {
1411    /// Starts a frame. `keep_prev` retains the list just finished so a
1412    /// departing path's ghost can copy its ops out of it.
1413    pub(crate) fn begin_frame(&mut self, keep_prev: bool) {
1414        self.runs.begin(keep_prev);
1415        self.ops.begin(keep_prev);
1416    }
1417
1418    /// Adds a path (ops in parent-box coordinates): boxes the outline, and
1419    /// stores the ops relative to the box. Returns the id and the box, or
1420    /// None for a path that draws nothing.
1421    pub(crate) fn push(
1422        &mut self,
1423        ops: &[PathOp],
1424        rule: FillRule,
1425        stroke_w: f32,
1426        dash: Option<crate::line::Cut>,
1427        turn: Option<Turn>,
1428    ) -> Option<(PathId, Rect)> {
1429        self.scratch.clear();
1430        flatten(ops, &mut self.scratch);
1431        let b = bounds(&self.scratch)?;
1432        // Two logical px past the outline — one for the bleed, one for
1433        // the edge ramp — and half the stroke's width further for a
1434        // stroke, so no mask is cut by its own edge.
1435        let pad = 2.0 + stroke_w * 0.5;
1436        let rect = match turn {
1437            None => Rect::new(b.x - pad, b.y - pad, b.w + 2.0 * pad, b.h + 2.0 * pad),
1438            // The square the turn sweeps: centred on the pivot, out to
1439            // the farthest point of the outline, so it is the same box —
1440            // and the same ops, hash and mask — at every angle.
1441            Some(turn) => {
1442                let c = turn
1443                    .pivot
1444                    .unwrap_or(Vec2::new(b.x + b.w * 0.5, b.y + b.h * 0.5));
1445                let far = self
1446                    .scratch
1447                    .iter()
1448                    .filter(|p| !p.x.is_nan())
1449                    .map(|p| (p.x - c.x).hypot(p.y - c.y))
1450                    .fold(0.0f32, f32::max);
1451                let half = far + pad;
1452                Rect::new(c.x - half, c.y - half, 2.0 * half, 2.0 * half)
1453            }
1454        };
1455        let origin = Vec2::new(rect.x, rect.y);
1456        let first = self.ops.len();
1457        let shift = Vec2::new(-origin.x, -origin.y);
1458        self.ops.extend(ops.iter().map(|op| op.shifted(shift)));
1459        let hash = hash_ops(&self.ops[first..]);
1460        let id = PathId(self.runs.len() as u32);
1461        self.runs.push(Run {
1462            first: first as u32,
1463            len: (self.ops.len() - first) as u32,
1464            rule,
1465            stroke_w,
1466            dash,
1467            hash,
1468            angle: turn.map(Turn::radians),
1469            animating: false,
1470        });
1471        Some((id, rect))
1472    }
1473
1474    pub(crate) fn set_animating(&mut self, id: PathId) {
1475        if let Some(run) = self.runs.get_mut(id.0 as usize) {
1476            run.animating = true;
1477        }
1478    }
1479
1480    /// This frame's run and its ops, relative to the node.
1481    pub(crate) fn run(&self, id: PathId) -> (Run, &[PathOp]) {
1482        let run = self.runs[id.0 as usize];
1483        (
1484            run,
1485            &self.ops[run.first as usize..(run.first + run.len) as usize],
1486        )
1487    }
1488
1489    /// The same, read from the previous frame's list (a departing path's
1490    /// id indexes that list, not this frame's).
1491    pub(crate) fn prev_run(&self, id: PathId) -> Option<(Run, &[PathOp])> {
1492        let run = *self.runs.prev().get(id.0 as usize)?;
1493        Some((
1494            run,
1495            &self.ops.prev()[run.first as usize..(run.first + run.len) as usize],
1496        ))
1497    }
1498
1499    pub fn len(&self) -> usize {
1500        self.runs.len()
1501    }
1502
1503    pub fn is_empty(&self) -> bool {
1504        self.runs.is_empty()
1505    }
1506}
1507
1508#[cfg(test)]
1509mod tests {
1510    use super::*;
1511
1512    fn pts(ops: &[PathOp]) -> Vec<Vec2> {
1513        let mut out = Vec::new();
1514        flatten(ops, &mut out);
1515        out
1516    }
1517
1518    #[test]
1519    fn parses_absolute_and_relative_commands_alike() {
1520        let a = Path::parse("M10 10 L20 10 L20 20 Z").unwrap();
1521        let b = Path::parse("m10,10 l10 0 l0 10 z").unwrap();
1522        assert_eq!(a.ops(), b.ops());
1523        assert_eq!(a.ops().len(), 4);
1524        // Implicit line-tos after a move, H and V, numbers run together.
1525        let c = Path::parse("M10 10 20 10V20z").unwrap();
1526        assert_eq!(a.ops(), c.ops());
1527        let d = Path::parse("M.5.5-1-1").unwrap();
1528        assert_eq!(
1529            d.ops(),
1530            &[
1531                PathOp::MoveTo(Vec2::new(0.5, 0.5)),
1532                PathOp::LineTo(Vec2::new(-1.0, -1.0))
1533            ]
1534        );
1535    }
1536
1537    #[test]
1538    fn smooth_curves_reflect_the_last_control_point() {
1539        let p = Path::parse("M0 0 C 10 0 20 10 20 20 S 30 40 40 40").unwrap();
1540        match p.ops()[2] {
1541            PathOp::CubicTo(c1, c2, to) => {
1542                assert_eq!(c1, Vec2::new(20.0, 30.0));
1543                assert_eq!(c2, Vec2::new(30.0, 40.0));
1544                assert_eq!(to, Vec2::new(40.0, 40.0));
1545            }
1546            op => panic!("{op:?}"),
1547        }
1548        let q = Path::parse("M0 0 Q 10 10 20 0 T 40 0").unwrap();
1549        match q.ops()[2] {
1550            PathOp::QuadTo(c, to) => {
1551                assert_eq!(c, Vec2::new(30.0, -10.0));
1552                assert_eq!(to, Vec2::new(40.0, 0.0));
1553            }
1554            op => panic!("{op:?}"),
1555        }
1556    }
1557
1558    #[test]
1559    fn a_malformed_string_names_its_byte() {
1560        let e = Path::parse("M10 10 L20").unwrap_err();
1561        assert_eq!(e.what, "a number");
1562        assert_eq!(e.at, 10);
1563        let e = Path::parse("10 10").unwrap_err();
1564        assert_eq!(e.what, "a command letter");
1565        let e = Path::parse("M0 0 X").unwrap_err();
1566        assert_eq!(e.what, "one of M L H V C S Q T A Z");
1567        let e = Path::parse("M0 0 A 5 5 0 2 0 10 10").unwrap_err();
1568        assert_eq!(e.what, "an arc flag (0 or 1)");
1569    }
1570
1571    #[test]
1572    fn the_wire_form_round_trips() {
1573        let p = Path::parse("M1 2 L3 4 Q5 6 7 8 C9 10 11 12 13 14 A15 16 17 1 0 18 19 Z").unwrap();
1574        let f = p.to_floats();
1575        assert_eq!(f.len(), 3 + 3 + 5 + 7 + 8 + 1);
1576        assert_eq!(Path::from_floats(&f).unwrap().ops(), p.ops());
1577        assert!(Path::from_floats(&[9.0]).is_err());
1578        assert!(Path::from_floats(&[OP_LINE, 1.0]).is_err());
1579    }
1580
1581    #[test]
1582    fn a_square_flattens_to_its_corners_and_hits_inside() {
1583        let p = Path::parse("M0 0 H10 V10 H0 Z").unwrap();
1584        let v = pts(p.ops());
1585        assert_eq!(v.len(), 5);
1586        assert!(v[4].x.is_nan());
1587        assert!(in_path(Vec2::new(5.0, 5.0), &v, FillRule::NonZero));
1588        assert!(in_path(Vec2::new(5.0, 5.0), &v, FillRule::EvenOdd));
1589        assert!(!in_path(Vec2::new(15.0, 5.0), &v, FillRule::NonZero));
1590        assert_eq!(bounds(&v), Some(Rect::new(0.0, 0.0, 10.0, 10.0)));
1591    }
1592
1593    #[test]
1594    fn a_ring_is_a_ring_by_either_rule_when_its_contours_oppose() {
1595        // Outer clockwise, inner counter-clockwise (y down).
1596        let p = Path::parse("M0 0 H20 V20 H0 Z M5 5 V15 H15 V5 Z").unwrap();
1597        let v = pts(p.ops());
1598        let hole = Vec2::new(10.0, 10.0);
1599        let band = Vec2::new(2.0, 10.0);
1600        assert!(!in_path(hole, &v, FillRule::NonZero));
1601        assert!(!in_path(hole, &v, FillRule::EvenOdd));
1602        assert!(in_path(band, &v, FillRule::NonZero));
1603        assert!(in_path(band, &v, FillRule::EvenOdd));
1604        // Both the same way: nonzero fills the hole, even-odd does not.
1605        let same = Path::parse("M0 0 H20 V20 H0 Z M5 5 H15 V15 H5 Z").unwrap();
1606        let v = pts(same.ops());
1607        assert!(in_path(hole, &v, FillRule::NonZero));
1608        assert!(!in_path(hole, &v, FillRule::EvenOdd));
1609    }
1610
1611    #[test]
1612    fn an_arc_flattens_onto_its_circle() {
1613        // A half circle of radius 10 from (0, 0) to (20, 0), bowing down.
1614        let p = Path::parse("M0 0 A10 10 0 0 1 20 0").unwrap();
1615        let v = pts(p.ops());
1616        assert!(v.len() >= 9, "{}", v.len());
1617        // Sweep 1 is the positive-angle direction, clockwise on screen
1618        // with y down: from the left end over the top to the right end.
1619        for q in v.iter().filter(|q| !q.x.is_nan()) {
1620            let r = ((q.x - 10.0).powi(2) + q.y.powi(2)).sqrt();
1621            assert!((r - 10.0).abs() < 0.3, "{q:?} is {r} from the centre");
1622            assert!(q.y <= 0.01, "{q:?} bows the wrong way");
1623        }
1624        assert_eq!(v[v.len() - 2], Vec2::new(20.0, 0.0));
1625        // The sweep flag picks the other half.
1626        let down = Path::parse("M0 0 A10 10 0 0 0 20 0").unwrap();
1627        assert!(
1628            pts(down.ops())
1629                .iter()
1630                .filter(|q| !q.x.is_nan())
1631                .all(|q| q.y >= -0.01)
1632        );
1633    }
1634
1635    #[test]
1636    fn a_sector_is_a_wedge_and_a_full_turn_is_a_disc() {
1637        let w = Path::sector(50.0, 50.0, 40.0, 0.0, 0.0, 0.25);
1638        let v = pts(w.ops());
1639        assert!(in_path(Vec2::new(70.0, 70.0), &v, FillRule::NonZero));
1640        assert!(!in_path(Vec2::new(30.0, 30.0), &v, FillRule::NonZero));
1641        let d = Path::sector(50.0, 50.0, 40.0, 20.0, 0.0, 1.0);
1642        let v = pts(d.ops());
1643        assert!(in_path(Vec2::new(50.0, 20.0), &v, FillRule::NonZero));
1644        assert!(!in_path(Vec2::new(50.0, 50.0), &v, FillRule::NonZero));
1645    }
1646
1647    #[test]
1648    fn a_draw_after_a_close_starts_where_the_subpath_began() {
1649        // The second triangle is (10,10) (10,40) (30,40): the hit outline
1650        // and the mask agree on it.
1651        let p = Path::parse("M10 10 H30 V30 Z L10 40 L30 40 Z").unwrap();
1652        let v = pts(p.ops());
1653        let m = rasterize(
1654            p.ops(),
1655            1.0,
1656            (0, 0),
1657            48,
1658            48,
1659            MaskPaint::Fill(FillRule::NonZero),
1660        );
1661        for (x, y) in [(12usize, 25usize), (28, 32), (28, 20), (14, 36)] {
1662            let hit = in_path(
1663                Vec2::new(x as f32 + 0.5, y as f32 + 0.5),
1664                &v,
1665                FillRule::NonZero,
1666            );
1667            assert_eq!(m[y * 48 + x] == 255, hit, "({x}, {y})");
1668        }
1669        assert_eq!(m[25 * 48 + 12], 255);
1670        assert_eq!(m[32 * 48 + 28], 0);
1671    }
1672
1673    #[test]
1674    fn a_strokes_pieces_close_only_what_its_z_closed() {
1675        let open = Path::parse("M0 0 L10 0 L10 10").unwrap();
1676        let mut v = Vec::new();
1677        flatten_stroke(open.ops(), &mut v);
1678        assert_eq!(v.len(), 4);
1679        assert_eq!(v[2], Vec2::new(10.0, 10.0));
1680        assert!(v[3].x.is_nan());
1681        let closed = Path::parse("M0 0 L10 0 L10 10 Z").unwrap();
1682        v.clear();
1683        flatten_stroke(closed.ops(), &mut v);
1684        assert_eq!(v.len(), 5);
1685        assert_eq!(v[3], Vec2::ZERO);
1686        assert!(v[4].x.is_nan());
1687    }
1688
1689    #[test]
1690    fn the_hash_follows_the_ops_and_nothing_else() {
1691        let a = Path::parse("M0 0 L10 0 L10 10 Z").unwrap();
1692        let b = Path::parse("M0 0 L10 0 L10 10 Z")
1693            .unwrap()
1694            .fill_rule(FillRule::EvenOdd);
1695        let c = Path::parse("M0 0 L10 0 L10 11 Z").unwrap();
1696        assert_eq!(hash_ops(a.ops()), hash_ops(b.ops()));
1697        assert_ne!(hash_ops(a.ops()), hash_ops(c.ops()));
1698    }
1699
1700    #[test]
1701    fn a_filled_square_is_opaque_inside_and_bleeds_past_its_edge() {
1702        let p = Path::parse("M2 2 H12 V12 H2 Z").unwrap();
1703        let m = rasterize(
1704            p.ops(),
1705            1.0,
1706            (0, 0),
1707            16,
1708            16,
1709            MaskPaint::Fill(FillRule::NonZero),
1710        );
1711        let at = |x: usize, y: usize| m[y * 16 + x];
1712        assert_eq!(at(7, 7), 255);
1713        assert_eq!(at(0, 0), 0);
1714        assert_eq!(at(14, 7), 0);
1715        // The edge at x = 2 lands between pixels 1 and 2: pixel 1 is the
1716        // bleed's half, pixel 2 solid.
1717        assert!(at(1, 7) > 100 && at(1, 7) < 200, "{}", at(1, 7));
1718        assert_eq!(at(2, 7), 255);
1719        // Two squares sharing the edge x = 12 composite to full coverage
1720        // along it: the right one's left bleed over the left one's own.
1721        let q = Path::parse("M12 2 H22 V12 H12 Z").unwrap();
1722        let n = rasterize(
1723            q.ops(),
1724            1.0,
1725            (0, 0),
1726            24,
1727            16,
1728            MaskPaint::Fill(FillRule::NonZero),
1729        );
1730        let (a, b) = (
1731            f32::from(at(12, 7)) / 255.0,
1732            f32::from(n[7 * 24 + 12]) / 255.0,
1733        );
1734        assert!(a + b * (1.0 - a) > 0.99, "{a} over {b}");
1735        let (a, b) = (
1736            f32::from(at(11, 7)) / 255.0,
1737            f32::from(n[7 * 24 + 11]) / 255.0,
1738        );
1739        assert!(a + b * (1.0 - a) > 0.99, "{a} over {b}");
1740    }
1741
1742    /// A dashed stroke's mask is the marks a dashed line would draw: the
1743    /// lengths seen are the ones declared, a short mark is a dot, and the
1744    /// offset moves them towards the start.
1745    #[test]
1746    fn a_dashed_stroke_is_marks_and_gaps() {
1747        let p = Path::parse("M4 8 H44").unwrap();
1748        let row = |dash: crate::line::Dash| {
1749            let paint = MaskPaint::Dashed(2.0, dash.cut(2.0).unwrap());
1750            let m = rasterize(p.ops(), 1.0, (0, 0), 48, 16, paint);
1751            // The row of pixels under the centre line, on or off.
1752            (0..48).map(|x| m[8 * 48 + x] > 127).collect::<Vec<_>>()
1753        };
1754        let on = |r: &[bool], x: std::ops::Range<usize>| r[x].iter().all(|&b| b);
1755        let off = |r: &[bool], x: std::ops::Range<usize>| r[x].iter().all(|&b| !b);
1756        // 6 on, 4 off from a cap before x = 4: marks cover 3..9, 13..19.
1757        let r = row(crate::line::Dash::new(6.0, 4.0));
1758        assert!(on(&r, 3..9) && off(&r, 10..12) && on(&r, 13..19), "{r:?}");
1759        // Dots 2 across, 8 apart.
1760        let r = row(crate::line::Dash::new(2.0, 6.0));
1761        assert!(on(&r, 3..5) && off(&r, 6..10) && on(&r, 11..13), "{r:?}");
1762        // 5 px in: the first mark's last pixel, then the gap.
1763        let r = row(crate::line::Dash::new(6.0, 4.0).offset(5.0));
1764        assert!(off(&r, 6..7) && on(&r, 8..14), "{r:?}");
1765    }
1766
1767    #[test]
1768    fn a_stroke_covers_the_outline_and_not_the_inside() {
1769        let p = Path::parse("M2 2 H12 V12 H2 Z").unwrap();
1770        let m = rasterize(p.ops(), 1.0, (0, 0), 16, 16, MaskPaint::Stroke(2.0));
1771        let at = |x: usize, y: usize| m[y * 16 + x];
1772        assert_eq!(at(7, 7), 0);
1773        assert!(at(2, 7) > 200, "{}", at(2, 7));
1774        assert!(at(1, 7) > 200, "{}", at(1, 7));
1775    }
1776
1777    #[test]
1778    fn the_store_boxes_a_path_and_keeps_the_ops_relative() {
1779        let mut store = PathStore::default();
1780        store.begin_frame(false);
1781        let p = Path::parse("M10 20 H30 V40 Z").unwrap();
1782        let (id, rect) = store
1783            .push(p.ops(), FillRule::NonZero, 0.0, None, None)
1784            .unwrap();
1785        assert_eq!(rect, Rect::new(8.0, 18.0, 24.0, 24.0));
1786        let (run, ops) = store.run(id);
1787        assert_eq!(ops[0], PathOp::MoveTo(Vec2::new(2.0, 2.0)));
1788        assert_eq!(run.len, 4);
1789        assert!(
1790            store
1791                .push(
1792                    &[PathOp::MoveTo(Vec2::ZERO)],
1793                    FillRule::NonZero,
1794                    0.0,
1795                    None,
1796                    None
1797                )
1798                .is_none()
1799        );
1800        // A stroke widens the box by half its width.
1801        let (_, rect) = store
1802            .push(p.ops(), FillRule::NonZero, 4.0, None, None)
1803            .unwrap();
1804        assert_eq!(rect, Rect::new(6.0, 16.0, 28.0, 28.0));
1805        // A turn boxes the path by the square it sweeps about its pivot:
1806        // the same box, ops and hash at any angle.
1807        let turn = |t: f32| Turn {
1808            turns: t,
1809            pivot: Some(Vec2::new(10.0, 20.0)),
1810        };
1811        let (a, ra) = store
1812            .push(p.ops(), FillRule::NonZero, 0.0, None, Some(turn(0.0)))
1813            .unwrap();
1814        let (b, rb) = store
1815            .push(p.ops(), FillRule::NonZero, 0.0, None, Some(turn(0.3)))
1816            .unwrap();
1817        let far = (20.0f32 * 20.0 + 20.0 * 20.0).sqrt() + 2.0;
1818        assert_eq!(ra, Rect::new(10.0 - far, 20.0 - far, 2.0 * far, 2.0 * far));
1819        assert_eq!(ra, rb);
1820        assert_eq!(store.run(a).0.hash, store.run(b).0.hash);
1821        assert_eq!(store.run(a).1, store.run(b).1);
1822        assert_eq!(store.run(b).0.angle, Some(0.3 * std::f32::consts::TAU));
1823        store.begin_frame(true);
1824        assert!(store.prev_run(id).is_some());
1825        assert!(store.is_empty());
1826    }
1827
1828    /// A fill with no area paints nothing: the bleed is an edge's, and a
1829    /// progress ring at 0 has none.
1830    #[test]
1831    fn a_fill_with_no_area_is_an_empty_mask() {
1832        for p in [
1833            Path::sector(16.0, 16.0, 12.0, 8.0, 0.0, 0.0),
1834            Path::parse("M2 2 L20 2 Z").unwrap(),
1835        ] {
1836            let m = rasterize(
1837                p.ops(),
1838                1.0,
1839                (0, 0),
1840                32,
1841                32,
1842                MaskPaint::Fill(FillRule::NonZero),
1843            );
1844            assert!(m.iter().all(|&a| a == 0));
1845        }
1846    }
1847}