//! Code-drawn test scenes with exact answers: centre lines with widths (round caps), filled
//! areas, junctions with their degrees, and corners (stroke ends that meet but are no junction).
use super::centre_geom::{P, seg_dist};
#[derive(Clone, Debug)]
pub struct GtStroke {
/// Dense centre line (≤ 0.25 px apart).
pub pts: Vec
,
pub w: Vec,
pub closed: bool,
}
#[derive(Clone, Debug)]
pub enum GtFill {
Disc(P, f64),
Poly(Vec),
}
impl GtFill {
pub fn inside(&self, q: P) -> bool {
match self {
GtFill::Disc(c, r) => q.dist(*c) <= *r,
GtFill::Poly(p) => {
let n = p.len();
let mut inside = false;
for i in 0..n {
let (a, b) = (p[i], p[(i + 1) % n]);
if (a.y > q.y) != (b.y > q.y) && q.x < a.x + (q.y - a.y) / (b.y - a.y) * (b.x - a.x) {
inside = !inside;
}
}
inside
}
}
}
/// Distance outside (0 inside), approximate for polygons (to the edges).
pub fn dist(&self, q: P) -> f64 {
if self.inside(q) {
return 0.0;
}
match self {
GtFill::Disc(c, r) => q.dist(*c) - r,
GtFill::Poly(p) => {
let n = p.len();
(0..n).map(|i| seg_dist(q, p[i], p[(i + 1) % n]).0).fold(f64::MAX, f64::min)
}
}
}
}
#[derive(Clone, Debug)]
pub struct Scene {
pub name: String,
pub w: usize,
pub h: usize,
pub strokes: Vec,
pub fills: Vec,
pub junctions: Vec<(P, usize)>,
pub corners: Vec,
pub wmax: f64,
pub min_len: f64,
/// Added to every coordinate (odd widths at integer positions put pixel centres exactly on
/// the edge: ties that make coverage numbers meaningless).
pub offset: P,
}
impl Scene {
fn new(name: &str, w: usize, h: usize, wmax: f64) -> Self {
Self {
name: name.into(),
w,
h,
strokes: vec![],
fills: vec![],
junctions: vec![],
corners: vec![],
wmax,
min_len: 20.0,
offset: P::new(0.0, 0.0),
}
}
/// A polyline stroke of constant width.
fn line(&mut self, pts: &[P], w: f64) {
self.var(pts, &vec![w; pts.len()], false);
}
/// A polyline stroke with a width at each vertex (linear between), densified.
fn var(&mut self, pts: &[P], ws: &[f64], closed: bool) {
let pts: Vec
= pts.iter().map(|&p| p + self.offset).collect();
let pts = &pts[..];
let mut d = vec![];
let mut dw = vec![];
let n = pts.len();
let segs = if closed { n } else { n.saturating_sub(1) };
for i in 0..segs {
let (a, b) = (pts[i], pts[(i + 1) % n]);
let (wa, wb) = (ws[i], ws[(i + 1) % n]);
let k = ((a.dist(b) / 0.25).ceil() as usize).max(1);
for j in 0..k {
let t = j as f64 / k as f64;
d.push(a.lerp(b, t));
dw.push(wa + (wb - wa) * t);
}
}
if !closed && let (Some(&l), Some(&lw)) = (pts.last(), ws.last()) {
d.push(l);
dw.push(lw);
}
self.strokes.push(GtStroke { pts: d, w: dw, closed });
}
fn curve(&mut self, f: impl Fn(f64) -> P, wf: impl Fn(f64) -> f64, n: usize, closed: bool) {
let m = if closed { n } else { n + 1 };
let pts: Vec
= (0..m).map(|k| f(k as f64 / n as f64)).collect();
let ws: Vec = (0..m).map(|k| wf(k as f64 / n as f64)).collect();
self.var(&pts, &ws, closed);
}
fn arc(&mut self, c: P, r: f64, a0: f64, a1: f64, w: f64) {
let n = ((r * (a1 - a0).abs() / 0.5).ceil() as usize).max(8);
self.curve(
|t| {
let a = a0 + (a1 - a0) * t;
c + P::new(r * a.cos(), r * a.sin())
},
|_| w,
n,
false,
);
}
fn ring(&mut self, c: P, r: f64, w: f64) {
let n = ((r * std::f64::consts::TAU / 0.5).ceil() as usize).max(16);
self.curve(
|t| {
let a = std::f64::consts::TAU * t;
c + P::new(r * a.cos(), r * a.sin())
},
|_| w,
n,
true,
);
}
/// Ink coverage (8 × 8 samples per pixel).
pub fn rasterise(&self) -> Vec {
self.rasterise_at(8)
}
/// Exact shape point-sampled at pixel centres (the reference for coverage).
pub fn exact(&self) -> Vec {
self.rasterise_at(1).into_iter().map(|c| c >= 1.0).collect()
}
fn rasterise_at(&self, ss: usize) -> Vec {
let (sw, sh) = (self.w * ss, self.h * ss);
let mut sub = vec![false; sw * sh];
let at = |x: usize, y: usize| P::new((x as f64 + 0.5) / ss as f64, (y as f64 + 0.5) / ss as f64);
for s in &self.strokes {
let n = s.pts.len();
let segs = if s.closed { n } else { n.saturating_sub(1) };
let mut stamp = |a: P, b: P, ra: f64, rb: f64| {
let r = ra.max(rb);
let x0 = (((a.x.min(b.x) - r) * ss as f64).floor().max(0.0)) as usize;
let y0 = (((a.y.min(b.y) - r) * ss as f64).floor().max(0.0)) as usize;
let x1 = ((((a.x.max(b.x) + r) * ss as f64).ceil().max(0.0)) as usize).min(sw);
let y1 = ((((a.y.max(b.y) + r) * ss as f64).ceil().max(0.0)) as usize).min(sh);
for y in y0..y1 {
for x in x0..x1 {
let q = at(x, y);
let (d, t) = seg_dist(q, a, b);
if d <= ra + (rb - ra) * t {
sub[y * sw + x] = true;
}
}
}
};
if n == 1 {
stamp(s.pts[0], s.pts[0], s.w[0] / 2.0, s.w[0] / 2.0);
}
for i in 0..segs {
let j = (i + 1) % n;
stamp(s.pts[i], s.pts[j], s.w[i] / 2.0, s.w[j] / 2.0);
}
}
for f in &self.fills {
for y in 0..sh {
for x in 0..sw {
if !sub[y * sw + x] && f.inside(at(x, y)) {
sub[y * sw + x] = true;
}
}
}
}
let mut out = vec![0.0f32; self.w * self.h];
for y in 0..self.h {
for x in 0..self.w {
let mut c = 0;
for yy in 0..ss {
for xx in 0..ss {
c += usize::from(sub[(y * ss + yy) * sw + x * ss + xx]);
}
}
out[y * self.w + x] = c as f32 / (ss * ss) as f32;
}
}
out
}
}
fn pol(c: P, r: f64, deg: f64) -> P {
let a = deg.to_radians();
c + P::new(r * a.cos(), r * a.sin())
}
/// 24 straight lines of width `w`, at 0°–172.5° in 7.5° steps, centres at varied sub-pixel phases.
pub fn lines(w: f64) -> Scene {
let mut s = Scene::new(&format!("lines w{w}"), 480, 320, 13.0);
for k in 0..24 {
let (cx, cy) = ((k % 6) as f64 * 80.0 + 40.0 + 0.13 * k as f64 % 1.0, (k / 6) as f64 * 80.0 + 40.0 + 0.29 * k as f64 % 1.0);
let c = P::new(cx, cy);
let a = 7.5 * k as f64;
s.line(&[pol(c, 25.0, a + 180.0), pol(c, 25.0, a)], w);
}
s
}
pub fn curves(w: f64) -> Scene {
let mut s = Scene::new(&format!("curves w{w}"), 400, 300, 13.0);
// Sine waves.
s.curve(|t| P::new(30.0 + 340.0 * t, 50.0 + 20.0 * (t * 12.0).sin()), |_| w, 400, false);
s.curve(|t| P::new(30.0 + 340.0 * t, 110.0 + 10.0 * (t * 25.0).sin()), |_| w, 600, false);
// Cubic S-curve.
let cubic = |t: f64| {
let (p0, p1, p2, p3) = (P::new(40.0, 280.0), P::new(60.0, 140.0), P::new(200.0, 300.0), P::new(190.0, 160.0));
let u = 1.0 - t;
p0 * (u * u * u) + p1 * (3.0 * u * u * t) + p2 * (3.0 * u * t * t) + p3 * (t * t * t)
};
s.curve(cubic, |_| w, 300, false);
// Spiral.
s.curve(
|t| {
let a = t * 4.0 * std::f64::consts::PI;
P::new(300.0, 225.0) + P::new(a.cos(), a.sin()) * (12.0 + 50.0 * t)
},
|_| w,
800,
false,
);
s
}
pub fn circles() -> Scene {
let mut s = Scene::new("circles", 520, 260, 13.0);
let mut x = 50.0;
for (r, w) in [(8.0, 1.0), (12.0, 2.0), (18.0, 3.0), (25.0, 4.0), (35.0, 6.0), (45.0, 8.0)] {
s.ring(P::new(x, 70.0 + 0.3 * r % 1.0), r, w);
s.ring(P::new(x + 10.0, 190.0), r * 1.2, (w * 0.5_f64).max(1.0));
x += 2.0 * r + 35.0;
}
s
}
pub fn junctions(w: f64) -> Scene {
let mut s = Scene::new(&format!("junctions w{w}"), 520, 220, 13.0);
s.offset = P::new(0.27, 0.41);
// T.
s.line(&[P::new(20.0, 40.0), P::new(100.0, 40.0)], w);
s.line(&[P::new(60.0, 40.0), P::new(60.0, 100.0)], w);
s.junctions.push((P::new(60.0, 40.0), 3));
// Y (120°).
let c = P::new(180.0, 70.0);
for a in [-90.0, 30.0, 150.0] {
s.line(&[c, pol(c, 40.0, a)], w);
}
s.junctions.push((c, 3));
// X (90°).
let c = P::new(300.0, 70.0);
s.line(&[pol(c, 40.0, 45.0), pol(c, 40.0, 225.0)], w);
s.line(&[pol(c, 40.0, 135.0), pol(c, 40.0, 315.0)], w);
s.junctions.push((c, 4));
// Oblique T (stem at 50°).
s.line(&[P::new(380.0, 40.0), P::new(500.0, 40.0)], w);
s.line(&[P::new(430.0, 40.0), pol(P::new(430.0, 40.0), 70.0, 50.0)], w);
s.junctions.push((P::new(430.0, 40.0), 3));
// Thin stem on a thick bar.
let bar = (w * 2.0).min(12.0);
s.line(&[P::new(20.0, 160.0), P::new(140.0, 160.0)], bar);
s.line(&[P::new(80.0, 160.0), P::new(80.0, 215.0)], (w * 0.75).max(1.0));
s.junctions.push((P::new(80.0, 160.0), 3));
// Narrow Y (arms 40° apart).
let c = P::new(230.0, 150.0);
s.line(&[c, pol(c, 60.0, 180.0)], w);
s.line(&[c, pol(c, 50.0, -20.0)], w);
s.line(&[c, pol(c, 50.0, 20.0)], w);
s.junctions.push((c, 3));
// Asterisk-free "K": arms meeting the stem at one point (degree 4).
let c = P::new(400.0, 165.0);
s.line(&[P::new(400.0, 120.0), P::new(400.0, 210.0)], w);
s.line(&[c, pol(c, 50.0, -40.0)], w);
s.line(&[c, pol(c, 50.0, 40.0)], w);
s.junctions.push((c, 4));
shifted(s)
}
pub fn crossings(w: f64) -> Scene {
let mut s = Scene::new(&format!("crossings w{w}"), 560, 140, 13.0);
s.offset = P::new(0.27, 0.41);
for (k, ang) in [20.0, 30.0, 45.0, 60.0, 90.0].into_iter().enumerate() {
let c = P::new(60.0 + 110.0 * k as f64, 70.0);
s.line(&[pol(c, 50.0, 180.0 - ang / 2.0), pol(c, 50.0, -ang / 2.0)], w);
s.line(&[pol(c, 50.0, 180.0 + ang / 2.0), pol(c, 50.0, ang / 2.0)], w);
s.junctions.push((c, 4));
}
shifted(s)
}
pub fn dashed(w: f64) -> Scene {
let mut s = Scene::new(&format!("dashed w{w}"), 400, 260, 13.0);
s.min_len = 0.0;
for (k, (dash, gap, ang)) in [(12.0, 4.0, 0.0), (6.0, 3.0, 15.0), (10.0, 4.0, 37.0), (16.0, 6.0, 90.0), (8.0, 5.0, 135.0)].into_iter().enumerate()
{
let c = P::new(80.0 + 120.0 * (k % 3) as f64, 70.0 + 120.0 * (k / 3) as f64);
let a: f64 = f64::to_radians(ang);
let dir = P::new(a.cos(), a.sin());
let mut t = -50.0;
while t + dash <= 50.0 {
s.line(&[c + dir * t, c + dir * (t + dash)], w);
// The visible gap is `gap`: the round caps reach w/2 past each dash's ends.
t += dash + gap + w;
}
}
s
}
/// Letter-like strokes drawn from lines and arcs (no fonts).
pub fn letters(w: f64) -> Scene {
let mut s = Scene::new(&format!("letters w{w}"), 520, 120, 13.0);
s.offset = P::new(0.27, 0.41);
// A: two legs meeting at the apex (a corner), crossbar (two T's).
let (l, apex, r) = (P::new(20.0, 100.0), P::new(50.0, 20.0), P::new(80.0, 100.0));
s.line(&[l, apex, r], w);
let (bl, br) = (l.lerp(apex, 0.4), r.lerp(apex, 0.4));
s.line(&[bl, br], w);
s.junctions.push((bl, 3));
s.junctions.push((br, 3));
s.corners.push(apex);
// H.
s.line(&[P::new(110.0, 20.0), P::new(110.0, 100.0)], w);
s.line(&[P::new(160.0, 20.0), P::new(160.0, 100.0)], w);
s.line(&[P::new(110.0, 60.0), P::new(160.0, 60.0)], w);
s.junctions.push((P::new(110.0, 60.0), 3));
s.junctions.push((P::new(160.0, 60.0), 3));
// e: bar across a circle that opens at the lower right.
let c = P::new(215.0, 65.0);
s.arc(c, 25.0, 0.0, -std::f64::consts::PI * 1.75, w);
s.line(&[pol(c, 25.0, 180.0), pol(c, 25.0, 0.0)], w);
s.junctions.push((pol(c, 25.0, 180.0), 3));
s.corners.push(pol(c, 25.0, 0.0));
// k.
let c = P::new(275.0, 75.0);
s.line(&[P::new(275.0, 15.0), P::new(275.0, 105.0)], w);
s.line(&[c, P::new(310.0, 45.0)], w);
s.line(&[c, P::new(310.0, 105.0)], w);
s.junctions.push((c, 4));
// x.
s.line(&[P::new(330.0, 40.0), P::new(370.0, 100.0)], w);
s.line(&[P::new(370.0, 40.0), P::new(330.0, 100.0)], w);
s.junctions.push((P::new(350.0, 70.0), 4));
// s: two arcs.
s.arc(P::new(405.0, 50.0), 15.0, -0.2, -std::f64::consts::PI * 1.5, w);
s.arc(P::new(405.0, 80.0), 15.0, -std::f64::consts::PI * 0.5, std::f64::consts::PI * 0.8, w);
s.corners.push(P::new(405.0, 65.0));
// o.
s.ring(P::new(475.0, 65.0), 25.0, w);
shifted(s)
}
/// Thin strokes and wide fills, with strokes running into fills; Stroke width 6.
pub fn mix() -> Scene {
let mut s = Scene::new("mix", 480, 300, 6.0);
s.fills.push(GtFill::Disc(P::new(100.0, 100.0), 30.0));
s.fills.push(GtFill::Poly(vec![P::new(260.0, 60.0), P::new(340.0, 60.0), P::new(340.0, 110.0), P::new(260.0, 110.0)]));
s.fills.push(GtFill::Disc(P::new(400.0, 220.0), 22.0));
// Lines into the disc and the rectangle.
s.line(&[P::new(20.0, 200.0), P::new(80.0, 120.0)], 3.0);
s.line(&[P::new(130.0, 100.0), P::new(260.0, 85.0)], 2.0);
s.line(&[P::new(300.0, 110.0), P::new(300.0, 200.0), P::new(380.0, 210.0)], 4.0);
// Free strokes.
s.line(&[P::new(40.0, 260.0), P::new(250.0, 280.0)], 3.0);
s.curve(|t| P::new(150.0 + 120.0 * t, 200.0 + 25.0 * (t * 7.0).sin()), |_| 2.0, 200, false);
s.line(&[P::new(420.0, 30.0), P::new(460.0, 150.0)], 5.0);
s
}
/// Strokes whose width varies: tapers, a lens, a swelling brush curve; Stroke width 12.
pub fn varying() -> Scene {
let mut s = Scene::new("varying", 480, 300, 12.0);
s.curve(|t| P::new(30.0 + 200.0 * t, 40.0), |t| 2.0 + 8.0 * t, 200, false);
s.curve(|t| P::new(260.0 + 200.0 * t, 40.0 + 30.0 * t), |t| 2.0 + 8.0 * (std::f64::consts::PI * t).sin(), 200, false);
s.curve(|t| P::new(30.0 + 420.0 * t, 150.0 + 40.0 * (t * 6.0).sin()), |t| 5.5 + 3.0 * (t * 9.0).sin(), 600, false);
s.curve(
|t| {
let a = t * 1.6 * std::f64::consts::PI;
P::new(120.0, 250.0) + P::new(a.cos(), a.sin()) * 35.0
},
|t| 2.0 + 9.0 * t,
300,
false,
);
s.curve(|t| P::new(250.0 + 200.0 * t, 260.0 - 50.0 * t * t), |t| 9.0 - 6.0 * t, 200, false);
s
}
/// Shifted copies of the integer-placed scenes' junctions and corners (strokes shift in `var`).
fn shifted(mut s: Scene) -> Scene {
let o = s.offset;
for j in &mut s.junctions {
j.0 = j.0 + o;
}
for c in &mut s.corners {
*c = *c + o;
}
s
}
pub fn all() -> Vec {
let mut v = vec![];
for w in [1.0, 2.0, 3.0, 4.0, 6.0, 8.0, 10.0, 12.0] {
v.push(lines(w));
}
for w in [2.0, 4.0, 8.0] {
v.push(curves(w));
}
v.push(circles());
for w in [2.0, 4.0, 8.0] {
v.push(junctions(w));
}
for w in [3.0, 6.0] {
v.push(crossings(w));
}
for w in [2.0, 4.0] {
v.push(dashed(w));
}
for w in [3.0, 5.0] {
v.push(letters(w));
}
v.push(mix());
v.push(varying());
v
}