Skip to main content

kui_core/
line.rs

1//! Strokes: what a `line` node draws.
2//!
3//! A line is a run of points and a [`Stroke`]; the core flattens a curve
4//! into a polyline here, boxes the run, and emits one
5//! [`crate::QuadKind::Segment`] per straight piece. The points live in a
6//! per-frame list beside the tree — rebuilt every frame, capacities kept —
7//! and a node refers to its run by [`LineId`] the way a text node refers to
8//! the frame's text list. The previous frame's list is kept by the same
9//! gated buffer swap the text list uses, so a departing line can copy its
10//! points out for its ghost.
11
12use crate::color::Color;
13use crate::geom::{Rect, Vec2};
14use crate::retain::Kept;
15
16/// Index into the frame's line list.
17#[derive(Clone, Copy, Debug, PartialEq, Eq)]
18pub struct LineId(pub u32);
19
20/// How a `line` is drawn: a width, a colour, and whether the points are
21/// the corners of a polyline or the knots of a curve through them.
22#[derive(Clone, Copy, Debug, PartialEq)]
23pub struct Stroke {
24    /// Stroke width in logical px. Round caps at both ends of every
25    /// segment, which is also the join between two of them.
26    pub width: f32,
27    /// The stroke colour. Inside the core it rides in the node's `bg`
28    /// slot, so a `transition` eases it and `enter` / `exit` can start
29    /// or end it there.
30    pub color: Color,
31    /// Draw a smooth curve *through* the points (a centripetal Catmull-Rom
32    /// spline, flattened by [`flatten_curve`]) instead of the polyline. Two
33    /// points are a straight segment either way.
34    pub curve: bool,
35    /// The marks and gaps the stroke is cut into; [`Dash::SOLID`], the
36    /// default, is none.
37    pub dash: Dash,
38}
39
40impl Stroke {
41    pub fn new(width: f32, color: Color) -> Self {
42        Self {
43            width,
44            color,
45            curve: false,
46            dash: Dash::SOLID,
47        }
48    }
49
50    pub fn curve(mut self) -> Self {
51        self.curve = true;
52        self
53    }
54
55    /// Cuts the stroke into marks `on` long with gaps `off` long between
56    /// them; see [`Dash`].
57    pub fn dash(mut self, on: f32, off: f32) -> Self {
58        self.dash = Dash {
59            offset: self.dash.offset,
60            ..Dash::new(on, off)
61        };
62        self
63    }
64
65    /// The whole pattern at once, for a dash-dot or one read from data.
66    pub fn dashed(mut self, dash: Dash) -> Self {
67        self.dash = dash;
68        self
69    }
70
71    /// How far into the pattern the stroke starts; see [`Dash::offset`].
72    pub fn dash_offset(mut self, offset: f32) -> Self {
73        self.dash.offset = offset;
74        self
75    }
76}
77
78/// A stroke's dash pattern (backlog V2): a mark, a gap, a second mark and
79/// a second gap, repeated along the stroke's length from its first point.
80///
81/// The lengths are the ones **seen**, in logical px. Every mark is
82/// round-capped, as the stroke itself is, so a mark `on` long is drawn as
83/// a capsule whose centre line is `on − width` long, and a mark no longer
84/// than the stroke is wide is a dot. (SVG's `stroke-dasharray` measures
85/// the centre line instead, which with a round cap makes `4 4` at a width
86/// of 4 a solid line; this pattern at any width is SVG's
87/// `on − width, off + width`.) The first mark's cap sits where a solid
88/// stroke's would, half the width before the first point.
89///
90/// A dot is as wide as the stroke whatever its mark says, so where a
91/// mark and its gap together come to no more than the width the dots
92/// meet or overlap and the gap is not seen. Such a gap closes: the marks
93/// either side of it are one mark, and a pattern with no gap left draws
94/// solid (backlog RG118) — where `{2, 2}` at a width of 8 was 8 px dots
95/// every 4 px, a lumpy solid line at a quad a dot. Every gap that is seen
96/// is the length given.
97///
98/// The pattern runs along the arc length of the whole stroke, so it keeps
99/// its phase across the corners of a polyline and the pieces of a curve;
100/// on a `path` it restarts at every subpath, as SVG's does.
101#[derive(Clone, Copy, Debug, PartialEq)]
102pub struct Dash {
103    /// Mark, gap, mark, gap. A two-length pattern is the same pair twice.
104    pub pattern: [f32; 4],
105    /// How far into the pattern the stroke starts, in logical px: growing
106    /// it moves the marks towards the stroke's first point, which is what
107    /// a marquee's marching ants are. Any number; it wraps.
108    pub offset: f32,
109}
110
111impl Default for Dash {
112    fn default() -> Self {
113        Dash::SOLID
114    }
115}
116
117/// What a dashed stroke is cut by: the centre-line lengths of the
118/// pattern's four entries — mark, gap, mark, gap — and where in it the
119/// stroke starts, `0 <= offset < period`. Logical px.
120#[derive(Clone, Copy, Debug, PartialEq)]
121pub struct Cut {
122    pub lens: [f32; 4],
123    pub offset: f32,
124}
125
126impl Cut {
127    pub fn period(&self) -> f32 {
128        self.lens.iter().sum()
129    }
130
131    /// Whether a mark and its gap come to less than `min` together, on
132    /// average: a pattern that fine reads as a tint, and is drawn solid.
133    pub fn finer_than(&self, min: f32) -> bool {
134        let pair = self.period() * 0.5;
135        pair.is_nan() || pair < min
136    }
137
138    /// The pattern as one number, for a mask's key and for telling a
139    /// pattern that moved from one that held.
140    pub(crate) fn hash(&self) -> u64 {
141        let [a, b, c, d] = self.lens.map(|l| u64::from(l.to_bits()));
142        let mut h = a ^ b.rotate_left(16) ^ c.rotate_left(32) ^ d.rotate_left(48);
143        h = h.wrapping_mul(0x0000_0100_0000_01b3) ^ u64::from(self.offset.to_bits());
144        h.wrapping_mul(0x0000_0100_0000_01b3)
145    }
146}
147
148/// The most marks one stroke is cut into; a pattern that would make more
149/// — a period of a pixel along a line no screen is long enough for —
150/// draws solid, which is what it would read as.
151pub const MAX_MARKS: usize = 16384;
152
153impl Dash {
154    /// No dashes: the stroke unbroken.
155    pub const SOLID: Dash = Dash {
156        pattern: [0.0; 4],
157        offset: 0.0,
158    };
159
160    /// Marks `on` long with gaps `off` long between them.
161    pub fn new(on: f32, off: f32) -> Self {
162        Dash {
163            pattern: [on, off, on, off],
164            offset: 0.0,
165        }
166    }
167
168    /// The pattern as the bindings spell it: one length (marks and gaps
169    /// alike), two (a mark and a gap) or four (a mark, a gap, a second
170    /// mark, a second gap — a dash-dot). None for any other count.
171    pub fn of(lengths: &[f32]) -> Option<Self> {
172        let pattern = match *lengths {
173            [a] => [a, a, a, a],
174            [a, b] => [a, b, a, b],
175            [a, b, c, d] => [a, b, c, d],
176            _ => return None,
177        };
178        Some(Dash {
179            pattern,
180            offset: 0.0,
181        })
182    }
183
184    /// [`Self::offset`] set.
185    pub fn offset(mut self, offset: f32) -> Self {
186        self.offset = offset;
187        self
188    }
189
190    /// Whether the pattern leaves the stroke unbroken: no gap in it, or a
191    /// length that is not a number.
192    pub fn is_solid(&self) -> bool {
193        self.cut(0.0).is_none()
194    }
195
196    /// The pattern as centre-line lengths for a stroke `width` wide, or
197    /// None for one that draws solid: a length that is not finite, or no
198    /// gap anywhere that the width leaves seen. Negative lengths are zero,
199    /// a pair that is zero altogether is the other pair, and a gap the
200    /// width closes joins the marks either side of it.
201    #[inline]
202    pub fn cut(&self, width: f32) -> Option<Cut> {
203        // The solid stroke, which is nearly every stroke, is told by its
204        // zeroes and pays for nothing below (backlog C52).
205        if self.pattern == Dash::SOLID.pattern {
206            return None;
207        }
208        self.cut_pattern(width)
209    }
210
211    #[cold]
212    #[inline(never)]
213    fn cut_pattern(&self, width: f32) -> Option<Cut> {
214        if !self.pattern.iter().all(|l| l.is_finite()) || !self.offset.is_finite() {
215            return None;
216        }
217        let [a, b, c, d] = self.pattern.map(|l| l.max(0.0));
218        if b <= 0.0 && d <= 0.0 {
219            return None;
220        }
221        let (first, second) = match (a + b > 0.0, c + d > 0.0) {
222            (true, true) => ([a, b], [c, d]),
223            (true, false) => ([a, b], [a, b]),
224            (false, _) => ([c, d], [c, d]),
225        };
226        let w = width.max(0.0);
227        // The mark's caps are part of what is seen, so they come out of
228        // its centre line and go into the gap's.
229        let centre = |[on, off]: [f32; 2]| {
230            let mark = (on - w).max(0.0);
231            [mark, on + off - mark]
232        };
233        let ([m0, g0], [m1, g1]) = (centre(first), centre(second));
234        let period = m0 + g0 + m1 + g1;
235        // A gap is seen as its centre line less the caps of the marks
236        // either side, half the width each; one that leaves nothing closes
237        // and its marks are one (RG118). The merged pair is written twice,
238        // so the walk is unchanged, and where the merged mark starts at the
239        // second mark the offset moves with it.
240        let (lens, from) = match (g0 > w, g1 > w) {
241            (true, true) => ([m0, g0, m1, g1], 0.0),
242            (false, true) => {
243                let m = m0 + g0 + m1;
244                ([m, g1, m, g1], 0.0)
245            }
246            (true, false) => {
247                let m = m1 + g1 + m0;
248                ([m, g0, m, g0], m0 + g0)
249            }
250            (false, false) => return None,
251        };
252        Some(Cut {
253            lens,
254            offset: (self.offset - from).rem_euclid(period),
255        })
256    }
257}
258
259impl Cut {
260    /// Calls `mark(from, to)` for every mark along the polyline `points`,
261    /// in order — a mark that turns a corner is one call per piece it
262    /// lies on, meeting at the corner, and a dot is a call with both ends
263    /// the same. False, with nothing called, for a stroke that draws
264    /// solid instead: a pattern [`Self::finer_than`] `min`, more than
265    /// [`MAX_MARKS`] marks, or a length that is zero or not finite.
266    pub fn marks(&self, points: &[Vec2], min: f32, mut mark: impl FnMut(Vec2, Vec2)) -> bool {
267        let period = self.period();
268        let piece = |pair: &[Vec2]| (pair[1].x - pair[0].x).hypot(pair[1].y - pair[0].y);
269        let total: f32 = points.windows(2).map(piece).sum();
270        // Two marks a period; `!(..)` so a NaN draws solid too.
271        let marks = total / period * 2.0;
272        // A stroke of no length has no mark to lie on and is the dot its
273        // solid one is (RG116).
274        if self.finer_than(min) || marks.is_nan() || marks > MAX_MARKS as f32 || total <= 0.0 {
275            return false;
276        }
277        // Where the stroke starts in the pattern: the entry and what is
278        // left of it.
279        let (mut i, mut left) = (0, self.lens[0]);
280        let mut skip = self.offset;
281        while skip >= left && skip > 0.0 {
282            skip -= left;
283            i = (i + 1) & 3;
284            left = self.lens[i];
285        }
286        left -= skip;
287        // A mark that ended exactly on a corner has been drawn up to it;
288        // the entry after it starts on the next piece.
289        for pair in points.windows(2) {
290            let len = piece(pair);
291            if len <= 0.0 {
292                continue;
293            }
294            let (a, b) = (pair[0], pair[1]);
295            let at = |s: f32| {
296                let t = s / len;
297                Vec2::new(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t)
298            };
299            let mut pos = 0.0;
300            loop {
301                let take = left.min(len - pos);
302                if i & 1 == 0 {
303                    mark(at(pos), at(pos + take));
304                }
305                pos += take;
306                left -= take;
307                if left > 0.0 {
308                    break;
309                }
310                i = (i + 1) & 3;
311                left = self.lens[i];
312                // The piece is used up: what starts here starts on the
313                // next one, except a dot, which has nowhere else to be
314                // when this is the last.
315                if pos >= len && left > 0.0 {
316                    break;
317                }
318            }
319        }
320        true
321    }
322}
323
324/// One stroke's run in the frame's point list. The points are stored
325/// relative to the node's box, so a node that eases or slides carries
326/// them along.
327#[derive(Clone, Copy, Debug, PartialEq)]
328pub(crate) struct Run {
329    pub first: u32,
330    pub len: u32,
331    /// Logical px.
332    pub width: f32,
333    /// What cuts the stroke into marks; None for a solid one.
334    pub dash: Option<Cut>,
335}
336
337/// The frame's strokes, and the previous frame's while an `exit` needs it.
338#[derive(Default)]
339pub struct LineStore {
340    runs: Kept<Run>,
341    points: Kept<Vec2>,
342}
343
344/// How far a stroke's box extends past its points: half the width, plus
345/// two logical px so a backend's edge ramp (`AA` = 0.75 physical px each
346/// side, sampled at pixel centres) is never cut by the quad, at scale 1
347/// included.
348pub(crate) fn pad(width: f32) -> f32 {
349    width.max(0.0) * 0.5 + 2.0
350}
351
352/// A curve span is flattened into one piece per this many logical px of
353/// chord, at least one and at most [`CURVE_MAX_PIECES`]. Fixed rather than
354/// tolerance-driven so the piece count is a function of the declared
355/// geometry alone — every binding gets the same segments, and the
356/// conformance corpus can pin them.
357pub const CURVE_STEP: f32 = 6.0;
358pub const CURVE_MAX_PIECES: usize = 32;
359
360impl LineStore {
361    /// Starts a frame. `keep_prev` retains the list just finished so a
362    /// departing line's ghost can copy its points out of it.
363    pub(crate) fn begin_frame(&mut self, keep_prev: bool) {
364        self.runs.begin(keep_prev);
365        self.points.begin(keep_prev);
366    }
367
368    /// Adds a stroke through `points` (parent-box coordinates): flattens a
369    /// curve, boxes the run, and stores the points relative to the box.
370    /// Returns the id and the box, or None for fewer than two points,
371    /// which draw nothing.
372    pub(crate) fn push(&mut self, points: &[Vec2], stroke: &Stroke) -> Option<(LineId, Rect)> {
373        if points.len() < 2 {
374            return None;
375        }
376        let first = self.points.len();
377        if stroke.curve && points.len() > 2 {
378            flatten_curve(points, &mut self.points);
379        } else {
380            self.points.extend_from_slice(points);
381        }
382        let run = &mut self.points[first..];
383        let (mut min, mut max) = (run[0], run[0]);
384        for p in run.iter() {
385            min.x = min.x.min(p.x);
386            min.y = min.y.min(p.y);
387            max.x = max.x.max(p.x);
388            max.y = max.y.max(p.y);
389        }
390        let pad = pad(stroke.width);
391        let origin = Vec2::new(min.x - pad, min.y - pad);
392        for p in run.iter_mut() {
393            p.x -= origin.x;
394            p.y -= origin.y;
395        }
396        let rect = Rect::new(
397            origin.x,
398            origin.y,
399            max.x - min.x + 2.0 * pad,
400            max.y - min.y + 2.0 * pad,
401        );
402        let id = LineId(self.runs.len() as u32);
403        self.runs.push(Run {
404            first: first as u32,
405            len: (self.points.len() - first) as u32,
406            width: stroke.width,
407            dash: stroke.dash.cut(stroke.width),
408        });
409        Some((id, rect))
410    }
411
412    /// This frame's run: its width and its points, relative to the node.
413    pub(crate) fn run(&self, id: LineId) -> (Run, &[Vec2]) {
414        let run = self.runs[id.0 as usize];
415        (
416            run,
417            &self.points[run.first as usize..(run.first + run.len) as usize],
418        )
419    }
420
421    /// The same, read from the previous frame's list (a departing line's
422    /// id indexes that list, not this frame's). Empty for an id the kept
423    /// frame does not have, which cannot happen while `begin_frame` keeps
424    /// the two in step.
425    pub(crate) fn prev_run(&self, id: LineId) -> (Run, &[Vec2]) {
426        match self.runs.prev().get(id.0 as usize) {
427            Some(run) => (
428                *run,
429                &self.points.prev()[run.first as usize..(run.first + run.len) as usize],
430            ),
431            None => (
432                Run {
433                    first: 0,
434                    len: 0,
435                    width: 0.0,
436                    dash: None,
437                },
438                &[],
439            ),
440        }
441    }
442
443    /// Runs this frame.
444    pub fn len(&self) -> usize {
445        self.runs.len()
446    }
447
448    pub fn is_empty(&self) -> bool {
449        self.runs.is_empty()
450    }
451}
452
453/// Flattens a **centripetal** Catmull-Rom spline through `knots` (at least
454/// three) into `out`, starting at the first knot and ending at the last.
455/// Each span is cut into `ceil(chord / CURVE_STEP)` pieces, clamped to
456/// `1..=CURVE_MAX_PIECES`.
457///
458/// Centripetal means the spline's knot parameter advances by the square
459/// root of each chord instead of by 1. A *uniform*
460/// spline — the textbook one, and what this was until it drew a mind
461/// map — ignores how far apart its knots are, so knots that are close
462/// together get as much parameter as knots that are far apart, and the
463/// curve has to move fast through the tight ones. On an elbow (a long run,
464/// then a sharp turn) that shows up as a bow out of the wrong side of the
465/// corner; on knots spaced unevenly enough it becomes a cusp or a loop,
466/// which a centripetal span provably never has. It bows less
467/// too, though it is not overshoot-free: a spline that must pass *through*
468/// a corner has to lean into it. A shape that should only be *pulled*
469/// towards its middle points is a Bézier, and the caller samples one
470/// (`examples/rust/widgets/line.rs`).
471///
472/// The end knots are not doubled — a repeated knot is a zero-length chord,
473/// which centripetal has no parameter for. Each end gets a mirrored
474/// phantom instead (`2·p1 − p2`), which spaces evenly and leaves the first
475/// span's tangent along its own chord. A coincident pair of *real* knots
476/// takes the same branch, so a duplicated point in a caller's list stays a
477/// harmless kink rather than a division by zero.
478///
479/// The chords are rolled forward rather than recomputed, so a span costs
480/// two square roots and not six; `frame_1k_curves` is the bench that
481/// watches the rest of it.
482pub fn flatten_curve(knots: &[Vec2], out: &mut Vec<Vec2>) {
483    let n = knots.len();
484    if n < 2 {
485        return;
486    }
487    out.push(knots[0]);
488    // This span's chord and its knot step (√chord), and the span before's:
489    // a zero chord means there is no neighbour on that side, either
490    // because the run ends there or because the two knots coincide.
491    let (mut prev, mut prev_step) = (0.0, 0.0);
492    let (mut chord, mut step) = chord_and_step(knots[0], knots[1]);
493    for i in 0..n - 1 {
494        let (p1, p2) = (knots[i], knots[i + 1]);
495        let (next, next_step) = match knots.get(i + 2) {
496            Some(&p) => chord_and_step(p2, p),
497            None => (0.0, 0.0),
498        };
499        let pieces = ((chord / CURVE_STEP).ceil() as usize).clamp(1, CURVE_MAX_PIECES);
500        if chord == 0.0 {
501            // Nothing to parameterize: the span is a point.
502            out.push(p2);
503        } else {
504            let mirror = |p: Vec2, q: Vec2| Vec2::new(2.0 * p.x - q.x, 2.0 * p.y - q.y);
505            let (p0, s0) = match prev > 0.0 {
506                true => (knots[i - 1], prev_step),
507                false => (mirror(p1, p2), step),
508            };
509            let (p3, s2) = match next > 0.0 {
510                true => (knots[i + 2], next_step),
511                false => (mirror(p2, p1), step),
512            };
513            // Knot times: 0, then one step per span.
514            let span = Span::new([p0, p1, p2, p3], [0.0, s0, s0 + step, s0 + step + s2]);
515            for k in 1..pieces {
516                out.push(span.at(s0 + step * (k as f32 / pieces as f32)));
517            }
518            // The knot itself rather than the evaluation at its time, so
519            // the run passes through it bit for bit whatever the
520            // arithmetic rounds to.
521            out.push(p2);
522        }
523        (prev, prev_step) = (chord, step);
524        (chord, step) = (next, next_step);
525    }
526}
527
528/// A span's length and the parameter it is worth: `√chord` is Lee's
529/// α = ½, the centripetal one. α = 0 would be `1.0` (uniform) and α = 1
530/// the chord itself (chordal).
531fn chord_and_step(a: Vec2, b: Vec2) -> (f32, f32) {
532    let chord = ((b.x - a.x).powi(2) + (b.y - a.y).powi(2)).sqrt();
533    (chord, chord.sqrt())
534}
535
536/// One span of the spline, evaluated between `p[1]` and `p[2]` by Barry
537/// and Goldman's pyramid: three interpolations of the knots in their own
538/// times, then two of those, then one. Written this way rather than as a
539/// cubic in `t` because the times are no longer evenly spaced.
540///
541/// A span is built once and asked for every piece, because the pyramid's
542/// five distinct denominators are the same five numbers each time. They
543/// are reciprocals, so `at(t[2])` is only *nearly* `p[2]` — the caller
544/// pushes the knot itself instead of asking for it.
545struct Span {
546    p: [Vec2; 4],
547    t: [f32; 4],
548    /// `1/(t[1]-t[0])`, `1/(t[2]-t[1])`, `1/(t[3]-t[2])`, `1/(t[2]-t[0])`,
549    /// `1/(t[3]-t[1])`, in the order the pyramid needs them.
550    inv: [f32; 5],
551}
552
553impl Span {
554    fn new(p: [Vec2; 4], t: [f32; 4]) -> Self {
555        let inv = [
556            1.0 / (t[1] - t[0]),
557            1.0 / (t[2] - t[1]),
558            1.0 / (t[3] - t[2]),
559            1.0 / (t[2] - t[0]),
560            1.0 / (t[3] - t[1]),
561        ];
562        Span { p, t, inv }
563    }
564
565    fn at(&self, u: f32) -> Vec2 {
566        let (p, t) = (&self.p, &self.t);
567        let blend = |a: Vec2, b: Vec2, ta: f32, inv: f32| {
568            let w = (u - ta) * inv;
569            Vec2::new(a.x + (b.x - a.x) * w, a.y + (b.y - a.y) * w)
570        };
571        let a1 = blend(p[0], p[1], t[0], self.inv[0]);
572        let a2 = blend(p[1], p[2], t[1], self.inv[1]);
573        let a3 = blend(p[2], p[3], t[2], self.inv[2]);
574        let b1 = blend(a1, a2, t[0], self.inv[3]);
575        let b2 = blend(a2, a3, t[1], self.inv[4]);
576        blend(b1, b2, t[1], self.inv[1])
577    }
578}
579
580#[cfg(test)]
581mod tests {
582    use super::*;
583
584    #[test]
585    fn a_curve_passes_through_its_knots_and_ends_where_they_end() {
586        let knots = [
587            Vec2::new(0.0, 0.0),
588            Vec2::new(30.0, 40.0),
589            Vec2::new(60.0, 0.0),
590        ];
591        let mut out = Vec::new();
592        flatten_curve(&knots, &mut out);
593        assert_eq!(out[0], knots[0]);
594        assert_eq!(*out.last().unwrap(), knots[2]);
595        // Chord 50 → 9 pieces per span, 1 + 9 + 9 points.
596        assert_eq!(out.len(), 19);
597        assert_eq!(out[9], knots[1]);
598    }
599
600    /// How sharply the run doubles back, worst piece, in degrees. A
601    /// smoothly flattened curve turns a few degrees a piece; a cusp turns
602    /// most of the way round.
603    fn worst_turn(pts: &[Vec2]) -> f32 {
604        pts.windows(3).fold(0.0f32, |worst, w| {
605            let (a, b) = (
606                Vec2::new(w[1].x - w[0].x, w[1].y - w[0].y),
607                Vec2::new(w[2].x - w[1].x, w[2].y - w[1].y),
608            );
609            let len = |v: Vec2| (v.x * v.x + v.y * v.y).sqrt();
610            let (la, lb) = (len(a), len(b));
611            if la < 1e-6 || lb < 1e-6 {
612                return worst;
613            }
614            let cos = ((a.x * b.x + a.y * b.y) / (la * lb)).clamp(-1.0, 1.0);
615            worst.max(cos.acos().to_degrees())
616        })
617    }
618
619    /// Whether any two non-adjacent pieces of the run cross.
620    fn ties_a_loop(pts: &[Vec2]) -> bool {
621        let side =
622            |a: Vec2, b: Vec2, c: Vec2| (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
623        (0..pts.len() - 1).any(|i| {
624            (i + 2..pts.len() - 1).any(|j| {
625                let (a, b, c, d) = (pts[i], pts[i + 1], pts[j], pts[j + 1]);
626                side(a, b, c) * side(a, b, d) < 0.0 && side(c, d, a) * side(c, d, b) < 0.0
627            })
628        })
629    }
630
631    /// The reason the parameterization is centripetal and not uniform.
632    /// Chords 671, 36 and 328: a knot pair nineteen times tighter than its
633    /// neighbours. A uniform parameter hands that 36px chord as much curve
634    /// as the 671px one, and the middle span has to tie a loop to spend
635    /// it — at `CURVE_STEP`'s own sampling, one crossing and a piece that
636    /// turns 95°. Centripetal spends parameter by `√chord`, so the tight
637    /// pair is just a corner.
638    #[test]
639    fn a_tight_knot_between_two_long_ones_neither_loops_nor_cusps() {
640        let knots = [
641            Vec2::new(0.0, 400.0),
642            Vec2::new(600.0, 100.0),
643            Vec2::new(630.0, 80.0),
644            Vec2::new(560.0, 400.0),
645        ];
646        let mut out = Vec::new();
647        flatten_curve(&knots, &mut out);
648        assert!(!ties_a_loop(&out), "the run crosses itself");
649        assert!(worst_turn(&out) < 45.0, "cusp: {:.1}°", worst_turn(&out));
650    }
651
652    /// A caller's list may repeat a point — a mind map with two cards at
653    /// the same place, a path snapped to a grid. The mirrored phantom
654    /// covers it: a zero chord has no `√chord` to divide by, and every
655    /// point that comes back is a number.
656    #[test]
657    fn a_repeated_knot_is_a_kink_and_not_a_division_by_zero() {
658        let knots = [
659            Vec2::new(0.0, 0.0),
660            Vec2::new(40.0, 0.0),
661            Vec2::new(40.0, 0.0),
662            Vec2::new(40.0, 40.0),
663            Vec2::new(80.0, 40.0),
664        ];
665        let mut out = Vec::new();
666        flatten_curve(&knots, &mut out);
667        assert!(out.iter().all(|p| p.x.is_finite() && p.y.is_finite()));
668        assert_eq!(out[0], knots[0]);
669        assert_eq!(*out.last().unwrap(), knots[4]);
670    }
671
672    #[test]
673    fn two_points_are_one_segment_even_as_a_curve() {
674        let mut store = LineStore::default();
675        let (id, rect) = store
676            .push(
677                &[Vec2::new(10.0, 10.0), Vec2::new(50.0, 40.0)],
678                &Stroke::new(2.0, Color::WHITE).curve(),
679            )
680            .unwrap();
681        let (run, pts) = store.run(id);
682        assert_eq!(run.len, 2);
683        assert_eq!(run.width, 2.0);
684        // Padded by half the width plus two: the box starts at (7, 7).
685        assert_eq!((rect.x, rect.y, rect.w, rect.h), (7.0, 7.0, 46.0, 36.0));
686        assert_eq!(pts[0], Vec2::new(3.0, 3.0));
687        assert_eq!(pts[1], Vec2::new(43.0, 33.0));
688    }
689
690    #[test]
691    fn one_point_draws_nothing() {
692        let mut store = LineStore::default();
693        assert!(
694            store
695                .push(&[Vec2::new(1.0, 1.0)], &Stroke::new(1.0, Color::WHITE))
696                .is_none()
697        );
698    }
699
700    #[test]
701    fn the_previous_frame_is_kept_only_when_asked() {
702        let mut store = LineStore::default();
703        let (id, _) = store
704            .push(
705                &[Vec2::new(0.0, 0.0), Vec2::new(4.0, 0.0)],
706                &Stroke::new(1.0, Color::WHITE),
707            )
708            .unwrap();
709        store.begin_frame(true);
710        assert_eq!(store.prev_run(id).0.len, 2);
711        assert!(store.is_empty());
712        store.begin_frame(false);
713        assert_eq!(store.prev_run(id).0.len, 0);
714    }
715}