simple_editor\engine/
shapes.rs

1//! Vector shapes and recorded drawings -> RGBA layers (CPU rasteriser; the GPU path samples the result).
2//!
3//! `render(style, scale, local_t)` draws the shape centred in its own tight layer:
4//!  * Rect / Ellipse / Triangle / Polygon / Star: `fill` plus an optional `stroke` outline of
5//!    `stroke_width` (project px x scale); rounded corners for Rect via `corner`.
6//!  * Line / Arrow: from (-w, -h) to (+w, +h) in layer space, head sized by `corner`.
7//!  * Draw: the recorded strokes revealed up to `local_t * draw_rate` (0 = all at once) on the optional
8//!    `page` background.
9//! Anti-aliased with a coverage rasteriser (no new deps). Cached by `ShapeStyle::cache_key()` + size +
10//! reveal bucket.
11
12use crate::media::Frame;
13use crate::model::{ShapeKind, ShapeStyle};
14use std::collections::hash_map::DefaultHasher;
15use std::hash::{Hash, Hasher};
16use std::sync::Arc;
17
18/// Vertical sub-scanlines per pixel row (horizontal coverage is exact, so 4 is plenty).
19const SUB: usize = 4;
20/// Layer dimensions are clamped to this; `scale` shrinks instead so the geometry stays consistent.
21const MAX_DIM: f32 = 8192.0;
22/// ...and so is the total pixel count (64 MB RGBA), which a 4K full-canvas shape stays well under.
23const MAX_PIXELS: f32 = 16.0e6;
24/// Reveal-time quantisation for the cache (a drawing re-rasterises 30x/s at most).
25const REVEAL_HZ: f64 = 30.0;
26/// Star inner radius as a fraction of the outer one.
27const STAR_INNER: f32 = 0.45;
28const CACHE_MAX: usize = 48;
29
30#[derive(Default)]
31pub struct ShapeRasterizer {
32    cache: std::collections::HashMap<(u64, u32, u32, u32), Arc<Frame>>,
33    /// Scratch: one coverage buffer, one polygon soup, one edge table, one outline.
34    cov: Vec<f32>,
35    path: Path,
36    edges: Vec<Edge>,
37    active: Vec<usize>,
38    xs: Vec<(f32, i32)>,
39    outline: Vec<[f32; 2]>,
40}
41
42impl ShapeRasterizer {
43    pub fn new() -> Self {
44        Self::default()
45    }
46    /// Layer size in project px for a style at clip-local time `t` (before `scale`).
47    pub fn size(style: &ShapeStyle, t: f64) -> (f32, f32) {
48        let (w, h) = half_size(style, t);
49        let sw = if style.stroke_width.is_finite() { style.stroke_width.max(0.0) } else { 0.0 };
50        let (fw, fh) = match style.kind {
51            ShapeKind::Draw => draw_half_size(style),
52            ShapeKind::Line => {
53                let pad = sw.max(1.0);
54                (w + pad * 0.5, h + pad * 0.5)
55            }
56            ShapeKind::Arrow => {
57                let pad = sw.max(1.0).max(arrow_head(style));
58                (w + pad * 0.5, h + pad * 0.5)
59            }
60            // Only pad for a stroke that is actually visible, so a plain rect gets an exact layer.
61            _ => {
62                let pad = if style.stroke[3] > 0 { sw * 0.5 } else { 0.0 };
63                (w + pad, h + pad)
64            }
65        };
66        ((fw * 2.0).max(1.0), (fh * 2.0).max(1.0))
67    }
68
69    /// Rasterise at `scale` (canvas px per project px) for clip-local time `t`.
70    pub fn render(&mut self, style: &ShapeStyle, scale: f32, t: f64) -> Arc<Frame> {
71        let (pw, ph) = Self::size(style, t);
72        let mut s = if scale.is_finite() && scale > 0.0 { scale } else { 1.0 };
73        s = s.min(MAX_DIM / pw).min(MAX_DIM / ph);
74        let area = pw * s * ph * s;
75        if area > MAX_PIXELS {
76            s *= (MAX_PIXELS / area).sqrt();
77        }
78        s = s.max(1e-3);
79        let lw = (pw * s).round().clamp(1.0, MAX_DIM) as u32;
80        let lh = (ph * s).round().clamp(1.0, MAX_DIM) as u32;
81        let bucket = reveal_bucket(style, t);
82        let mut hh = DefaultHasher::new();
83        style.cache_key().hash(&mut hh);
84        for f in [s, pw, ph] {
85            f.to_bits().hash(&mut hh);
86        }
87        let key = (hh.finish(), lw, lh, bucket);
88        if let Some(f) = self.cache.get(&key) {
89            return f.clone();
90        }
91        let frame = Arc::new(self.rasterize(style, s, t, lw, lh, bucket));
92        if self.cache.len() >= CACHE_MAX {
93            // ponytail: drop-all cache — LRU if shape-heavy projects thrash
94            self.cache.clear();
95        }
96        self.cache.insert(key, frame.clone());
97        frame
98    }
99
100    fn rasterize(&mut self, style: &ShapeStyle, s: f32, t: f64, lw: u32, lh: u32, bucket: u32) -> Frame {
101        let mut out = Frame::new(lw, lh);
102        let (cx, cy) = (lw as f32 * 0.5, lh as f32 * 0.5);
103        let (w, h) = half_size(style, t);
104        let sw = (style.stroke_width.max(0.0) * s).max(0.0);
105        self.path.clear();
106        match style.kind {
107            ShapeKind::Draw => {
108                if style.page[3] > 0 {
109                    out.fill(style.page);
110                }
111                let reveal = bucket_time(bucket);
112                for st in &style.strokes {
113                    revealed(&st.points, reveal, &mut self.outline);
114                    if self.outline.is_empty() {
115                        continue;
116                    }
117                    to_layer(&mut self.outline, cx, cy, s);
118                    self.path.clear();
119                    add_stroke(&mut self.path, &self.outline, false, st.width.max(0.1) * s * 0.5);
120                    self.flush(&mut out, st.color);
121                }
122                return out;
123            }
124            ShapeKind::Line | ShapeKind::Arrow => {
125                let head = if style.kind == ShapeKind::Arrow { arrow_head(style) * s } else { 0.0 };
126                // signed: the layer is sized from the absolute extents, but which corner the line runs
127                // between is exactly what the signs say
128                let (sw_, sh_) = signed_half(style, t);
129                let (a, b) = ([cx - sw_ * s, cy - sh_ * s], [cx + sw_ * s, cy + sh_ * s]);
130                let (dx, dy) = (b[0] - a[0], b[1] - a[1]);
131                let len = (dx * dx + dy * dy).sqrt();
132                if len > 1e-4 {
133                    let (ux, uy) = (dx / len, dy / len);
134                    // stop the shaft inside the head so it does not poke through the tip
135                    let end = [b[0] - ux * head * 0.6, b[1] - uy * head * 0.6];
136                    self.outline.clear();
137                    self.outline.push(a);
138                    self.outline.push(end);
139                    add_stroke(&mut self.path, &self.outline, false, (sw * 0.5).max(0.35));
140                    if head > 0.0 {
141                        let base = [b[0] - ux * head, b[1] - uy * head];
142                        let (px, py) = (-uy * head * 0.5, ux * head * 0.5);
143                        self.path.push(b);
144                        self.path.push([base[0] + px, base[1] + py]);
145                        self.path.push([base[0] - px, base[1] - py]);
146                        self.path.end();
147                    }
148                } else if sw > 0.0 {
149                    add_disc(&mut self.path, [cx, cy], (sw * 0.5).max(0.35));
150                }
151                self.flush(&mut out, style.stroke);
152                return out;
153            }
154            _ => shape_outline(style, w, h, s, &mut self.outline),
155        }
156        to_layer(&mut self.outline, cx, cy, s);
157        if style.fill[3] > 0 {
158            for p in self.outline.iter() {
159                self.path.push(*p);
160            }
161            self.path.end();
162            self.flush(&mut out, style.fill);
163        }
164        if style.stroke[3] > 0 && sw > 0.0 {
165            self.path.clear();
166            add_stroke(&mut self.path, &self.outline, true, (sw * 0.5).max(0.35));
167            self.flush(&mut out, style.stroke);
168        }
169        out
170    }
171
172    /// Rasterise the current path into the scratch coverage buffer and composite `color` over `out`.
173    fn flush(&mut self, out: &mut Frame, color: [u8; 4]) {
174        if color[3] == 0 {
175            self.path.clear();
176            return;
177        }
178        let (w, h) = (out.width as usize, out.height as usize);
179        let band = fill_path(&self.path, w, h, &mut self.cov, &mut self.edges, &mut self.active, &mut self.xs);
180        self.path.clear();
181        let Some((x0, y0, x1, y1)) = band else { return };
182        for y in y0..y1 {
183            for x in x0..x1 {
184                let c = self.cov[y * w + x];
185                if c > 0.0015 {
186                    let i = (y * w + x) * 4;
187                    blend(&mut out.rgba[i..i + 4], color, c);
188                }
189            }
190        }
191    }
192}
193
194// ---------- geometry ----------
195
196fn half_size(style: &ShapeStyle, t: f64) -> (f32, f32) {
197    let f = |a: f64| {
198        let v = a as f32;
199        if v.is_finite() {
200            v.abs().min(20000.0)
201        } else {
202            0.0
203        }
204    };
205    // an explicit vertex list sizes its own layer (symmetric about the centre, like a drawing)
206    if let Some(pts) = style.poly_points() {
207        let (mut hx, mut hy) = (0.0f32, 0.0f32);
208        for &(x, y) in pts {
209            if x.is_finite() && y.is_finite() {
210                hx = hx.max(x.abs());
211                hy = hy.max(y.abs());
212            }
213        }
214        return (hx.min(20000.0).max(0.5), hy.min(20000.0).max(0.5));
215    }
216    (f(style.w.at(t)), f(style.h.at(t)))
217}
218
219/// Half-extents WITH their sign, for the shapes whose extents encode a direction. Line and Arrow run
220/// from (-w, -h) to (+w, +h), so making them absolute (as `half_size` must, for a layer size) collapses
221/// every line onto the same diagonal whichever way it was actually drawn.
222fn signed_half(style: &ShapeStyle, t: f64) -> (f32, f32) {
223    let f = |a: f64| {
224        let v = a as f32;
225        if v.is_finite() {
226            v.clamp(-20000.0, 20000.0)
227        } else {
228            0.0
229        }
230    };
231    (f(style.w.at(t)), f(style.h.at(t)))
232}
233
234/// Arrow head length in project px (`corner`, or a stroke-relative default when it is 0).
235fn arrow_head(style: &ShapeStyle) -> f32 {
236    if style.corner > 0.0 && style.corner.is_finite() {
237        style.corner
238    } else {
239        (style.stroke_width.max(0.0) * 4.0).max(8.0)
240    }
241}
242
243/// Half-size of a drawing: the strokes are relative to the layer centre, so the layer is symmetric.
244fn draw_half_size(style: &ShapeStyle) -> (f32, f32) {
245    let (mut hx, mut hy) = (0.0f32, 0.0f32);
246    for st in &style.strokes {
247        let r = st.width.max(0.5) * 0.5;
248        for p in &st.points {
249            if p.0.is_finite() && p.1.is_finite() {
250                hx = hx.max(p.0.abs() + r);
251                hy = hy.max(p.1.abs() + r);
252            }
253        }
254    }
255    if style.page[3] > 0 {
256        let (w, h) = (style.w.value as f32, style.h.value as f32);
257        hx = hx.max(w.abs());
258        hy = hy.max(h.abs());
259    }
260    (hx.max(0.5), hy.max(0.5))
261}
262
263/// Closed outline of a filled shape in project px, centred at the origin. `s` only sets how finely
264/// curves are subdivided (so a big shape at a big scale stays smooth).
265fn shape_outline(style: &ShapeStyle, w: f32, h: f32, s: f32, out: &mut Vec<[f32; 2]>) {
266    out.clear();
267    match style.kind {
268        ShapeKind::Rect => {
269            let r = style.corner.max(0.0).min(w).min(h);
270            if r <= 0.01 {
271                out.extend_from_slice(&[[-w, -h], [w, -h], [w, h], [-w, h]]);
272            } else {
273                let n = arc_steps(r * s);
274                // corners clockwise from top-left, y down
275                for (i, (cx, cy)) in
276                    [(-w + r, -h + r), (w - r, -h + r), (w - r, h - r), (-w + r, h - r)].into_iter().enumerate()
277                {
278                    let a0 = std::f32::consts::PI * (1.0 + 0.5 * i as f32);
279                    for k in 0..=n {
280                        let a = a0 + std::f32::consts::FRAC_PI_2 * (k as f32 / n as f32);
281                        out.push([cx + r * a.cos(), cy + r * a.sin()]);
282                    }
283                }
284            }
285        }
286        ShapeKind::Ellipse => {
287            let n = arc_steps(w.max(h) * s) * 4;
288            for i in 0..n {
289                let a = std::f32::consts::TAU * (i as f32 / n as f32);
290                out.push([w * a.cos(), h * a.sin()]);
291            }
292        }
293        ShapeKind::Triangle => out.extend_from_slice(&[[0.0, -h], [w, h], [-w, h]]),
294        ShapeKind::Star => out.extend(ngon(style.sides, w, h, true)),
295        _ => match style.poly_points() {
296            Some(pts) => out.extend(pts.iter().map(|&(x, y)| [x, y])),
297            None => out.extend(ngon(style.sides, w, h, false)),
298        },
299    }
300}
301
302/// Regular polygon (or star: 2x the vertices, alternating radii) inscribed in the w x h ellipse,
303/// first vertex pointing up.
304fn ngon(sides: u32, w: f32, h: f32, star: bool) -> Vec<[f32; 2]> {
305    let n = sides.clamp(3, 64) as usize;
306    let count = if star { n * 2 } else { n };
307    (0..count)
308        .map(|i| {
309            let a = -std::f32::consts::FRAC_PI_2 + std::f32::consts::TAU * (i as f32 / count as f32);
310            let r = if star && i % 2 == 1 { STAR_INNER } else { 1.0 };
311            [w * r * a.cos(), h * r * a.sin()]
312        })
313        .collect()
314}
315
316/// Segments per quarter turn for a curve of pixel radius `r_px` (sagitta stays well under a pixel).
317fn arc_steps(r_px: f32) -> usize {
318    ((r_px.max(1.0) / 3.0) as usize).clamp(4, 64)
319}
320
321/// Points of a stroke revealed at `reveal` seconds (the last segment is cut proportionally).
322fn revealed(points: &[(f32, f32, f32)], reveal: f32, out: &mut Vec<[f32; 2]>) {
323    out.clear();
324    if points.is_empty() {
325        return;
326    }
327    if !reveal.is_finite() {
328        out.extend(points.iter().map(|p| [p.0, p.1]));
329        return;
330    }
331    for (i, p) in points.iter().enumerate() {
332        if p.2 <= reveal {
333            out.push([p.0, p.1]);
334            continue;
335        }
336        if i > 0 {
337            let q = points[i - 1];
338            let span = p.2 - q.2;
339            let f = if span > 0.0 { ((reveal - q.2) / span).clamp(0.0, 1.0) } else { 0.0 };
340            if f > 0.0 {
341                out.push([q.0 + (p.0 - q.0) * f, q.1 + (p.1 - q.1) * f]);
342            }
343        }
344        break;
345    }
346}
347
348fn to_layer(pts: &mut [[f32; 2]], cx: f32, cy: f32, s: f32) {
349    for p in pts {
350        p[0] = cx + p[0] * s;
351        p[1] = cy + p[1] * s;
352    }
353}
354
355/// Reveal bucket for the cache key: 0 = not a drawing, `u32::MAX` = fully revealed.
356fn reveal_bucket(style: &ShapeStyle, t: f64) -> u32 {
357    if style.kind != ShapeKind::Draw {
358        return 0;
359    }
360    let rate = style.draw_rate as f64;
361    if !(rate > 0.0) || !rate.is_finite() {
362        return u32::MAX;
363    }
364    let r = t.max(0.0) * rate;
365    if !r.is_finite() || r >= style.draw_duration() {
366        return u32::MAX;
367    }
368    ((r * REVEAL_HZ) as u32).min(u32::MAX - 1)
369}
370
371fn bucket_time(bucket: u32) -> f32 {
372    if bucket == u32::MAX {
373        f32::INFINITY
374    } else {
375        (bucket as f64 / REVEAL_HZ) as f32
376    }
377}
378
379// ---------- polygon soup ----------
380
381/// `pts[ends[i-1]..ends[i]]` is one closed subpath, all wound the same way so a nonzero fill
382/// unions overlapping pieces instead of cancelling them.
383#[derive(Default)]
384struct Path {
385    pts: Vec<[f32; 2]>,
386    ends: Vec<usize>,
387}
388
389impl Path {
390    fn clear(&mut self) {
391        self.pts.clear();
392        self.ends.clear();
393    }
394    fn push(&mut self, p: [f32; 2]) {
395        self.pts.push(p);
396    }
397    /// Close the subpath started after the previous one, dropping degenerate ones.
398    fn end(&mut self) {
399        let start = self.ends.last().copied().unwrap_or(0);
400        if self.pts.len() < start + 3 {
401            self.pts.truncate(start);
402            return;
403        }
404        let seg = &mut self.pts[start..];
405        let mut area = 0.0f32;
406        for i in 0..seg.len() {
407            let a = seg[i];
408            let b = seg[(i + 1) % seg.len()];
409            area += a[0] * b[1] - b[0] * a[1];
410        }
411        if area < 0.0 {
412            seg.reverse();
413        }
414        self.ends.push(self.pts.len());
415    }
416}
417
418fn add_disc(path: &mut Path, c: [f32; 2], r: f32) {
419    let n = ((r * 2.0) as usize).clamp(6, 40);
420    for i in 0..n {
421        let a = std::f32::consts::TAU * (i as f32 / n as f32);
422        path.push([c[0] + r * a.cos(), c[1] + r * a.sin()]);
423    }
424    path.end();
425}
426
427/// Thick polyline in layer px: one quad per segment plus a disc at every vertex (round caps/joins).
428fn add_stroke(path: &mut Path, pts: &[[f32; 2]], closed: bool, half: f32) {
429    let half = half.max(0.35);
430    let n = pts.len();
431    if n == 0 {
432        return;
433    }
434    let segs = if closed { n } else { n.saturating_sub(1) };
435    for i in 0..segs {
436        let a = pts[i];
437        let b = pts[(i + 1) % n];
438        let (dx, dy) = (b[0] - a[0], b[1] - a[1]);
439        let len = (dx * dx + dy * dy).sqrt();
440        if !(len > 1e-5) {
441            continue;
442        }
443        let (nx, ny) = (-dy / len * half, dx / len * half);
444        path.push([a[0] + nx, a[1] + ny]);
445        path.push([b[0] + nx, b[1] + ny]);
446        path.push([b[0] - nx, b[1] - ny]);
447        path.push([a[0] - nx, a[1] - ny]);
448        path.end();
449    }
450    if half > 0.6 || segs == 0 {
451        for p in pts {
452            add_disc(path, *p, half);
453        }
454    }
455}
456
457// ---------- coverage rasteriser ----------
458
459/// A non-horizontal edge, top to bottom.
460struct Edge {
461    y0: f32,
462    y1: f32,
463    x0: f32,
464    dxdy: f32,
465    dir: i32,
466}
467
468/// Scanline fill with 4x vertical sub-sampling and exact horizontal coverage. Writes `cov`
469/// (zeroing only the band it touches) and returns that band as (x0, y0, x1, y1).
470fn fill_path(
471    path: &Path,
472    w: usize,
473    h: usize,
474    cov: &mut Vec<f32>,
475    edges: &mut Vec<Edge>,
476    active: &mut Vec<usize>,
477    xs: &mut Vec<(f32, i32)>,
478) -> Option<(usize, usize, usize, usize)> {
479    if path.ends.is_empty() || w == 0 || h == 0 {
480        return None;
481    }
482    edges.clear();
483    let (mut mnx, mut mny, mut mxx, mut mxy) = (f32::MAX, f32::MAX, f32::MIN, f32::MIN);
484    let mut start = 0;
485    for &end in &path.ends {
486        let sub = &path.pts[start..end];
487        for i in 0..sub.len() {
488            let a = sub[i];
489            let b = sub[(i + 1) % sub.len()];
490            if !(a[0].is_finite() && a[1].is_finite() && b[0].is_finite() && b[1].is_finite()) {
491                continue;
492            }
493            mnx = mnx.min(a[0]);
494            mxx = mxx.max(a[0]);
495            mny = mny.min(a[1]);
496            mxy = mxy.max(a[1]);
497            if a[1] == b[1] {
498                continue;
499            }
500            let (top, bot, dir) = if a[1] < b[1] { (a, b, 1) } else { (b, a, -1) };
501            edges.push(Edge { y0: top[1], y1: bot[1], x0: top[0], dxdy: (bot[0] - top[0]) / (bot[1] - top[1]), dir });
502        }
503        start = end;
504    }
505    if edges.is_empty() || mnx > mxx {
506        return None;
507    }
508    let x0 = (mnx.floor().max(0.0) as usize).min(w);
509    let x1 = ((mxx.ceil().max(0.0) as usize) + 1).min(w);
510    let y0 = (mny.floor().max(0.0) as usize).min(h);
511    let y1 = ((mxy.ceil().max(0.0) as usize) + 1).min(h);
512    if x1 <= x0 || y1 <= y0 {
513        return None;
514    }
515    if cov.len() < w * h {
516        cov.resize(w * h, 0.0);
517    }
518    for y in y0..y1 {
519        cov[y * w + x0..y * w + x1].fill(0.0);
520    }
521    edges.sort_by(|a, b| a.y0.partial_cmp(&b.y0).unwrap_or(std::cmp::Ordering::Equal));
522    active.clear();
523    let mut cursor = 0usize;
524    let amt = 1.0 / SUB as f32;
525    for y in y0..y1 {
526        for s in 0..SUB {
527            let sy = y as f32 + (s as f32 + 0.5) / SUB as f32;
528            while cursor < edges.len() && edges[cursor].y0 <= sy {
529                active.push(cursor);
530                cursor += 1;
531            }
532            active.retain(|&i| edges[i].y1 > sy);
533            if active.len() < 2 {
534                continue;
535            }
536            xs.clear();
537            for &i in active.iter() {
538                let e = &edges[i];
539                if e.y0 <= sy {
540                    xs.push((e.x0 + (sy - e.y0) * e.dxdy, e.dir));
541                }
542            }
543            if xs.len() < 2 {
544                continue;
545            }
546            xs.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
547            let row = &mut cov[y * w..y * w + w];
548            let mut wind = 0;
549            for i in 0..xs.len() - 1 {
550                wind += xs[i].1;
551                if wind != 0 {
552                    add_span(row, xs[i].0, xs[i + 1].0, amt, x0, x1);
553                }
554            }
555        }
556    }
557    Some((x0, y0, x1, y1))
558}
559
560fn add_span(row: &mut [f32], a: f32, b: f32, amt: f32, lo: usize, hi: usize) {
561    let a = a.max(lo as f32);
562    let b = b.min(hi as f32);
563    if !(b > a) {
564        return;
565    }
566    let i0 = a.floor() as usize;
567    let i1 = (b.ceil() as usize).min(hi);
568    if i1 <= i0 || i0 >= hi {
569        return;
570    }
571    if i1 - i0 == 1 {
572        row[i0] += (b - a) * amt;
573        return;
574    }
575    row[i0] += ((i0 + 1) as f32 - a) * amt;
576    for c in &mut row[i0 + 1..i1 - 1] {
577        *c += amt;
578    }
579    row[i1 - 1] += (b - (i1 - 1) as f32) * amt;
580}
581
582/// Source-over in straight (non-premultiplied) alpha.
583fn blend(px: &mut [u8], color: [u8; 4], cov: f32) {
584    let sa = color[3] as f32 / 255.0 * cov.clamp(0.0, 1.0);
585    if sa <= 0.0 {
586        return;
587    }
588    let da = px[3] as f32 / 255.0;
589    let oa = sa + da * (1.0 - sa);
590    if oa <= 0.0 {
591        return;
592    }
593    for i in 0..3 {
594        let v = (color[i] as f32 * sa + px[i] as f32 * da * (1.0 - sa)) / oa;
595        px[i] = v.round().clamp(0.0, 255.0) as u8;
596    }
597    px[3] = (oa * 255.0).round().clamp(0.0, 255.0) as u8;
598}
599
600#[cfg(test)]
601mod tests {
602    use super::*;
603    use crate::model::{Animated, Stroke};
604
605    fn style(kind: ShapeKind, w: f32, h: f32) -> ShapeStyle {
606        ShapeStyle {
607            w: Animated::new(w as f64),
608            h: Animated::new(h as f64),
609            stroke: [0, 0, 0, 0],
610            ..ShapeStyle::new(kind)
611        }
612    }
613    fn px(f: &Frame, x: u32, y: u32) -> [u8; 4] {
614        let i = ((y * f.width + x) * 4) as usize;
615        [f.rgba[i], f.rgba[i + 1], f.rgba[i + 2], f.rgba[i + 3]]
616    }
617
618    #[test]
619    fn rect_fills_its_tight_layer() {
620        let mut r = ShapeRasterizer::new();
621        let s = style(ShapeKind::Rect, 50.0, 30.0);
622        assert_eq!(ShapeRasterizer::size(&s, 0.0), (100.0, 60.0));
623        let f = r.render(&s, 1.0, 0.0);
624        assert_eq!((f.width, f.height), (100, 60));
625        assert_eq!(px(&f, 50, 30), [255, 255, 255, 255], "middle must be opaque fill");
626        assert_eq!(px(&f, 0, 0), [255, 255, 255, 255], "a tight rect covers its whole layer");
627    }
628
629    #[test]
630    fn ellipse_is_transparent_outside() {
631        let mut r = ShapeRasterizer::new();
632        let f = r.render(&style(ShapeKind::Ellipse, 50.0, 30.0), 1.0, 0.0);
633        assert_eq!((f.width, f.height), (100, 60));
634        assert_eq!(px(&f, 50, 30)[3], 255, "centre filled");
635        assert_eq!(px(&f, 0, 0)[3], 0, "corner outside the ellipse");
636        assert_eq!(px(&f, 99, 59)[3], 0, "corner outside the ellipse");
637        // the extreme points of the ellipse are covered
638        assert!(px(&f, 50, 1)[3] > 100, "top of the ellipse: {:?}", px(&f, 50, 1));
639    }
640
641    #[test]
642    fn rounded_rect_clears_its_corners() {
643        let mut r = ShapeRasterizer::new();
644        let mut s = style(ShapeKind::Rect, 50.0, 30.0);
645        s.corner = 20.0;
646        let f = r.render(&s, 1.0, 0.0);
647        assert_eq!(px(&f, 1, 1)[3], 0, "rounded corner must be transparent");
648        assert_eq!(px(&f, 50, 30)[3], 255);
649    }
650
651    #[test]
652    fn stroke_only_shape_is_hollow() {
653        let mut r = ShapeRasterizer::new();
654        let mut s = style(ShapeKind::Rect, 50.0, 30.0);
655        s.fill = [0, 0, 0, 0];
656        s.stroke = [255, 0, 0, 255];
657        s.stroke_width = 6.0;
658        // the layer grows by the stroke width (half sticks out on each side)
659        assert_eq!(ShapeRasterizer::size(&s, 0.0), (106.0, 66.0));
660        let f = r.render(&s, 1.0, 0.0);
661        assert_eq!(px(&f, 53, 33)[3], 0, "centre must be hollow");
662        assert_eq!(px(&f, 53, 1), [255, 0, 0, 255], "top edge is stroked");
663        assert_eq!(px(&f, 1, 33), [255, 0, 0, 255], "left edge is stroked");
664    }
665
666    #[test]
667    fn scale_changes_the_pixel_size_only() {
668        let mut r = ShapeRasterizer::new();
669        let s = style(ShapeKind::Rect, 50.0, 30.0);
670        let f = r.render(&s, 2.0, 0.0);
671        assert_eq!((f.width, f.height), (200, 120));
672        assert_eq!(px(&f, 100, 60)[3], 255);
673    }
674
675    #[test]
676    fn draw_reveals_progressively() {
677        let mut r = ShapeRasterizer::new();
678        let mut s = style(ShapeKind::Draw, 0.0, 0.0);
679        s.strokes =
680            vec![Stroke { color: [0, 255, 0, 255], width: 8.0, points: vec![(-100.0, 0.0, 0.0), (100.0, 0.0, 1.0)] }];
681        s.draw_rate = 1.0;
682        assert_eq!(ShapeRasterizer::size(&s, 0.0), (208.0, 8.0));
683        let mid = r.render(&s, 1.0, 0.5);
684        let cy = mid.height / 2;
685        assert!(px(&mid, 20, cy)[3] > 200, "start of the stroke is drawn: {:?}", px(&mid, 20, cy));
686        assert!(px(&mid, 104, cy)[3] > 200, "halfway point is drawn: {:?}", px(&mid, 104, cy));
687        assert_eq!(px(&mid, 180, cy)[3], 0, "the tail is not revealed yet");
688        // draw_rate 0 = the whole sketch at once, at any time
689        s.draw_rate = 0.0;
690        let all = r.render(&s, 1.0, 0.0);
691        assert!(px(&all, 180, cy)[3] > 200, "rate 0 draws everything: {:?}", px(&all, 180, cy));
692        assert!(px(&all, 20, cy)[3] > 200);
693    }
694
695    #[test]
696    fn draw_page_paints_the_background() {
697        let mut r = ShapeRasterizer::new();
698        let mut s = style(ShapeKind::Draw, 60.0, 40.0);
699        s.page = [10, 20, 30, 255];
700        s.strokes = vec![Stroke { color: [255, 0, 0, 255], width: 4.0, points: vec![(0.0, 0.0, 0.0)] }];
701        let f = r.render(&s, 1.0, 0.0);
702        assert_eq!((f.width, f.height), (120, 80));
703        assert_eq!(px(&f, 2, 2), [10, 20, 30, 255], "page fills the layer");
704        assert_eq!(px(&f, 60, 40), [255, 0, 0, 255], "the dot sits on the page");
705    }
706
707    #[test]
708    fn line_and_arrow_are_drawn() {
709        let mut r = ShapeRasterizer::new();
710        let mut s = style(ShapeKind::Line, 40.0, 0.0);
711        s.stroke = [255, 255, 255, 255];
712        s.stroke_width = 4.0;
713        let f = r.render(&s, 1.0, 0.0);
714        let (cx, cy) = (f.width / 2, f.height / 2);
715        assert!(px(&f, cx, cy)[3] > 200, "the line crosses the centre");
716        let mut a = style(ShapeKind::Arrow, 40.0, 0.0);
717        a.stroke = [255, 255, 255, 255];
718        a.stroke_width = 4.0;
719        a.corner = 16.0;
720        // head is 16 px long, so the (symmetric) layer is padded by it
721        assert_eq!(ShapeRasterizer::size(&a, 0.0), (96.0, 16.0));
722        let g = r.render(&a, 1.0, 0.0);
723        assert!(px(&g, 84, 8)[3] > 200, "near the tip: {:?}", px(&g, 84, 8));
724        assert!(px(&g, 76, 4)[3] > 200, "the head flares wider than the shaft: {:?}", px(&g, 76, 4));
725        assert_eq!(px(&g, 20, 4)[3], 0, "the shaft is thin away from the head");
726    }
727
728    /// A REAL pixel check of the diagonal case (h != 0), not just the geometry that feeds it: a line
729    /// dragged up-and-right (w > 0, h < 0, since screen y grows downward) must actually cover pixels
730    /// near the bottom-left and top-right of its layer and be empty near the other two corners, and the
731    /// opposite diagonal (w > 0, h > 0) must do the reverse.
732    #[test]
733    fn diagonal_line_covers_the_right_corners() {
734        let mut r = ShapeRasterizer::new();
735        let mut up_right = style(ShapeKind::Line, 40.0, -30.0);
736        up_right.stroke = [255, 255, 255, 255];
737        up_right.stroke_width = 4.0;
738        let f = r.render(&up_right, 1.0, 0.0);
739        let (w, h) = (f.width, f.height);
740        assert!(px(&f, 3, h - 3)[3] > 150, "bottom-left must be covered: {:?}", px(&f, 3, h - 3));
741        assert!(px(&f, w - 3, 3)[3] > 150, "top-right must be covered: {:?}", px(&f, w - 3, 3));
742        assert_eq!(px(&f, 3, 3)[3], 0, "top-left must be empty: {:?}", px(&f, 3, 3));
743        assert_eq!(px(&f, w - 3, h - 3)[3], 0, "bottom-right must be empty: {:?}", px(&f, w - 3, h - 3));
744
745        let mut down_right = style(ShapeKind::Line, 40.0, 30.0);
746        down_right.stroke = [255, 255, 255, 255];
747        down_right.stroke_width = 4.0;
748        let g = r.render(&down_right, 1.0, 0.0);
749        assert!(px(&g, 3, 3)[3] > 150, "top-left must be covered: {:?}", px(&g, 3, 3));
750        assert!(px(&g, w - 3, h - 3)[3] > 150, "bottom-right must be covered: {:?}", px(&g, w - 3, h - 3));
751        assert_eq!(px(&g, w - 3, 3)[3], 0, "top-right must be empty: {:?}", px(&g, w - 3, 3));
752        assert_eq!(px(&g, 3, h - 3)[3], 0, "bottom-left must be empty: {:?}", px(&g, 3, h - 3));
753    }
754
755    #[test]
756    fn ngon_vertex_counts() {
757        assert_eq!(ngon(6, 10.0, 10.0, false).len(), 6);
758        assert_eq!(ngon(3, 10.0, 10.0, false).len(), 3);
759        assert_eq!(ngon(5, 10.0, 10.0, true).len(), 10, "a star has two vertices per side");
760        assert_eq!(ngon(8, 10.0, 10.0, true).len(), 16);
761        assert_eq!(ngon(1, 10.0, 10.0, false).len(), 3, "sides are clamped to a triangle");
762        assert_eq!(ngon(999, 10.0, 10.0, false).len(), 64, "and to 64");
763        // first vertex points up (negative y)
764        let p = ngon(5, 10.0, 10.0, false)[0];
765        assert!(p[1] < -9.0, "{p:?}");
766    }
767
768    #[test]
769    fn star_and_polygon_render_inside_the_layer() {
770        let mut r = ShapeRasterizer::new();
771        for kind in [ShapeKind::Star, ShapeKind::Polygon, ShapeKind::Triangle] {
772            let f = r.render(&style(kind, 50.0, 50.0), 1.0, 0.0);
773            assert_eq!((f.width, f.height), (100, 100), "{kind:?}");
774            assert_eq!(px(&f, 50, 55)[3], 255, "{kind:?} centre filled");
775            assert_eq!(px(&f, 2, 2)[3], 0, "{kind:?} top-left corner is empty");
776        }
777    }
778
779    #[test]
780    fn explicit_points_draw_the_polygon() {
781        let mut r = ShapeRasterizer::new();
782        // Triangle's outline is exactly [(0,-h), (w,h), (-w,h)] — the same vertices by hand must give
783        // the same pixels, not the regular pentagon `sides` asks for
784        let regular = style(ShapeKind::Triangle, 50.0, 30.0);
785        let mut poly = style(ShapeKind::Polygon, 50.0, 30.0);
786        poly.points = vec![(0.0, -30.0), (50.0, 30.0), (-50.0, 30.0)];
787        assert_eq!(ShapeRasterizer::size(&poly, 0.0), ShapeRasterizer::size(&regular, 0.0));
788        let a = r.render(&regular, 1.0, 0.0);
789        let b = r.render(&poly, 1.0, 0.0);
790        assert_eq!((b.width, b.height), (a.width, a.height));
791        assert_eq!(b.rgba, a.rgba, "an explicit triangle covers what the regular one does");
792        // the layer is sized from the points, so w/h no longer bound them
793        let mut wrong_size = poly.clone();
794        wrong_size.w = Animated::new(5.0);
795        wrong_size.h = Animated::new(5.0);
796        assert_eq!(ShapeRasterizer::size(&wrong_size, 0.0), (100.0, 60.0));
797        // fewer than 3 points cannot enclose anything: back to the n-gon
798        poly.points.truncate(2);
799        assert!(!Arc::ptr_eq(&r.render(&poly, 1.0, 0.0), &b));
800    }
801
802    #[test]
803    fn identical_requests_hit_the_cache() {
804        let mut r = ShapeRasterizer::new();
805        let s = style(ShapeKind::Ellipse, 40.0, 40.0);
806        let a = r.render(&s, 1.0, 0.0);
807        let b = r.render(&s, 1.0, 0.0);
808        assert!(Arc::ptr_eq(&a, &b), "cache hit expected");
809        let c = r.render(&s, 2.0, 0.0);
810        assert!(!Arc::ptr_eq(&a, &c), "a different scale is a different layer");
811    }
812
813    #[test]
814    fn draw_reveal_buckets_share_cache_entries() {
815        let mut r = ShapeRasterizer::new();
816        let mut s = style(ShapeKind::Draw, 0.0, 0.0);
817        s.strokes =
818            vec![Stroke { color: [255, 255, 255, 255], width: 4.0, points: vec![(0.0, 0.0, 0.0), (50.0, 0.0, 2.0)] }];
819        let a = r.render(&s, 1.0, 0.5);
820        let b = r.render(&s, 1.0, 0.51); // same 1/30 s bucket
821        assert!(Arc::ptr_eq(&a, &b), "reveal is bucketed for the cache");
822        let c = r.render(&s, 1.0, 1.0);
823        assert!(!Arc::ptr_eq(&a, &c));
824        // past the end everything collapses onto one fully-revealed entry
825        let d = r.render(&s, 1.0, 9.0);
826        let e = r.render(&s, 1.0, 99.0);
827        assert!(Arc::ptr_eq(&d, &e), "finished drawings share one entry");
828    }
829
830    #[test]
831    fn degenerate_styles_never_panic() {
832        let mut r = ShapeRasterizer::new();
833        for kind in ShapeKind::ALL {
834            let mut s = style(kind, 0.0, 0.0);
835            s.stroke_width = 0.0;
836            s.sides = 0;
837            let f = r.render(&s, 0.0, -5.0);
838            assert!(f.width >= 1 && f.height >= 1, "{kind:?}");
839            let mut odd = style(kind, f32::INFINITY, f32::NAN);
840            odd.stroke = [255, 255, 255, 255];
841            odd.corner = -3.0;
842            let g = r.render(&odd, 3.0, f64::NAN);
843            assert!(g.width >= 1 && g.height >= 1, "{kind:?}");
844        }
845    }
846
847    #[test]
848    fn oversized_layers_are_clamped() {
849        let mut r = ShapeRasterizer::new();
850        let s = style(ShapeKind::Rect, 100_000.0, 1.0);
851        // half-size is capped at 20000 project px, then the layer is capped at MAX_DIM
852        assert_eq!(ShapeRasterizer::size(&s, 0.0), (40_000.0, 2.0));
853        let f = r.render(&s, 1.0, 0.0);
854        assert_eq!(f.width, MAX_DIM as u32);
855        assert!(f.height >= 1 && f.height < 4);
856    }
857
858    #[test]
859    fn revealed_cuts_the_last_segment() {
860        let pts = [(0.0f32, 0.0f32, 0.0f32), (100.0, 0.0, 1.0)];
861        let mut out = Vec::new();
862        revealed(&pts, 0.5, &mut out);
863        assert_eq!(out, vec![[0.0, 0.0], [50.0, 0.0]]);
864        revealed(&pts, f32::INFINITY, &mut out);
865        assert_eq!(out, vec![[0.0, 0.0], [100.0, 0.0]]);
866        revealed(&pts, -1.0, &mut out);
867        assert!(out.is_empty(), "nothing drawn before the first point");
868    }
869}