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 in a mask's physical px: every point at `scale` from
123    /// `off`, the radii at `scale`.
124    fn placed(&self, scale: f32, off: Vec2) -> PathOp {
125        let s = |p: Vec2| Vec2::new(p.x * scale + off.x, p.y * scale + off.y);
126        match *self {
127            PathOp::MoveTo(p) => PathOp::MoveTo(s(p)),
128            PathOp::LineTo(p) => PathOp::LineTo(s(p)),
129            PathOp::QuadTo(c, p) => PathOp::QuadTo(s(c), s(p)),
130            PathOp::CubicTo(a, b, p) => PathOp::CubicTo(s(a), s(b), s(p)),
131            PathOp::ArcTo {
132                rx,
133                ry,
134                rotation,
135                large,
136                sweep,
137                to,
138            } => PathOp::ArcTo {
139                rx: rx * scale,
140                ry: ry * scale,
141                rotation,
142                large,
143                sweep,
144                to: s(to),
145            },
146            PathOp::Close => PathOp::Close,
147        }
148    }
149
150    /// The op with every point moved by `d`.
151    fn shifted(&self, d: Vec2) -> PathOp {
152        let s = |p: Vec2| Vec2::new(p.x + d.x, p.y + d.y);
153        match *self {
154            PathOp::MoveTo(p) => PathOp::MoveTo(s(p)),
155            PathOp::LineTo(p) => PathOp::LineTo(s(p)),
156            PathOp::QuadTo(c, p) => PathOp::QuadTo(s(c), s(p)),
157            PathOp::CubicTo(a, b, p) => PathOp::CubicTo(s(a), s(b), s(p)),
158            PathOp::ArcTo {
159                rx,
160                ry,
161                rotation,
162                large,
163                sweep,
164                to,
165            } => PathOp::ArcTo {
166                rx,
167                ry,
168                rotation,
169                large,
170                sweep,
171                to: s(to),
172            },
173            PathOp::Close => PathOp::Close,
174        }
175    }
176}
177
178/// How a path's inside is decided: SVG's default, or the polygon's rule.
179#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
180pub enum FillRule {
181    /// A point is inside when the outline winds around it a net nonzero
182    /// number of times: a self-intersecting outline fills its overlaps.
183    #[default]
184    NonZero,
185    /// A point is inside when an odd number of edges cross a ray from it:
186    /// overlaps are unfilled, which is how a ring is a ring.
187    EvenOdd,
188}
189
190impl FillRule {
191    /// The names the bindings spell: `nonzero`, `evenodd`.
192    pub const ALL: &[&str] = &["nonzero", "evenodd"];
193
194    pub fn from_index(i: usize) -> Self {
195        match i {
196            1 => FillRule::EvenOdd,
197            _ => FillRule::NonZero,
198        }
199    }
200
201    pub fn index(self) -> usize {
202        match self {
203            FillRule::NonZero => 0,
204            FillRule::EvenOdd => 1,
205        }
206    }
207
208    pub fn parse(s: &str) -> Option<Self> {
209        match s {
210            "nonzero" => Some(FillRule::NonZero),
211            "evenodd" => Some(FillRule::EvenOdd),
212            _ => None,
213        }
214    }
215}
216
217/// A path's turn (`docs/adr/0041-a-mask-turns-about-its-centre.md`): how
218/// far it is turned, in turns — clockwise with y down, as
219/// [`Path::sector`] counts them — and the point it turns about, in the
220/// path's own coordinates; `None` is the centre of the outline's box. A
221/// path with a turn is boxed by the square the turn sweeps, so its mask
222/// is one mask at every angle and the quad that draws it carries the
223/// angle.
224#[derive(Clone, Copy, Debug, Default, PartialEq)]
225pub struct Turn {
226    pub turns: f32,
227    pub pivot: Option<Vec2>,
228}
229
230impl Turn {
231    /// The angle in radians, clockwise with y down.
232    pub fn radians(self) -> f32 {
233        self.turns * std::f32::consts::TAU
234    }
235}
236
237/// `p` turned by `angle` radians (clockwise, y down) about `c`.
238pub fn turned(p: Vec2, c: Vec2, angle: f32) -> Vec2 {
239    let (sin, cos) = angle.sin_cos();
240    let (x, y) = (p.x - c.x, p.y - c.y);
241    Vec2::new(c.x + x * cos - y * sin, c.y + x * sin + y * cos)
242}
243
244/// Where a `d` string went wrong: the byte offset and what was expected.
245#[derive(Clone, Debug, PartialEq, Eq)]
246pub struct PathError {
247    pub at: usize,
248    pub what: &'static str,
249}
250
251impl std::fmt::Display for PathError {
252    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
253        write!(f, "{} at byte {}", self.what, self.at)
254    }
255}
256
257impl std::error::Error for PathError {}
258
259/// A path as an app builds one: the ops, the fill rule, and an optional
260/// stroke. The fill colour is the node's `bg`; the stroke's width and
261/// colour are the [`crate::line::Stroke`]'s (`curve` is ignored).
262#[derive(Clone, Debug, PartialEq)]
263pub struct Path {
264    ops: Vec<PathOp>,
265    rule: FillRule,
266    stroke: Option<crate::line::Stroke>,
267    turn: Option<Turn>,
268}
269
270impl Default for Path {
271    fn default() -> Self {
272        Self::new()
273    }
274}
275
276impl Path {
277    pub fn new() -> Self {
278        Self {
279            ops: Vec::new(),
280            rule: FillRule::NonZero,
281            stroke: None,
282            turn: None,
283        }
284    }
285
286    /// A path over ops already built.
287    pub fn from_ops(ops: Vec<PathOp>) -> Self {
288        Self {
289            ops,
290            rule: FillRule::NonZero,
291            stroke: None,
292            turn: None,
293        }
294    }
295
296    /// A path from its flat wire form: a code, then its operands, per op.
297    pub fn from_floats(f: &[f32]) -> Result<Self, PathError> {
298        let mut ops = Vec::new();
299        let mut i = 0;
300        while i < f.len() {
301            let code = f[i];
302            let Some(n) = PathOp::operands(code) else {
303                return Err(PathError {
304                    at: i,
305                    what: "an op code 0..=5",
306                });
307            };
308            let Some(a) = f.get(i + 1..i + 1 + n) else {
309                return Err(PathError {
310                    at: i,
311                    what: "the op's operands",
312                });
313            };
314            ops.push(match code as i32 {
315                0 => PathOp::MoveTo(Vec2::new(a[0], a[1])),
316                1 => PathOp::LineTo(Vec2::new(a[0], a[1])),
317                2 => PathOp::QuadTo(Vec2::new(a[0], a[1]), Vec2::new(a[2], a[3])),
318                3 => PathOp::CubicTo(
319                    Vec2::new(a[0], a[1]),
320                    Vec2::new(a[2], a[3]),
321                    Vec2::new(a[4], a[5]),
322                ),
323                4 => PathOp::ArcTo {
324                    rx: a[0],
325                    ry: a[1],
326                    rotation: a[2],
327                    large: a[3] != 0.0,
328                    sweep: a[4] != 0.0,
329                    to: Vec2::new(a[5], a[6]),
330                },
331                _ => PathOp::Close,
332            });
333            i += 1 + n;
334        }
335        Ok(Self::from_ops(ops))
336    }
337
338    /// The flat wire form.
339    pub fn to_floats(&self) -> Vec<f32> {
340        let mut out = Vec::with_capacity(self.ops.len() * 3);
341        for op in &self.ops {
342            op.write(&mut out);
343        }
344        out
345    }
346
347    pub fn ops(&self) -> &[PathOp] {
348        &self.ops
349    }
350
351    pub fn into_ops(self) -> Vec<PathOp> {
352        self.ops
353    }
354
355    pub fn rule(&self) -> FillRule {
356        self.rule
357    }
358
359    pub fn stroke(&self) -> Option<crate::line::Stroke> {
360        self.stroke
361    }
362
363    pub fn turn(&self) -> Option<Turn> {
364        self.turn
365    }
366
367    /// Turns the path by `turns` (clockwise, y down) about its pivot —
368    /// the centre of its outline's box unless [`Self::pivot`] names one.
369    /// The turn is the quad's and not the mask's: one raster, whatever
370    /// the angle.
371    pub fn rotated(mut self, turns: f32) -> Self {
372        self.turn.get_or_insert_default().turns = turns;
373        self
374    }
375
376    /// The point the path turns about, in the path's own coordinates. A
377    /// spinner's arc names its circle's centre, which is not its own
378    /// box's.
379    pub fn pivot(mut self, x: f32, y: f32) -> Self {
380        self.turn.get_or_insert_default().pivot = Some(Vec2::new(x, y));
381        self
382    }
383
384    pub fn fill_rule(mut self, rule: FillRule) -> Self {
385        self.rule = rule;
386        self
387    }
388
389    /// Strokes the outline `stroke.width` wide in `stroke.color`, over
390    /// the fill. Round joins and caps.
391    pub fn stroked(mut self, stroke: crate::line::Stroke) -> Self {
392        self.stroke = Some(stroke);
393        self
394    }
395
396    pub fn move_to(mut self, x: f32, y: f32) -> Self {
397        self.ops.push(PathOp::MoveTo(Vec2::new(x, y)));
398        self
399    }
400
401    pub fn line_to(mut self, x: f32, y: f32) -> Self {
402        self.ops.push(PathOp::LineTo(Vec2::new(x, y)));
403        self
404    }
405
406    pub fn quad_to(mut self, cx: f32, cy: f32, x: f32, y: f32) -> Self {
407        self.ops
408            .push(PathOp::QuadTo(Vec2::new(cx, cy), Vec2::new(x, y)));
409        self
410    }
411
412    pub fn cubic_to(mut self, c1x: f32, c1y: f32, c2x: f32, c2y: f32, x: f32, y: f32) -> Self {
413        self.ops.push(PathOp::CubicTo(
414            Vec2::new(c1x, c1y),
415            Vec2::new(c2x, c2y),
416            Vec2::new(x, y),
417        ));
418        self
419    }
420
421    /// An elliptical arc to `(x, y)`, as SVG's `A`: radii, the ellipse's
422    /// rotation in degrees, whether the larger of the two arcs is taken,
423    /// whether it sweeps clockwise (y down).
424    #[allow(clippy::too_many_arguments)]
425    pub fn arc_to(
426        mut self,
427        rx: f32,
428        ry: f32,
429        rotation: f32,
430        large: bool,
431        sweep: bool,
432        x: f32,
433        y: f32,
434    ) -> Self {
435        self.ops.push(PathOp::ArcTo {
436            rx,
437            ry,
438            rotation,
439            large,
440            sweep,
441            to: Vec2::new(x, y),
442        });
443        self
444    }
445
446    pub fn close(mut self) -> Self {
447        self.ops.push(PathOp::Close);
448        self
449    }
450
451    /// A sector of an annulus centred on `(cx, cy)`: from `from` turns
452    /// (0 is east, turns run clockwise with y down) sweeping `sweep` turns,
453    /// between `inner` and `outer` radii. A pie wedge with `inner` 0, a
454    /// donut's segment otherwise, a ring with `sweep` 1.
455    pub fn sector(cx: f32, cy: f32, outer: f32, inner: f32, from: f32, sweep: f32) -> Self {
456        let tau = std::f32::consts::TAU;
457        let sweep = sweep.clamp(-1.0, 1.0);
458        let (a0, a1) = (from * tau, (from + sweep) * tau);
459        let at = |r: f32, a: f32| (cx + r * a.cos(), cy + r * a.sin());
460        let large = sweep.abs() > 0.5;
461        let cw = sweep >= 0.0;
462        if sweep.abs() >= 1.0 {
463            // A full turn is two half arcs, since one arc cannot return to
464            // its start.
465            let mid = a0 + tau * 0.5;
466            let (ox, oy) = at(outer, a0);
467            let (mx, my) = at(outer, mid);
468            let mut p = Path::new()
469                .move_to(ox, oy)
470                .arc_to(outer, outer, 0.0, false, cw, mx, my)
471                .arc_to(outer, outer, 0.0, false, cw, ox, oy)
472                .close();
473            if inner > 0.0 {
474                let (ix, iy) = at(inner, a0);
475                let (jx, jy) = at(inner, mid);
476                p = p
477                    .move_to(ix, iy)
478                    .arc_to(inner, inner, 0.0, false, !cw, jx, jy)
479                    .arc_to(inner, inner, 0.0, false, !cw, ix, iy)
480                    .close();
481            }
482            return p;
483        }
484        let (ox0, oy0) = at(outer, a0);
485        let (ox1, oy1) = at(outer, a1);
486        let mut p = Path::new()
487            .move_to(ox0, oy0)
488            .arc_to(outer, outer, 0.0, large, cw, ox1, oy1);
489        if inner > 0.0 {
490            let (ix1, iy1) = at(inner, a1);
491            let (ix0, iy0) = at(inner, a0);
492            p = p
493                .line_to(ix1, iy1)
494                .arc_to(inner, inner, 0.0, large, !cw, ix0, iy0);
495        } else {
496            p = p.line_to(cx, cy);
497        }
498        p.close()
499    }
500
501    /// Parses SVG path data: `M L H V C S Q T A Z` and their relative
502    /// forms, implicit repeats, numbers run together as SVG allows. Every
503    /// relative command, `H`, `V`, `S` and `T` is expanded into the six
504    /// ops, so what comes out is the same in every binding.
505    pub fn parse(d: &str) -> Result<Self, PathError> {
506        let b = d.as_bytes();
507        let mut i = 0;
508        let mut ops = Vec::new();
509        let mut cur = Vec2::ZERO;
510        let mut start = Vec2::ZERO;
511        // The last control point, for `S` / `T` reflection, and which
512        // kind of curve left it.
513        let mut last_cubic: Option<Vec2> = None;
514        let mut last_quad: Option<Vec2> = None;
515        let mut cmd: Option<u8> = None;
516
517        fn skip_ws(b: &[u8], i: &mut usize) {
518            while *i < b.len() && (b[*i].is_ascii_whitespace() || b[*i] == b',') {
519                *i += 1;
520            }
521        }
522        fn number(b: &[u8], i: &mut usize) -> Result<f32, PathError> {
523            skip_ws(b, i);
524            let at = *i;
525            let mut j = at;
526            if j < b.len() && (b[j] == b'-' || b[j] == b'+') {
527                j += 1;
528            }
529            let digits = j;
530            while j < b.len() && b[j].is_ascii_digit() {
531                j += 1;
532            }
533            if j < b.len() && b[j] == b'.' {
534                j += 1;
535                while j < b.len() && b[j].is_ascii_digit() {
536                    j += 1;
537                }
538            }
539            if j == digits || (j == digits + 1 && b[digits] == b'.') {
540                return Err(PathError {
541                    at,
542                    what: "a number",
543                });
544            }
545            if j < b.len() && (b[j] == b'e' || b[j] == b'E') {
546                let mut k = j + 1;
547                if k < b.len() && (b[k] == b'-' || b[k] == b'+') {
548                    k += 1;
549                }
550                let e = k;
551                while k < b.len() && b[k].is_ascii_digit() {
552                    k += 1;
553                }
554                if k > e {
555                    j = k;
556                }
557            }
558            let s = std::str::from_utf8(&b[at..j]).map_err(|_| PathError {
559                at,
560                what: "a number",
561            })?;
562            *i = j;
563            s.parse::<f32>().map_err(|_| PathError {
564                at,
565                what: "a number",
566            })
567        }
568        fn flag(b: &[u8], i: &mut usize) -> Result<bool, PathError> {
569            skip_ws(b, i);
570            match b.get(*i) {
571                Some(b'0') => {
572                    *i += 1;
573                    Ok(false)
574                }
575                Some(b'1') => {
576                    *i += 1;
577                    Ok(true)
578                }
579                _ => Err(PathError {
580                    at: *i,
581                    what: "an arc flag (0 or 1)",
582                }),
583            }
584        }
585
586        loop {
587            skip_ws(b, &mut i);
588            if i >= b.len() {
589                break;
590            }
591            let c = b[i];
592            if c.is_ascii_alphabetic() {
593                cmd = Some(c);
594                i += 1;
595                if c == b'Z' || c == b'z' {
596                    ops.push(PathOp::Close);
597                    cur = start;
598                    last_cubic = None;
599                    last_quad = None;
600                    continue;
601                }
602            } else if cmd.is_none() {
603                return Err(PathError {
604                    at: i,
605                    what: "a command letter",
606                });
607            }
608            let Some(c) = cmd else { break };
609            let rel = c.is_ascii_lowercase();
610            let base = if rel { cur } else { Vec2::ZERO };
611            let point = |b: &[u8], i: &mut usize| -> Result<Vec2, PathError> {
612                let x = number(b, i)?;
613                let y = number(b, i)?;
614                Ok(Vec2::new(base.x + x, base.y + y))
615            };
616            match c.to_ascii_uppercase() {
617                b'M' => {
618                    let p = point(b, &mut i)?;
619                    ops.push(PathOp::MoveTo(p));
620                    cur = p;
621                    start = p;
622                    // Further pairs are implicit line-tos.
623                    cmd = Some(if rel { b'l' } else { b'L' });
624                    last_cubic = None;
625                    last_quad = None;
626                }
627                b'L' => {
628                    let p = point(b, &mut i)?;
629                    ops.push(PathOp::LineTo(p));
630                    cur = p;
631                    last_cubic = None;
632                    last_quad = None;
633                }
634                b'H' => {
635                    let x = number(b, &mut i)?;
636                    let p = Vec2::new(base.x + x, cur.y);
637                    ops.push(PathOp::LineTo(p));
638                    cur = p;
639                    last_cubic = None;
640                    last_quad = None;
641                }
642                b'V' => {
643                    let y = number(b, &mut i)?;
644                    let p = Vec2::new(cur.x, base.y + y);
645                    ops.push(PathOp::LineTo(p));
646                    cur = p;
647                    last_cubic = None;
648                    last_quad = None;
649                }
650                b'C' => {
651                    let c1 = point(b, &mut i)?;
652                    let c2 = point(b, &mut i)?;
653                    let p = point(b, &mut i)?;
654                    ops.push(PathOp::CubicTo(c1, c2, p));
655                    last_cubic = Some(c2);
656                    last_quad = None;
657                    cur = p;
658                }
659                b'S' => {
660                    let c2 = point(b, &mut i)?;
661                    let p = point(b, &mut i)?;
662                    let c1 = match last_cubic {
663                        Some(l) => Vec2::new(2.0 * cur.x - l.x, 2.0 * cur.y - l.y),
664                        None => cur,
665                    };
666                    ops.push(PathOp::CubicTo(c1, c2, p));
667                    last_cubic = Some(c2);
668                    last_quad = None;
669                    cur = p;
670                }
671                b'Q' => {
672                    let c1 = point(b, &mut i)?;
673                    let p = point(b, &mut i)?;
674                    ops.push(PathOp::QuadTo(c1, p));
675                    last_quad = Some(c1);
676                    last_cubic = None;
677                    cur = p;
678                }
679                b'T' => {
680                    let p = point(b, &mut i)?;
681                    let c1 = match last_quad {
682                        Some(l) => Vec2::new(2.0 * cur.x - l.x, 2.0 * cur.y - l.y),
683                        None => cur,
684                    };
685                    ops.push(PathOp::QuadTo(c1, p));
686                    last_quad = Some(c1);
687                    last_cubic = None;
688                    cur = p;
689                }
690                b'A' => {
691                    let rx = number(b, &mut i)?;
692                    let ry = number(b, &mut i)?;
693                    let rotation = number(b, &mut i)?;
694                    let large = flag(b, &mut i)?;
695                    let sweep = flag(b, &mut i)?;
696                    let p = point(b, &mut i)?;
697                    ops.push(PathOp::ArcTo {
698                        rx: rx.abs(),
699                        ry: ry.abs(),
700                        rotation,
701                        large,
702                        sweep,
703                        to: p,
704                    });
705                    last_cubic = None;
706                    last_quad = None;
707                    cur = p;
708                }
709                _ => {
710                    return Err(PathError {
711                        at: i - 1,
712                        what: "one of M L H V C S Q T A Z",
713                    });
714                }
715            }
716        }
717        Ok(Self::from_ops(ops))
718    }
719}
720
721/// Separates two contours in a flattened outline: a point that is not a
722/// point. `in_path` reads it as the end of one closed contour and the
723/// start of the next.
724pub const CONTOUR_BREAK: Vec2 = Vec2 {
725    x: f32::NAN,
726    y: f32::NAN,
727};
728
729/// How far a flattened curve may stray from the true one, logical px.
730/// The hit outline's tolerance; the rasterizer flattens on its own.
731pub const FLATTEN_TOLERANCE: f32 = 0.2;
732
733/// The most pieces one curve or arc is cut into.
734const MAX_PIECES: usize = 128;
735
736/// How finely a flattening cuts: how far a chord may stray from the
737/// curve, and the most pieces one curve or arc becomes.
738#[derive(Clone, Copy)]
739struct Fineness {
740    tolerance: f32,
741    pieces: usize,
742}
743
744/// The hit outline's, in logical px.
745const HIT: Fineness = Fineness {
746    tolerance: FLATTEN_TOLERANCE,
747    pieces: MAX_PIECES,
748};
749
750/// A stroke's, in physical px, as the rasterizer is handed it (see
751/// [`rasterize_at`]): a tenth of a pixel, under what antialiasing shows.
752const RASTER: Fineness = Fineness {
753    tolerance: 0.1,
754    pieces: 4096,
755};
756
757/// Flattens `ops` into closed contours in `out`, each contour's points
758/// followed by [`CONTOUR_BREAK`]. Every subpath is closed for the
759/// purpose of a fill, as SVG closes it. A move with nothing after it
760/// adds nothing.
761pub fn flatten(ops: &[PathOp], out: &mut Vec<Vec2>) {
762    flatten_as(ops, out, false, HIT, None);
763}
764
765/// Flattens `ops` into the polylines a stroke draws, each followed by
766/// [`CONTOUR_BREAK`]: a subpath its `Z` closed ends back on its start, an
767/// open one ends where it was left, so a stroke's hit pieces are the
768/// pieces it paints and no chord across an open curve is among them.
769pub fn flatten_stroke(ops: &[PathOp], out: &mut Vec<Vec2>) {
770    flatten_as(ops, out, true, HIT, None);
771}
772
773/// With `closes`, a stroke's flattening says for each contour, in order,
774/// whether its `Z` closed it — and leaves the start off its end, for the
775/// rasterizer to close it as one outline, the way a stroke joins there.
776fn flatten_as(
777    ops: &[PathOp],
778    out: &mut Vec<Vec2>,
779    stroke: bool,
780    fine: Fineness,
781    mut closes: Option<&mut Vec<bool>>,
782) {
783    let mut cur = Vec2::ZERO;
784    let mut open = false;
785    let mut contour_start = out.len();
786    let mut close = |out: &mut Vec<Vec2>, open: &mut bool, contour_start: usize, closed: bool| {
787        if *open {
788            // A contour of one point is nothing.
789            if out.len() - contour_start >= 2 {
790                match closes.as_deref_mut() {
791                    Some(closes) => closes.push(closed),
792                    None if stroke && closed => out.push(out[contour_start]),
793                    None => {}
794                }
795                out.push(CONTOUR_BREAK);
796            } else {
797                out.truncate(contour_start);
798            }
799            *open = false;
800        }
801    };
802    for op in ops {
803        match *op {
804            PathOp::MoveTo(p) => {
805                close(out, &mut open, contour_start, false);
806                contour_start = out.len();
807                out.push(p);
808                cur = p;
809                open = true;
810            }
811            PathOp::Close => {
812                close(out, &mut open, contour_start, true);
813                // A draw after a close starts where the subpath began.
814                if let Some(&s) = out.get(contour_start) {
815                    cur = s;
816                }
817                contour_start = out.len();
818            }
819            _ => {
820                if !open {
821                    contour_start = out.len();
822                    out.push(cur);
823                    open = true;
824                }
825                match *op {
826                    PathOp::LineTo(p) => {
827                        out.push(p);
828                        cur = p;
829                    }
830                    PathOp::QuadTo(c, p) => {
831                        flatten_quad(cur, c, p, out, fine);
832                        cur = p;
833                    }
834                    PathOp::CubicTo(a, b, p) => {
835                        flatten_cubic(cur, a, b, p, out, fine);
836                        cur = p;
837                    }
838                    PathOp::ArcTo {
839                        rx,
840                        ry,
841                        rotation,
842                        large,
843                        sweep,
844                        to,
845                    } => {
846                        flatten_arc(cur, rx, ry, rotation, large, sweep, to, out, fine);
847                        cur = to;
848                    }
849                    PathOp::MoveTo(_) | PathOp::Close => unreachable!(),
850                }
851            }
852        }
853    }
854    close(out, &mut open, contour_start, false);
855}
856
857fn pieces(dd: f32, k: f32, fine: Fineness) -> usize {
858    ((dd * k / fine.tolerance).sqrt().ceil() as usize).clamp(1, fine.pieces)
859}
860
861fn flatten_quad(p0: Vec2, c: Vec2, p1: Vec2, out: &mut Vec<Vec2>, fine: Fineness) {
862    let dd = Vec2::new(p0.x - 2.0 * c.x + p1.x, p0.y - 2.0 * c.y + p1.y);
863    let n = pieces((dd.x * dd.x + dd.y * dd.y).sqrt(), 0.25, fine);
864    for i in 1..=n {
865        let t = i as f32 / n as f32;
866        let u = 1.0 - t;
867        out.push(Vec2::new(
868            u * u * p0.x + 2.0 * u * t * c.x + t * t * p1.x,
869            u * u * p0.y + 2.0 * u * t * c.y + t * t * p1.y,
870        ));
871    }
872}
873
874fn flatten_cubic(p0: Vec2, a: Vec2, b: Vec2, p1: Vec2, out: &mut Vec<Vec2>, fine: Fineness) {
875    let d1 = Vec2::new(p0.x - 2.0 * a.x + b.x, p0.y - 2.0 * a.y + b.y);
876    let d2 = Vec2::new(a.x - 2.0 * b.x + p1.x, a.y - 2.0 * b.y + p1.y);
877    let dd = (d1.x * d1.x + d1.y * d1.y)
878        .max(d2.x * d2.x + d2.y * d2.y)
879        .sqrt();
880    let n = pieces(dd, 0.75, fine);
881    for i in 1..=n {
882        let t = i as f32 / n as f32;
883        let u = 1.0 - t;
884        let (uu, tt) = (u * u, t * t);
885        out.push(Vec2::new(
886            uu * u * p0.x + 3.0 * uu * t * a.x + 3.0 * u * tt * b.x + tt * t * p1.x,
887            uu * u * p0.y + 3.0 * uu * t * a.y + 3.0 * u * tt * b.y + tt * t * p1.y,
888        ));
889    }
890}
891
892/// An arc's rotation in degrees within one turn, exactly for any finite
893/// number: a rotation past 1e38 was a NaN in every point of zeno's arc
894/// (backlog FZ2), and the sine of one is noise even where it is a number.
895fn turn_of(degrees: f32) -> f32 {
896    degrees.rem_euclid(360.0)
897}
898
899/// An SVG arc in centre form: the centre, the radii as scaled up when the
900/// endpoints are too far apart, the start angle and the sweep in radians.
901/// `None` for an arc whose endpoints coincide or whose radii are zero,
902/// which SVG draws as a line (or nothing).
903///
904/// Worked in f64: the products of four radii it takes overflow an f32
905/// past a radius of 1e19, and a radius of 1e25 made its centre the
906/// chord's middle and its ellipse a small one (backlog FZ2).
907#[allow(clippy::too_many_arguments)]
908fn arc_center(
909    from: Vec2,
910    rx: f32,
911    ry: f32,
912    rotation: f32,
913    large: bool,
914    sweep: bool,
915    to: Vec2,
916) -> Option<(Vec2, f32, f32, f32, f32, f32)> {
917    if (from.x - to.x).abs() < 1e-6 && (from.y - to.y).abs() < 1e-6 {
918        return None;
919    }
920    let (mut rx, mut ry) = (f64::from(rx.abs()), f64::from(ry.abs()));
921    if rx < 1e-6 || ry < 1e-6 {
922        return None;
923    }
924    let phi = f64::from(turn_of(rotation)).to_radians();
925    let (sin_phi, cos_phi) = phi.sin_cos();
926    let (fx, fy, tx, ty) = (
927        f64::from(from.x),
928        f64::from(from.y),
929        f64::from(to.x),
930        f64::from(to.y),
931    );
932    let dx = (fx - tx) * 0.5;
933    let dy = (fy - ty) * 0.5;
934    let x1 = cos_phi * dx + sin_phi * dy;
935    let y1 = -sin_phi * dx + cos_phi * dy;
936    let lambda = (x1 * x1) / (rx * rx) + (y1 * y1) / (ry * ry);
937    if lambda > 1.0 {
938        let s = lambda.sqrt();
939        rx *= s;
940        ry *= s;
941    }
942    let num = (rx * rx * ry * ry - rx * rx * y1 * y1 - ry * ry * x1 * x1).max(0.0);
943    let den = rx * rx * y1 * y1 + ry * ry * x1 * x1;
944    let mut coef = if den > 0.0 { (num / den).sqrt() } else { 0.0 };
945    if large == sweep {
946        coef = -coef;
947    }
948    let cx1 = coef * rx * y1 / ry;
949    let cy1 = -coef * ry * x1 / rx;
950    let cx = cos_phi * cx1 - sin_phi * cy1 + (fx + tx) * 0.5;
951    let cy = sin_phi * cx1 + cos_phi * cy1 + (fy + ty) * 0.5;
952    let ux = (x1 - cx1) / rx;
953    let uy = (y1 - cy1) / ry;
954    let vx = (-x1 - cx1) / rx;
955    let vy = (-y1 - cy1) / ry;
956    let angle = |ax: f64, ay: f64, bx: f64, by: f64| -> f64 {
957        let dot = ax * bx + ay * by;
958        let len = ((ax * ax + ay * ay) * (bx * bx + by * by)).sqrt();
959        let mut a = (dot / len).clamp(-1.0, 1.0).acos();
960        if ax * by - ay * bx < 0.0 {
961            a = -a;
962        }
963        a
964    };
965    let theta = angle(1.0, 0.0, ux, uy);
966    let mut delta = angle(ux, uy, vx, vy);
967    let tau = std::f64::consts::TAU;
968    if !sweep && delta > 0.0 {
969        delta -= tau;
970    } else if sweep && delta < 0.0 {
971        delta += tau;
972    }
973    Some((
974        Vec2::new(cx as f32, cy as f32),
975        rx as f32,
976        ry as f32,
977        phi as f32,
978        theta as f32,
979        delta as f32,
980    ))
981}
982
983#[allow(clippy::too_many_arguments)]
984fn flatten_arc(
985    from: Vec2,
986    rx: f32,
987    ry: f32,
988    rotation: f32,
989    large: bool,
990    sweep: bool,
991    to: Vec2,
992    out: &mut Vec<Vec2>,
993    fine: Fineness,
994) {
995    let Some((c, rx, ry, phi, theta, delta)) = arc_center(from, rx, ry, rotation, large, sweep, to)
996    else {
997        out.push(to);
998        return;
999    };
1000    let r = rx.max(ry);
1001    // The chord of one piece strays `r (1 - cos(step / 2))` from the arc.
1002    // For a radius a tolerance is next to nothing against, `1 - tol / r`
1003    // is 1 in an f32 and the step 0, which made a large arc one piece -
1004    // a line - where it goes round an ellipse 1e17 px across (backlog
1005    // FZ2): `acos(1 - x)` is `sqrt(2x)` there.
1006    let x = fine.tolerance / r;
1007    let step = if x < 1e-4 {
1008        2.0 * (2.0 * x).sqrt()
1009    } else {
1010        2.0 * (1.0 - x).clamp(-1.0, 1.0).acos()
1011    };
1012    let n = if step > 0.0 {
1013        ((delta.abs() / step).ceil() as usize).clamp(1, fine.pieces)
1014    } else {
1015        1
1016    };
1017    let (sin_phi, cos_phi) = phi.sin_cos();
1018    for i in 1..=n {
1019        let a = theta + delta * i as f32 / n as f32;
1020        let (sa, ca) = a.sin_cos();
1021        let x = rx * ca;
1022        let y = ry * sa;
1023        out.push(Vec2::new(
1024            c.x + cos_phi * x - sin_phi * y,
1025            c.y + sin_phi * x + cos_phi * y,
1026        ));
1027    }
1028    // The last point is the declared end, to the bit.
1029    if let Some(last) = out.last_mut() {
1030        *last = to;
1031    }
1032}
1033
1034/// Whether `p` is inside the flattened contours `pts` (as [`flatten`]
1035/// lays them out) by `rule`. A point on an edge counts as inside on one
1036/// side and outside on the other, so of two wedges sharing an edge
1037/// exactly one takes it.
1038pub fn in_path(p: Vec2, pts: &[Vec2], rule: FillRule) -> bool {
1039    let mut winding = 0i32;
1040    let mut crossings = 0u32;
1041    for contour in pts.split(|v| v.x.is_nan()) {
1042        let n = contour.len();
1043        if n < 3 {
1044            continue;
1045        }
1046        let mut j = n - 1;
1047        for i in 0..n {
1048            let (a, b) = (contour[i], contour[j]);
1049            if (a.y > p.y) != (b.y > p.y) {
1050                let x = a.x + (p.y - a.y) / (b.y - a.y) * (b.x - a.x);
1051                if p.x < x {
1052                    crossings += 1;
1053                    winding += if b.y > a.y { 1 } else { -1 };
1054                }
1055            }
1056            j = i;
1057        }
1058    }
1059    match rule {
1060        FillRule::NonZero => winding != 0,
1061        FillRule::EvenOdd => crossings % 2 == 1,
1062    }
1063}
1064
1065/// The bounding box of a flattened outline, ignoring contour breaks;
1066/// `None` for no points.
1067pub fn bounds(pts: &[Vec2]) -> Option<Rect> {
1068    let mut it = pts.iter().filter(|p| !p.x.is_nan());
1069    let first = *it.next()?;
1070    let (mut min, mut max) = (first, first);
1071    for p in it {
1072        min.x = min.x.min(p.x);
1073        min.y = min.y.min(p.y);
1074        max.x = max.x.max(p.x);
1075        max.y = max.y.max(p.y);
1076    }
1077    Some(Rect::new(min.x, min.y, max.x - min.x, max.y - min.y))
1078}
1079
1080/// FNV-1a over the ops' bits: what a mask is keyed on. The same ops are
1081/// the same hash in every binding, since the floats crossed as floats.
1082pub fn hash_ops(ops: &[PathOp]) -> u64 {
1083    const OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
1084    const PRIME: u64 = 0x0000_0100_0000_01b3;
1085    let mut h = OFFSET;
1086    let mut mix = |v: u32| {
1087        for byte in v.to_le_bytes() {
1088            h ^= u64::from(byte);
1089            h = h.wrapping_mul(PRIME);
1090        }
1091    };
1092    let mut scratch = Vec::with_capacity(8);
1093    for op in ops {
1094        scratch.clear();
1095        op.write(&mut scratch);
1096        for v in &scratch {
1097            mix(v.to_bits());
1098        }
1099    }
1100    h
1101}
1102
1103/// What one mask paints: the fill by its rule, bled half a pixel so two
1104/// fills sharing an edge meet without the background showing, or the
1105/// outline stroked `width` physical px wide — whole, or cut into the
1106/// marks of a dash pattern (backlog V2), its lengths physical too.
1107#[derive(Clone, Copy, Debug, PartialEq)]
1108pub enum MaskPaint {
1109    Fill(FillRule),
1110    Stroke(f32),
1111    Dashed(f32, DashCut),
1112}
1113
1114/// A dash pattern as the rasterizer takes it: the centre-line lengths of
1115/// a mark, a gap, a mark and a gap, and how far into them the stroke
1116/// starts. [`crate::line::Dash`] is what an app declares.
1117pub type DashCut = crate::line::Cut;
1118
1119/// Rasterizes `ops` (in logical px, relative to the node's box) into a
1120/// `w × h` alpha mask at `scale`, the node's box shifted by `bin`
1121/// quarter-pixels so a node at a fractional position lands on the pixel
1122/// grid as it would be drawn. `w * h` bytes, row after row.
1123///
1124/// An outline the rasterizer cannot draw is left blank (backlog FZ2): a
1125/// number that is not finite, an outline more than [`RASTER_SPAN`] px
1126/// wide and tall together at this scale (stroke included), one farther
1127/// than [`RASTER_REACH`] from the mask's origin, or a mask wider or taller
1128/// than [`MAX_MASK_SIDE`]. A path node never asks for one — its mask is
1129/// its outline's box, and at most `MAX_MASK_SIDE` a side.
1130pub fn rasterize(
1131    ops: &[PathOp],
1132    scale: f32,
1133    bin: (u8, u8),
1134    w: u32,
1135    h: u32,
1136    paint: MaskPaint,
1137) -> Vec<u8> {
1138    let off = Vec2::new(f32::from(bin.0) * 0.25, f32::from(bin.1) * 0.25);
1139    rasterize_at(ops, scale, off, w, h, paint)
1140}
1141
1142/// [`rasterize`] with the box's origin `off` physical px into the mask:
1143/// what a turning path's mask is drawn with, its pivot at the mask's
1144/// centre.
1145pub fn rasterize_at(
1146    ops: &[PathOp],
1147    scale: f32,
1148    off: Vec2,
1149    w: u32,
1150    h: u32,
1151    paint: MaskPaint,
1152) -> Vec<u8> {
1153    use swash::zeno::{Cap, Command, Fill, Join, Mask, PathBuilder, Point, Stroke};
1154    let len = (w as usize) * (h as usize);
1155    let mut buf = vec![0u8; len];
1156    if len == 0 || ops.is_empty() || !drawable(ops, scale, off, w, h, paint) {
1157        return buf;
1158    }
1159    // On zeno's own grid, a 256th of a pixel: two points are then one
1160    // point or a step apart. Its stroker turns a curve's tangent into a
1161    // normal by its length, and a control point 1e-41 px from the curve's
1162    // end made that normal - and the outline offset along it - vast
1163    // (backlog FZ2): an overflow in a debug build, a number past what its
1164    // conversion takes in a release one. Nothing drawn moves by more than
1165    // the 512th of a pixel the rasterizer resolves anyway.
1166    let snap = |v: f32| (v * 256.0).round() / 256.0;
1167    let place = |p: Vec2| Vec2::new(p.x * scale + off.x, p.y * scale + off.y);
1168    let at = |p: Vec2| {
1169        let q = place(p);
1170        Point::new(snap(q.x), snap(q.y))
1171    };
1172    let mut arc = Vec::new();
1173    let mut cmds: Vec<Command> = Vec::with_capacity(ops.len() + 1);
1174    let mut cur = Vec2::ZERO;
1175    let mut start = Vec2::ZERO;
1176    let mut open = false;
1177    for op in ops {
1178        match *op {
1179            PathOp::MoveTo(p) => {
1180                cmds.move_to(at(p));
1181                cur = p;
1182                start = p;
1183                open = true;
1184            }
1185            PathOp::Close => {
1186                if open {
1187                    cmds.close();
1188                }
1189                // A draw after a close starts where the subpath began,
1190                // as `flatten` has it.
1191                cur = start;
1192                open = false;
1193            }
1194            _ => {
1195                if !open {
1196                    cmds.move_to(at(cur));
1197                    start = cur;
1198                    open = true;
1199                }
1200                match *op {
1201                    PathOp::LineTo(p) => {
1202                        cmds.line_to(at(p));
1203                        cur = p;
1204                    }
1205                    PathOp::QuadTo(c, p) => {
1206                        cmds.quad_to(at(c), at(p));
1207                        cur = p;
1208                    }
1209                    PathOp::CubicTo(a, b, p) => {
1210                        cmds.curve_to(at(a), at(b), at(p));
1211                        cur = p;
1212                    }
1213                    PathOp::ArcTo {
1214                        rx,
1215                        ry,
1216                        rotation,
1217                        large,
1218                        sweep,
1219                        to,
1220                    } => {
1221                        // kui's own arc, cut at [`RASTER`]'s fineness, not
1222                        // zeno's: its arc is f32 throughout, and it scaled a
1223                        // radius of 1e-9 up to an ellipse, drew a radius of
1224                        // 1e25 round one `drawable` had not measured, and
1225                        // took a rotation past 1e38 to a NaN (backlog FZ2).
1226                        // This is the arc the hit outline and `drawable`
1227                        // are cut from, finer.
1228                        arc.clear();
1229                        flatten_arc(
1230                            place(cur),
1231                            rx * scale,
1232                            ry * scale,
1233                            rotation,
1234                            large,
1235                            sweep,
1236                            place(to),
1237                            &mut arc,
1238                            RASTER,
1239                        );
1240                        for p in &arc {
1241                            cmds.line_to(Point::new(snap(p.x), snap(p.y)));
1242                        }
1243                        cur = to;
1244                    }
1245                    PathOp::MoveTo(_) | PathOp::Close => unreachable!(),
1246                }
1247            }
1248        }
1249    }
1250    // What a stroke is drawn from: the outline flattened here, in physical
1251    // px at [`RASTER`]'s fineness, not zeno's curves. Its stroker offsets
1252    // a curve along its normals, and a curve that doubles back on itself
1253    // - a quad whose control point lies on the line past its end, `Q50 0
1254    // 8 0` from the origin - offset to control points past what its fixed
1255    // point holds (backlog FZ2). A polyline stroked with round joins and
1256    // caps is the same stroke, and a dash measures the same length along
1257    // it. The fill keeps the curves: zeno fills them without offsetting.
1258    let lines = {
1259        let mut physical: Vec<PathOp> = Vec::with_capacity(ops.len() + 1);
1260        // A path that draws before it moves starts at its origin, which
1261        // is `off` in the mask - not the mask's own origin, where
1262        // flattening starts one: an arc whose ends were 1e-41 apart was
1263        // flattened from a point a quarter-pixel off and went round an
1264        // ellipse 1e25 px tall (backlog FZ2).
1265        if !matches!(ops.first(), Some(PathOp::MoveTo(_))) {
1266            physical.push(PathOp::MoveTo(off));
1267        }
1268        physical.extend(ops.iter().map(|op| op.placed(scale, off)));
1269        let (mut pts, mut closes) = (Vec::new(), Vec::new());
1270        flatten_as(&physical, &mut pts, true, RASTER, Some(&mut closes));
1271        let mut lines: Vec<Command> = Vec::with_capacity(pts.len() + closes.len());
1272        let mut closes = closes.into_iter();
1273        let mut kept: Vec<Point> = Vec::new();
1274        for contour in pts.split(|p| p.x.is_nan()).filter(|c| !c.is_empty()) {
1275            let closed = closes.next() == Some(true);
1276            // A piece of no length is no piece: the stroker would join at
1277            // a direction it has to make up, and a contour that is one
1278            // point once they are gone - an arc whose ends are 1e-41 apart -
1279            // it normalizes to a NaN (backlog FZ2). Such a contour draws
1280            // nothing.
1281            kept.clear();
1282            for p in contour {
1283                let q = Point::new(snap(p.x), snap(p.y));
1284                if kept.last() != Some(&q) {
1285                    kept.push(q);
1286                }
1287            }
1288            if closed && kept.len() > 1 && kept.first() == kept.last() {
1289                kept.pop();
1290            }
1291            let Some((&first, rest)) = kept.split_first() else {
1292                continue;
1293            };
1294            if rest.is_empty() {
1295                continue;
1296            }
1297            lines.move_to(first);
1298            for &q in rest {
1299                lines.line_to(q);
1300            }
1301            // Closed as zeno closes it, one outline joined at the start:
1302            // two ends meeting there are two antialiased edges, and their
1303            // coverage adds.
1304            if closed {
1305                lines.close();
1306            }
1307        }
1308        lines
1309    };
1310    match paint {
1311        MaskPaint::Fill(rule) => {
1312            let fill = match rule {
1313                FillRule::NonZero => Fill::NonZero,
1314                FillRule::EvenOdd => Fill::EvenOdd,
1315            };
1316            Mask::new(&cmds[..])
1317                .style(fill)
1318                .size(w, h)
1319                .render_into(&mut buf, None);
1320            // A fill that covers nothing - a ring's sector at a sweep of
1321            // 0, out along a radius and back - has no edge to bleed: the
1322            // outline alone would paint it as a hairline.
1323            if buf.iter().all(|&a| a == 0) {
1324                return buf;
1325            }
1326            // The bleed: the outline a pixel wide, in the same mask by
1327            // max, so two fills sharing an edge overlap by the ramp.
1328            let mut edge = vec![0u8; len];
1329            let mut stroke = Stroke::new(1.0);
1330            stroke.join(Join::Round).cap(Cap::Round);
1331            Mask::new(&lines[..])
1332                .style(stroke)
1333                .size(w, h)
1334                .render_into(&mut edge, None);
1335            for (a, e) in buf.iter_mut().zip(edge) {
1336                *a = (*a).max(e);
1337            }
1338        }
1339        MaskPaint::Stroke(width) => {
1340            let mut stroke = Stroke::new(width.max(0.0));
1341            stroke.join(Join::Round).cap(Cap::Round);
1342            Mask::new(&lines[..])
1343                .style(stroke)
1344                .size(w, h)
1345                .render_into(&mut buf, None);
1346        }
1347        MaskPaint::Dashed(width, cut) => {
1348            let mut stroke = Stroke::new(width.max(0.0));
1349            stroke.join(Join::Round).cap(Cap::Round);
1350            // A mark and its gap under a pixel are not a pattern any
1351            // more, as on a line: the stroke whole.
1352            if !cut.finer_than(1.0) {
1353                stroke.dash(&cut.lens, cut.offset);
1354            }
1355            Mask::new(&lines[..])
1356                .style(stroke)
1357                .size(w, h)
1358                .render_into(&mut buf, None);
1359        }
1360    }
1361    buf
1362}
1363
1364/// The most an outline may span, wide plus tall, in physical px with its
1365/// stroke, for the rasterizer to draw it (backlog FZ2). zeno walks a line
1366/// in 24.8 fixed point and sums its run and rise, each times a pixel's
1367/// 256, in an `i32`: past some 16 416 px together that overflows — a
1368/// panic in a debug build, a wrong mask in a release one. A mask of
1369/// [`MAX_MASK_SIDE`] on both sides is exactly this, and every line zeno
1370/// cuts an outline into lies inside the outline's box.
1371pub const RASTER_SPAN: f32 = 16384.0;
1372
1373/// How far from the mask's origin, in physical px, an outline may lie
1374/// for the rasterizer to draw it (backlog FZ2). zeno converts a
1375/// coordinate to fixed point with an unchecked cast, undefined behaviour
1376/// past ±2^23 px, and sums eight of a curve's where it splits one, which
1377/// overflows past ±2^20; a curve's control points can lie a few times its
1378/// span outside its box, so the box is held well inside both.
1379pub const RASTER_REACH: f32 = 262_144.0;
1380
1381/// Whether zeno can draw `ops` at `scale` from `off` into a `w × h`
1382/// mask with `paint`: every number finite, and the outline within
1383/// [`RASTER_SPAN`] and [`RASTER_REACH`]. What the box is taken from is
1384/// the hit outline's flattening, the same points a path node's mask is
1385/// sized by.
1386fn drawable(ops: &[PathOp], scale: f32, off: Vec2, w: u32, h: u32, paint: MaskPaint) -> bool {
1387    let width = match paint {
1388        // The fill's bleed is a one-pixel stroke.
1389        MaskPaint::Fill(_) => 1.0,
1390        MaskPaint::Stroke(w) => w,
1391        MaskPaint::Dashed(w, cut) => {
1392            if !cut.offset.is_finite() || !cut.lens.iter().all(|l| l.is_finite()) {
1393                return false;
1394            }
1395            w
1396        }
1397    };
1398    if !(scale.is_finite() && off.x.is_finite() && off.y.is_finite() && width.is_finite())
1399        || w > MAX_MASK_SIDE
1400        || h > MAX_MASK_SIDE
1401        || !ops.iter().all(PathOp::is_finite)
1402    {
1403        return false;
1404    }
1405    let mut pts = Vec::new();
1406    flatten(ops, &mut pts);
1407    let Some(b) = bounds(&pts) else {
1408        // A move alone: nothing to draw, and nothing zeno trips on.
1409        return true;
1410    };
1411    let (x0, y0) = (b.x * scale + off.x, b.y * scale + off.y);
1412    let (x1, y1) = (x0 + b.w * scale, y0 + b.h * scale);
1413    let reach = [x0, y0, x1, y1].iter().all(|v| v.abs() <= RASTER_REACH);
1414    reach && (x1 - x0).abs() + (y1 - y0).abs() + 2.0 * width.abs() <= RASTER_SPAN
1415}
1416
1417/// The texels a mask may take of the atlas before it goes to a texture
1418/// of its own: a quarter of the biggest page, since four of them would
1419/// empty it every frame.
1420pub const MAX_ATLAS_MASK_TEXELS: u64 = 2048 * 2048;
1421
1422/// The widest or tallest a mask may be and still be drawn from a texture
1423/// of its own: a texture every device kui runs on can hold — `kui-wgpu`
1424/// opens its device with wgpu's default limits, whose
1425/// `max_texture_dimension_2d` is this number whatever the adapter could
1426/// do, so the core's limit and the renderer's are one. Past it the node
1427/// draws nothing, with `path-too-large`. A host that draws the list
1428/// itself on a device that holds less refuses the texture on its side.
1429pub const MAX_MASK_SIDE: u32 = 8192;
1430
1431/// A mask drawn from a texture of its own rather than the atlas (ADR
1432/// 0040, decisions 7 and 8): the handle the backend caches it under, and
1433/// the pixels it uploads from.
1434pub struct PathTexture {
1435    pub id: crate::resources::ImageId,
1436    pub w: u32,
1437    pub h: u32,
1438    pub rgba: std::sync::Arc<Vec<u8>>,
1439    last_used: u64,
1440}
1441
1442/// The texture-backed masks of one core, by the mask key the atlas would
1443/// have held them under. One a frame does not draw is dropped at the
1444/// next frame's start, its handle handed to the display list for the
1445/// backend to free; an animating path, whose key is new each frame, thus
1446/// uploads one texture a frame and frees one. The ones a core still
1447/// holds when it goes go the way a removed image does: to the session,
1448/// for the next display list any of its windows builds (RG112).
1449pub struct PathTextures {
1450    by_key: rustc_hash::FxHashMap<u64, PathTexture>,
1451    session: crate::session::Session,
1452}
1453
1454impl Drop for PathTextures {
1455    fn drop(&mut self) {
1456        for t in self.by_key.values() {
1457            crate::resources::unmint_image(t.id);
1458        }
1459        // Held only if the core is dropped from inside a borrow of its
1460        // session, which nothing does; then the handles are forgotten
1461        // and the backend keeps the textures, as before.
1462        if let Some(mut sess) = self.session.try_state() {
1463            sess.dropped
1464                .images
1465                .extend(self.by_key.values().map(|t| t.id));
1466        }
1467    }
1468}
1469
1470impl PathTextures {
1471    pub(crate) fn new(session: crate::session::Session) -> Self {
1472        Self {
1473            by_key: Default::default(),
1474            session,
1475        }
1476    }
1477
1478    /// The pixels of the texture minted as `id`, for a host that draws
1479    /// the list itself and asks by handle.
1480    pub(crate) fn pixels(
1481        &self,
1482        id: crate::resources::ImageId,
1483    ) -> Option<(u32, u32, std::sync::Arc<Vec<u8>>)> {
1484        self.by_key
1485            .values()
1486            .find(|t| t.id == id)
1487            .map(|t| (t.w, t.h, t.rgba.clone()))
1488    }
1489
1490    /// The texture for `key`, made from `coverage` (`w * h` alpha bytes)
1491    /// on a miss.
1492    pub(crate) fn get_or_make(
1493        &mut self,
1494        key: u64,
1495        w: u32,
1496        h: u32,
1497        frame: u64,
1498        session: crate::resources::SessionId,
1499        coverage: impl FnOnce() -> Vec<u8>,
1500    ) -> &PathTexture {
1501        if let Some(tex) = self.by_key.get(&key) {
1502            debug_assert!(tex.w == w && tex.h == h, "a mask key names one size");
1503        }
1504        let tex = self.by_key.entry(key).or_insert_with(|| {
1505            let mask = coverage();
1506            let mut rgba = Vec::with_capacity(mask.len() * 4);
1507            for &a in &mask {
1508                rgba.extend_from_slice(&[255, 255, 255, a]);
1509            }
1510            PathTexture {
1511                id: crate::resources::mint_image(session),
1512                w,
1513                h,
1514                rgba: std::sync::Arc::new(rgba),
1515                last_used: frame,
1516            }
1517        });
1518        tex.last_used = frame;
1519        tex
1520    }
1521
1522    /// Drops every texture no frame since `frame - 1` drew, and returns
1523    /// their handles for the display list's `dropped_textures`.
1524    pub(crate) fn sweep(&mut self, frame: u64) -> Vec<crate::resources::ImageId> {
1525        let mut gone = Vec::new();
1526        self.by_key.retain(|_, t| {
1527            if t.last_used + 1 < frame {
1528                crate::resources::unmint_image(t.id);
1529                gone.push(t.id);
1530                false
1531            } else {
1532                true
1533            }
1534        });
1535        gone
1536    }
1537
1538    /// How many masks are texture-backed right now.
1539    pub fn len(&self) -> usize {
1540        self.by_key.len()
1541    }
1542
1543    pub fn is_empty(&self) -> bool {
1544        self.by_key.is_empty()
1545    }
1546}
1547
1548/// The mask key the atlas and the textures hold a path's mask under: its
1549/// ops' hash with the scale, the quarter-pixel bin and the paint mixed in.
1550/// The bin a turning path's masks are keyed under: none of the sixteen,
1551/// since its mask is centred on its pivot and not binned.
1552pub(crate) const TURNED_BIN: (u8, u8) = (0xff, 0xff);
1553
1554pub(crate) fn mask_key(hash: u64, scale: f32, bin: (u8, u8), paint: MaskPaint) -> u64 {
1555    const PRIME: u64 = 0x0000_0100_0000_01b3;
1556    let mut h = hash;
1557    let mut mix = |v: u64| {
1558        h ^= v;
1559        h = h.wrapping_mul(PRIME);
1560    };
1561    mix(u64::from(scale.to_bits()));
1562    mix(u64::from(bin.0) | (u64::from(bin.1) << 8));
1563    match paint {
1564        MaskPaint::Fill(rule) => mix(0x1000 | rule.index() as u64),
1565        MaskPaint::Stroke(w) => mix(0x2000_0000_0000 | u64::from(w.to_bits())),
1566        MaskPaint::Dashed(w, cut) => {
1567            mix(0x3000_0000_0000 | u64::from(w.to_bits()));
1568            mix(cut.hash());
1569        }
1570    }
1571    h
1572}
1573
1574/// How many frames apart two changes of one key's ops may be and still
1575/// be an animation: a shape driven at a quarter of
1576/// the frame rate, or one whose changes have a frame between them that
1577/// something else asked for, moves as surely as one that changes every
1578/// frame, and each of its shapes would be a slot the atlas never reuses.
1579pub const ANIMATING_WINDOW: u64 = 8;
1580
1581/// How many frames an animating key's shape stays the same before it is
1582/// a still shape again, and its mask the atlas's: a second at 120 Hz. A
1583/// spinner that pauses for a frame or ten stays out; one that stopped
1584/// stops costing a texture and a draw of its own.
1585pub const SETTLED_AFTER: u64 = 120;
1586
1587/// What the core remembers of one `path` key between frames, to tell a
1588/// new shape from a moving one.
1589#[derive(Clone, Copy, Debug)]
1590pub(crate) struct Motion {
1591    /// The ops' hash the key last declared, and the frame it did.
1592    pub hash: u64,
1593    pub seen: u64,
1594    /// The frame the hash last differed from the one before; 0 for never.
1595    pub changed: u64,
1596    /// Two changes within [`ANIMATING_WINDOW`], until the shape has held
1597    /// for [`SETTLED_AFTER`].
1598    pub animating: bool,
1599}
1600
1601/// A `d` string's ops as the core last parsed them under one key, so a
1602/// binding that hands the string over every frame pays the parse once
1603/// per string rather than once per frame.
1604pub(crate) struct Parsed {
1605    /// `key::hash_bulk` of the string, and its length.
1606    pub hash: u64,
1607    pub len: usize,
1608    pub seen: u64,
1609    pub ops: Vec<PathOp>,
1610}
1611
1612/// One path's run in the frame's op list. The ops are stored relative to
1613/// the node's box, so a node that eases or slides carries them along.
1614#[derive(Clone, Copy, Debug, PartialEq)]
1615pub(crate) struct Run {
1616    pub first: u32,
1617    pub len: u32,
1618    pub rule: FillRule,
1619    /// The stroke width in logical px; 0 for no stroke.
1620    pub stroke_w: f32,
1621    /// What cuts the stroke into marks; None for a solid one.
1622    pub dash: Option<crate::line::Cut>,
1623    /// [`hash_ops`] of the ops as stored.
1624    pub hash: u64,
1625    /// The turn in radians when the path declared one: its box is then
1626    /// the square about its pivot, and the angle is the quad's.
1627    pub angle: Option<f32>,
1628    /// The key's ops changed twice within [`ANIMATING_WINDOW`] frames:
1629    /// the mask goes to a texture of its own until the shape has held
1630    /// for [`SETTLED_AFTER`].
1631    pub animating: bool,
1632}
1633
1634/// The frame's paths, and the previous frame's while an `exit` needs it.
1635#[derive(Default)]
1636pub struct PathStore {
1637    runs: Kept<Run>,
1638    ops: Kept<PathOp>,
1639    scratch: Vec<Vec2>,
1640}
1641
1642impl PathStore {
1643    /// Starts a frame. `keep_prev` retains the list just finished so a
1644    /// departing path's ghost can copy its ops out of it.
1645    pub(crate) fn begin_frame(&mut self, keep_prev: bool) {
1646        self.runs.begin(keep_prev);
1647        self.ops.begin(keep_prev);
1648    }
1649
1650    /// Adds a path (ops in parent-box coordinates): boxes the outline, and
1651    /// stores the ops relative to the box. Returns the id and the box, or
1652    /// None for a path that draws nothing.
1653    pub(crate) fn push(
1654        &mut self,
1655        ops: &[PathOp],
1656        rule: FillRule,
1657        stroke_w: f32,
1658        dash: Option<crate::line::Cut>,
1659        turn: Option<Turn>,
1660    ) -> Option<(PathId, Rect)> {
1661        self.scratch.clear();
1662        flatten(ops, &mut self.scratch);
1663        let b = bounds(&self.scratch)?;
1664        // Two logical px past the outline — one for the bleed, one for
1665        // the edge ramp — and half the stroke's width further for a
1666        // stroke, so no mask is cut by its own edge.
1667        let pad = 2.0 + stroke_w * 0.5;
1668        let rect = match turn {
1669            None => Rect::new(b.x - pad, b.y - pad, b.w + 2.0 * pad, b.h + 2.0 * pad),
1670            // The square the turn sweeps: centred on the pivot, out to
1671            // the farthest point of the outline, so it is the same box —
1672            // and the same ops, hash and mask — at every angle.
1673            Some(turn) => {
1674                let c = turn
1675                    .pivot
1676                    .unwrap_or(Vec2::new(b.x + b.w * 0.5, b.y + b.h * 0.5));
1677                let far = self
1678                    .scratch
1679                    .iter()
1680                    .filter(|p| !p.x.is_nan())
1681                    .map(|p| (p.x - c.x).hypot(p.y - c.y))
1682                    .fold(0.0f32, f32::max);
1683                let half = far + pad;
1684                Rect::new(c.x - half, c.y - half, 2.0 * half, 2.0 * half)
1685            }
1686        };
1687        let origin = Vec2::new(rect.x, rect.y);
1688        let first = self.ops.len();
1689        let shift = Vec2::new(-origin.x, -origin.y);
1690        self.ops.extend(ops.iter().map(|op| op.shifted(shift)));
1691        let hash = hash_ops(&self.ops[first..]);
1692        let id = PathId(self.runs.len() as u32);
1693        self.runs.push(Run {
1694            first: first as u32,
1695            len: (self.ops.len() - first) as u32,
1696            rule,
1697            stroke_w,
1698            dash,
1699            hash,
1700            angle: turn.map(Turn::radians),
1701            animating: false,
1702        });
1703        Some((id, rect))
1704    }
1705
1706    pub(crate) fn set_animating(&mut self, id: PathId) {
1707        if let Some(run) = self.runs.get_mut(id.0 as usize) {
1708            run.animating = true;
1709        }
1710    }
1711
1712    /// This frame's run and its ops, relative to the node.
1713    pub(crate) fn run(&self, id: PathId) -> (Run, &[PathOp]) {
1714        let run = self.runs[id.0 as usize];
1715        (
1716            run,
1717            &self.ops[run.first as usize..(run.first + run.len) as usize],
1718        )
1719    }
1720
1721    /// The same, read from the previous frame's list (a departing path's
1722    /// id indexes that list, not this frame's).
1723    pub(crate) fn prev_run(&self, id: PathId) -> Option<(Run, &[PathOp])> {
1724        let run = *self.runs.prev().get(id.0 as usize)?;
1725        Some((
1726            run,
1727            &self.ops.prev()[run.first as usize..(run.first + run.len) as usize],
1728        ))
1729    }
1730
1731    pub fn len(&self) -> usize {
1732        self.runs.len()
1733    }
1734
1735    pub fn is_empty(&self) -> bool {
1736        self.runs.is_empty()
1737    }
1738}
1739
1740#[cfg(test)]
1741mod tests {
1742    use super::*;
1743
1744    fn pts(ops: &[PathOp]) -> Vec<Vec2> {
1745        let mut out = Vec::new();
1746        flatten(ops, &mut out);
1747        out
1748    }
1749
1750    /// What zeno cannot draw is left blank, not handed to it (backlog
1751    /// FZ2, from the first fuzz round): a line wider than its fixed point
1752    /// spans overflowed it, a control point at 1.4e18 overflowed its curve
1753    /// split, and a NaN or a point past ±2^23 px is undefined behaviour in
1754    /// its conversion. An outline it can draw still draws, and one the
1755    /// size of the largest mask a node makes is one it can.
1756    #[test]
1757    fn an_outline_past_the_rasterizer_is_left_blank() {
1758        let blank = |ops: &[PathOp], paint| {
1759            rasterize(ops, 1.0, (0, 0), 48, 32, paint)
1760                .iter()
1761                .all(|&a| a == 0)
1762        };
1763        let fill = MaskPaint::Fill(FillRule::NonZero);
1764        let long = Path::parse("M60 60 L60610 0 Z").unwrap();
1765        assert!(blank(long.ops(), fill));
1766        let far = [
1767            PathOp::MoveTo(Vec2::new(10.0, 0.0)),
1768            PathOp::CubicTo(
1769                Vec2::new(50.0, 0.0),
1770                Vec2::new(1.0, 0.0),
1771                Vec2::new(20.0, 1.44e18),
1772            ),
1773        ];
1774        assert!(blank(&far, fill));
1775        let away = Path::parse("M1e7 1e7 l10 0 l0 10 z").unwrap();
1776        assert!(blank(away.ops(), MaskPaint::Stroke(2.0)));
1777        let nan = [
1778            PathOp::MoveTo(Vec2::new(2.0, 2.0)),
1779            PathOp::LineTo(Vec2::new(f32::NAN, 20.0)),
1780            PathOp::LineTo(Vec2::new(30.0, 20.0)),
1781        ];
1782        assert!(blank(&nan, fill));
1783        // A stroke as wide as the span is past it too.
1784        let tri = Path::parse("M2 2 L40 2 L20 30 Z").unwrap();
1785        assert!(blank(tri.ops(), MaskPaint::Stroke(RASTER_SPAN)));
1786        assert!(!blank(tri.ops(), fill));
1787        assert!(!blank(tri.ops(), MaskPaint::Stroke(2.0)));
1788        // The largest mask a node makes, corner to corner, still draws.
1789        let side = (MAX_MASK_SIDE - 2) as f32;
1790        let diag = [
1791            PathOp::MoveTo(Vec2::new(1.0, 1.0)),
1792            PathOp::LineTo(Vec2::new(side, side)),
1793            PathOp::LineTo(Vec2::new(1.0, side)),
1794            PathOp::Close,
1795        ];
1796        assert!(drawable(
1797            &diag,
1798            1.0,
1799            Vec2::ZERO,
1800            MAX_MASK_SIDE,
1801            MAX_MASK_SIDE,
1802            fill
1803        ));
1804    }
1805
1806    /// An arc's rotation is taken within one turn before it is drawn
1807    /// (backlog FZ2): one of -1.7e38 degrees became an infinite angle in
1808    /// zeno and a NaN in every point after. It draws what the same
1809    /// rotation within a turn draws; and a closed contour's stroke is one
1810    /// outline, so its start is no darker than the rest of its edge.
1811    #[test]
1812    fn an_arc_turned_past_any_turn_draws_as_within_one() {
1813        let arc = |rotation: f32| {
1814            let ops = [
1815                PathOp::MoveTo(Vec2::new(4.0, 20.0)),
1816                PathOp::ArcTo {
1817                    rx: 20.0,
1818                    ry: 8.0,
1819                    rotation,
1820                    large: true,
1821                    sweep: true,
1822                    to: Vec2::new(40.0, 24.0),
1823                },
1824                PathOp::Close,
1825            ];
1826            let fill = rasterize(
1827                &ops,
1828                1.0,
1829                (0, 0),
1830                64,
1831                48,
1832                MaskPaint::Fill(FillRule::NonZero),
1833            );
1834            let stroke = rasterize(&ops, 1.0, (0, 0), 64, 48, MaskPaint::Stroke(1.5));
1835            (fill, stroke)
1836        };
1837        let far = -1.7e38f32;
1838        assert_eq!(arc(far), arc(far.rem_euclid(360.0)));
1839        assert!(arc(far).0.iter().any(|&a| a > 0));
1840        // A large arc round an ellipse 1e17 px across is flattened round
1841        // it, not to the one piece an f32's `1 - tol / r` made it - so it
1842        // is too wide to draw, and is left blank rather than handed over.
1843        let huge = [
1844            PathOp::MoveTo(Vec2::new(20.0, 80.0)),
1845            PathOp::ArcTo {
1846                rx: 3.6e17,
1847                ry: 5.0,
1848                rotation: 30.0,
1849                large: true,
1850                sweep: false,
1851                to: Vec2::new(20.0, 20.0),
1852            },
1853        ];
1854        let mut pts = Vec::new();
1855        flatten(&huge, &mut pts);
1856        assert!(pts.len() > 64, "{} points", pts.len());
1857        let m = rasterize(
1858            &huge,
1859            1.0,
1860            (0, 0),
1861            64,
1862            96,
1863            MaskPaint::Fill(FillRule::NonZero),
1864        );
1865        assert!(m.iter().all(|&a| a == 0));
1866        // The closed square's corner at its start is the corner opposite
1867        // it, to the coverage.
1868        let square = Path::parse("M10 10 L30 10 L30 30 L10 30 Z").unwrap();
1869        let m = rasterize(square.ops(), 1.0, (0, 0), 40, 40, MaskPaint::Stroke(2.0));
1870        // The pixels just inside each corner, and the ones just outside,
1871        // to a rasterizer's rounding: two ends meeting there added up to
1872        // half again.
1873        let near = |a: u8, b: u8| a.abs_diff(b) <= 4;
1874        assert!(near(m[10 * 40 + 10], m[29 * 40 + 29]));
1875        assert!(
1876            near(m[9 * 40 + 9], m[30 * 40 + 30]),
1877            "{} {}",
1878            m[9 * 40 + 9],
1879            m[30 * 40 + 30]
1880        );
1881    }
1882
1883    /// A path that draws before it moves starts at its origin, in the
1884    /// stroke's flattening as in the fill's (backlog FZ2): the stroke's
1885    /// started at the mask's own origin, a quarter-pixel off at this bin,
1886    /// and an arc whose ends were 1e-41 apart went round an ellipse 1e25
1887    /// px tall from there.
1888    #[test]
1889    fn a_path_that_draws_before_it_moves_starts_at_its_origin() {
1890        let stroke = |d: &str| {
1891            let p = Path::parse(d).unwrap();
1892            rasterize(p.ops(), 1.0, (1, 3), 40, 40, MaskPaint::Stroke(2.0))
1893        };
1894        assert_eq!(stroke("L20 10 L30 30"), stroke("M0 0 L20 10 L30 30"));
1895        let arc = [
1896            PathOp::ArcTo {
1897                rx: 16.0,
1898                ry: 1.0e25,
1899                rotation: 0.0,
1900                large: true,
1901                sweep: true,
1902                to: Vec2::new(5.7e-41, 0.0),
1903            },
1904            PathOp::Close,
1905        ];
1906        let m = rasterize(&arc, 1.0, (1, 3), 48, 32, MaskPaint::Stroke(1.5));
1907        assert!(m.iter().all(|&a| a == 0));
1908    }
1909
1910    #[test]
1911    fn parses_absolute_and_relative_commands_alike() {
1912        let a = Path::parse("M10 10 L20 10 L20 20 Z").unwrap();
1913        let b = Path::parse("m10,10 l10 0 l0 10 z").unwrap();
1914        assert_eq!(a.ops(), b.ops());
1915        assert_eq!(a.ops().len(), 4);
1916        // Implicit line-tos after a move, H and V, numbers run together.
1917        let c = Path::parse("M10 10 20 10V20z").unwrap();
1918        assert_eq!(a.ops(), c.ops());
1919        let d = Path::parse("M.5.5-1-1").unwrap();
1920        assert_eq!(
1921            d.ops(),
1922            &[
1923                PathOp::MoveTo(Vec2::new(0.5, 0.5)),
1924                PathOp::LineTo(Vec2::new(-1.0, -1.0))
1925            ]
1926        );
1927    }
1928
1929    #[test]
1930    fn smooth_curves_reflect_the_last_control_point() {
1931        let p = Path::parse("M0 0 C 10 0 20 10 20 20 S 30 40 40 40").unwrap();
1932        match p.ops()[2] {
1933            PathOp::CubicTo(c1, c2, to) => {
1934                assert_eq!(c1, Vec2::new(20.0, 30.0));
1935                assert_eq!(c2, Vec2::new(30.0, 40.0));
1936                assert_eq!(to, Vec2::new(40.0, 40.0));
1937            }
1938            op => panic!("{op:?}"),
1939        }
1940        let q = Path::parse("M0 0 Q 10 10 20 0 T 40 0").unwrap();
1941        match q.ops()[2] {
1942            PathOp::QuadTo(c, to) => {
1943                assert_eq!(c, Vec2::new(30.0, -10.0));
1944                assert_eq!(to, Vec2::new(40.0, 0.0));
1945            }
1946            op => panic!("{op:?}"),
1947        }
1948    }
1949
1950    #[test]
1951    fn a_malformed_string_names_its_byte() {
1952        let e = Path::parse("M10 10 L20").unwrap_err();
1953        assert_eq!(e.what, "a number");
1954        assert_eq!(e.at, 10);
1955        let e = Path::parse("10 10").unwrap_err();
1956        assert_eq!(e.what, "a command letter");
1957        let e = Path::parse("M0 0 X").unwrap_err();
1958        assert_eq!(e.what, "one of M L H V C S Q T A Z");
1959        let e = Path::parse("M0 0 A 5 5 0 2 0 10 10").unwrap_err();
1960        assert_eq!(e.what, "an arc flag (0 or 1)");
1961    }
1962
1963    #[test]
1964    fn the_wire_form_round_trips() {
1965        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();
1966        let f = p.to_floats();
1967        assert_eq!(f.len(), 3 + 3 + 5 + 7 + 8 + 1);
1968        assert_eq!(Path::from_floats(&f).unwrap().ops(), p.ops());
1969        assert!(Path::from_floats(&[9.0]).is_err());
1970        assert!(Path::from_floats(&[OP_LINE, 1.0]).is_err());
1971    }
1972
1973    #[test]
1974    fn a_square_flattens_to_its_corners_and_hits_inside() {
1975        let p = Path::parse("M0 0 H10 V10 H0 Z").unwrap();
1976        let v = pts(p.ops());
1977        assert_eq!(v.len(), 5);
1978        assert!(v[4].x.is_nan());
1979        assert!(in_path(Vec2::new(5.0, 5.0), &v, FillRule::NonZero));
1980        assert!(in_path(Vec2::new(5.0, 5.0), &v, FillRule::EvenOdd));
1981        assert!(!in_path(Vec2::new(15.0, 5.0), &v, FillRule::NonZero));
1982        assert_eq!(bounds(&v), Some(Rect::new(0.0, 0.0, 10.0, 10.0)));
1983    }
1984
1985    #[test]
1986    fn a_ring_is_a_ring_by_either_rule_when_its_contours_oppose() {
1987        // Outer clockwise, inner counter-clockwise (y down).
1988        let p = Path::parse("M0 0 H20 V20 H0 Z M5 5 V15 H15 V5 Z").unwrap();
1989        let v = pts(p.ops());
1990        let hole = Vec2::new(10.0, 10.0);
1991        let band = Vec2::new(2.0, 10.0);
1992        assert!(!in_path(hole, &v, FillRule::NonZero));
1993        assert!(!in_path(hole, &v, FillRule::EvenOdd));
1994        assert!(in_path(band, &v, FillRule::NonZero));
1995        assert!(in_path(band, &v, FillRule::EvenOdd));
1996        // Both the same way: nonzero fills the hole, even-odd does not.
1997        let same = Path::parse("M0 0 H20 V20 H0 Z M5 5 H15 V15 H5 Z").unwrap();
1998        let v = pts(same.ops());
1999        assert!(in_path(hole, &v, FillRule::NonZero));
2000        assert!(!in_path(hole, &v, FillRule::EvenOdd));
2001    }
2002
2003    #[test]
2004    fn an_arc_flattens_onto_its_circle() {
2005        // A half circle of radius 10 from (0, 0) to (20, 0), bowing down.
2006        let p = Path::parse("M0 0 A10 10 0 0 1 20 0").unwrap();
2007        let v = pts(p.ops());
2008        assert!(v.len() >= 9, "{}", v.len());
2009        // Sweep 1 is the positive-angle direction, clockwise on screen
2010        // with y down: from the left end over the top to the right end.
2011        for q in v.iter().filter(|q| !q.x.is_nan()) {
2012            let r = ((q.x - 10.0).powi(2) + q.y.powi(2)).sqrt();
2013            assert!((r - 10.0).abs() < 0.3, "{q:?} is {r} from the centre");
2014            assert!(q.y <= 0.01, "{q:?} bows the wrong way");
2015        }
2016        assert_eq!(v[v.len() - 2], Vec2::new(20.0, 0.0));
2017        // The sweep flag picks the other half.
2018        let down = Path::parse("M0 0 A10 10 0 0 0 20 0").unwrap();
2019        assert!(
2020            pts(down.ops())
2021                .iter()
2022                .filter(|q| !q.x.is_nan())
2023                .all(|q| q.y >= -0.01)
2024        );
2025    }
2026
2027    #[test]
2028    fn a_sector_is_a_wedge_and_a_full_turn_is_a_disc() {
2029        let w = Path::sector(50.0, 50.0, 40.0, 0.0, 0.0, 0.25);
2030        let v = pts(w.ops());
2031        assert!(in_path(Vec2::new(70.0, 70.0), &v, FillRule::NonZero));
2032        assert!(!in_path(Vec2::new(30.0, 30.0), &v, FillRule::NonZero));
2033        let d = Path::sector(50.0, 50.0, 40.0, 20.0, 0.0, 1.0);
2034        let v = pts(d.ops());
2035        assert!(in_path(Vec2::new(50.0, 20.0), &v, FillRule::NonZero));
2036        assert!(!in_path(Vec2::new(50.0, 50.0), &v, FillRule::NonZero));
2037    }
2038
2039    #[test]
2040    fn a_draw_after_a_close_starts_where_the_subpath_began() {
2041        // The second triangle is (10,10) (10,40) (30,40): the hit outline
2042        // and the mask agree on it.
2043        let p = Path::parse("M10 10 H30 V30 Z L10 40 L30 40 Z").unwrap();
2044        let v = pts(p.ops());
2045        let m = rasterize(
2046            p.ops(),
2047            1.0,
2048            (0, 0),
2049            48,
2050            48,
2051            MaskPaint::Fill(FillRule::NonZero),
2052        );
2053        for (x, y) in [(12usize, 25usize), (28, 32), (28, 20), (14, 36)] {
2054            let hit = in_path(
2055                Vec2::new(x as f32 + 0.5, y as f32 + 0.5),
2056                &v,
2057                FillRule::NonZero,
2058            );
2059            assert_eq!(m[y * 48 + x] == 255, hit, "({x}, {y})");
2060        }
2061        assert_eq!(m[25 * 48 + 12], 255);
2062        assert_eq!(m[32 * 48 + 28], 0);
2063    }
2064
2065    #[test]
2066    fn a_strokes_pieces_close_only_what_its_z_closed() {
2067        let open = Path::parse("M0 0 L10 0 L10 10").unwrap();
2068        let mut v = Vec::new();
2069        flatten_stroke(open.ops(), &mut v);
2070        assert_eq!(v.len(), 4);
2071        assert_eq!(v[2], Vec2::new(10.0, 10.0));
2072        assert!(v[3].x.is_nan());
2073        let closed = Path::parse("M0 0 L10 0 L10 10 Z").unwrap();
2074        v.clear();
2075        flatten_stroke(closed.ops(), &mut v);
2076        assert_eq!(v.len(), 5);
2077        assert_eq!(v[3], Vec2::ZERO);
2078        assert!(v[4].x.is_nan());
2079    }
2080
2081    #[test]
2082    fn the_hash_follows_the_ops_and_nothing_else() {
2083        let a = Path::parse("M0 0 L10 0 L10 10 Z").unwrap();
2084        let b = Path::parse("M0 0 L10 0 L10 10 Z")
2085            .unwrap()
2086            .fill_rule(FillRule::EvenOdd);
2087        let c = Path::parse("M0 0 L10 0 L10 11 Z").unwrap();
2088        assert_eq!(hash_ops(a.ops()), hash_ops(b.ops()));
2089        assert_ne!(hash_ops(a.ops()), hash_ops(c.ops()));
2090    }
2091
2092    #[test]
2093    fn a_filled_square_is_opaque_inside_and_bleeds_past_its_edge() {
2094        let p = Path::parse("M2 2 H12 V12 H2 Z").unwrap();
2095        let m = rasterize(
2096            p.ops(),
2097            1.0,
2098            (0, 0),
2099            16,
2100            16,
2101            MaskPaint::Fill(FillRule::NonZero),
2102        );
2103        let at = |x: usize, y: usize| m[y * 16 + x];
2104        assert_eq!(at(7, 7), 255);
2105        assert_eq!(at(0, 0), 0);
2106        assert_eq!(at(14, 7), 0);
2107        // The edge at x = 2 lands between pixels 1 and 2: pixel 1 is the
2108        // bleed's half, pixel 2 solid.
2109        assert!(at(1, 7) > 100 && at(1, 7) < 200, "{}", at(1, 7));
2110        assert_eq!(at(2, 7), 255);
2111        // Two squares sharing the edge x = 12 composite to full coverage
2112        // along it: the right one's left bleed over the left one's own.
2113        let q = Path::parse("M12 2 H22 V12 H12 Z").unwrap();
2114        let n = rasterize(
2115            q.ops(),
2116            1.0,
2117            (0, 0),
2118            24,
2119            16,
2120            MaskPaint::Fill(FillRule::NonZero),
2121        );
2122        let (a, b) = (
2123            f32::from(at(12, 7)) / 255.0,
2124            f32::from(n[7 * 24 + 12]) / 255.0,
2125        );
2126        assert!(a + b * (1.0 - a) > 0.99, "{a} over {b}");
2127        let (a, b) = (
2128            f32::from(at(11, 7)) / 255.0,
2129            f32::from(n[7 * 24 + 11]) / 255.0,
2130        );
2131        assert!(a + b * (1.0 - a) > 0.99, "{a} over {b}");
2132    }
2133
2134    /// A dashed stroke's mask is the marks a dashed line would draw: the
2135    /// lengths seen are the ones declared, a short mark is a dot, and the
2136    /// offset moves them towards the start.
2137    #[test]
2138    fn a_dashed_stroke_is_marks_and_gaps() {
2139        let p = Path::parse("M4 8 H44").unwrap();
2140        let row = |dash: crate::line::Dash| {
2141            let paint = MaskPaint::Dashed(2.0, dash.cut(2.0).unwrap());
2142            let m = rasterize(p.ops(), 1.0, (0, 0), 48, 16, paint);
2143            // The row of pixels under the centre line, on or off.
2144            (0..48).map(|x| m[8 * 48 + x] > 127).collect::<Vec<_>>()
2145        };
2146        let on = |r: &[bool], x: std::ops::Range<usize>| r[x].iter().all(|&b| b);
2147        let off = |r: &[bool], x: std::ops::Range<usize>| r[x].iter().all(|&b| !b);
2148        // 6 on, 4 off from a cap before x = 4: marks cover 3..9, 13..19.
2149        let r = row(crate::line::Dash::new(6.0, 4.0));
2150        assert!(on(&r, 3..9) && off(&r, 10..12) && on(&r, 13..19), "{r:?}");
2151        // Dots 2 across, 8 apart.
2152        let r = row(crate::line::Dash::new(2.0, 6.0));
2153        assert!(on(&r, 3..5) && off(&r, 6..10) && on(&r, 11..13), "{r:?}");
2154        // 5 px in: the first mark's last pixel, then the gap.
2155        let r = row(crate::line::Dash::new(6.0, 4.0).offset(5.0));
2156        assert!(off(&r, 6..7) && on(&r, 8..14), "{r:?}");
2157    }
2158
2159    #[test]
2160    fn a_stroke_covers_the_outline_and_not_the_inside() {
2161        let p = Path::parse("M2 2 H12 V12 H2 Z").unwrap();
2162        let m = rasterize(p.ops(), 1.0, (0, 0), 16, 16, MaskPaint::Stroke(2.0));
2163        let at = |x: usize, y: usize| m[y * 16 + x];
2164        assert_eq!(at(7, 7), 0);
2165        assert!(at(2, 7) > 200, "{}", at(2, 7));
2166        assert!(at(1, 7) > 200, "{}", at(1, 7));
2167    }
2168
2169    #[test]
2170    fn the_store_boxes_a_path_and_keeps_the_ops_relative() {
2171        let mut store = PathStore::default();
2172        store.begin_frame(false);
2173        let p = Path::parse("M10 20 H30 V40 Z").unwrap();
2174        let (id, rect) = store
2175            .push(p.ops(), FillRule::NonZero, 0.0, None, None)
2176            .unwrap();
2177        assert_eq!(rect, Rect::new(8.0, 18.0, 24.0, 24.0));
2178        let (run, ops) = store.run(id);
2179        assert_eq!(ops[0], PathOp::MoveTo(Vec2::new(2.0, 2.0)));
2180        assert_eq!(run.len, 4);
2181        assert!(
2182            store
2183                .push(
2184                    &[PathOp::MoveTo(Vec2::ZERO)],
2185                    FillRule::NonZero,
2186                    0.0,
2187                    None,
2188                    None
2189                )
2190                .is_none()
2191        );
2192        // A stroke widens the box by half its width.
2193        let (_, rect) = store
2194            .push(p.ops(), FillRule::NonZero, 4.0, None, None)
2195            .unwrap();
2196        assert_eq!(rect, Rect::new(6.0, 16.0, 28.0, 28.0));
2197        // A turn boxes the path by the square it sweeps about its pivot:
2198        // the same box, ops and hash at any angle.
2199        let turn = |t: f32| Turn {
2200            turns: t,
2201            pivot: Some(Vec2::new(10.0, 20.0)),
2202        };
2203        let (a, ra) = store
2204            .push(p.ops(), FillRule::NonZero, 0.0, None, Some(turn(0.0)))
2205            .unwrap();
2206        let (b, rb) = store
2207            .push(p.ops(), FillRule::NonZero, 0.0, None, Some(turn(0.3)))
2208            .unwrap();
2209        let far = (20.0f32 * 20.0 + 20.0 * 20.0).sqrt() + 2.0;
2210        assert_eq!(ra, Rect::new(10.0 - far, 20.0 - far, 2.0 * far, 2.0 * far));
2211        assert_eq!(ra, rb);
2212        assert_eq!(store.run(a).0.hash, store.run(b).0.hash);
2213        assert_eq!(store.run(a).1, store.run(b).1);
2214        assert_eq!(store.run(b).0.angle, Some(0.3 * std::f32::consts::TAU));
2215        store.begin_frame(true);
2216        assert!(store.prev_run(id).is_some());
2217        assert!(store.is_empty());
2218    }
2219
2220    /// A fill with no area paints nothing: the bleed is an edge's, and a
2221    /// progress ring at 0 has none.
2222    #[test]
2223    fn a_fill_with_no_area_is_an_empty_mask() {
2224        for p in [
2225            Path::sector(16.0, 16.0, 12.0, 8.0, 0.0, 0.0),
2226            Path::parse("M2 2 L20 2 Z").unwrap(),
2227        ] {
2228            let m = rasterize(
2229                p.ops(),
2230                1.0,
2231                (0, 0),
2232                32,
2233                32,
2234                MaskPaint::Fill(FillRule::NonZero),
2235            );
2236            assert!(m.iter().all(|&a| a == 0));
2237        }
2238    }
2239}