T07: one coverage per pixel (overlaps cut, strokes as polygons, the cross cut around its first band); a drawn base keeps its geometry under marks
This commit is contained in:
6 files changed
+506
-318
No files matched your search
@@ -0,0 +1,170 @@
|
||||
use super::{Point, Shape};
|
||||
|
||||
/// Straight pieces a cubic curve is cut into.
|
||||
const SEGMENTS: usize = 16;
|
||||
|
||||
/// A rounded corner from the light lines' centers to two edges.
|
||||
pub(super) fn arc(code: u32, w: u32, h: u32, t: u32) -> Vec<Shape> {
|
||||
let (wf, hf, tf) = (w as f32, h as f32, t as f32);
|
||||
let cx = (w.saturating_sub(t) / 2) as f32 + tf / 2.;
|
||||
let cy = (h.saturating_sub(t) / 2) as f32 + tf / 2.;
|
||||
let r = wf.min(hf) / 2.;
|
||||
let s = 0.25;
|
||||
let [dy, dx]: [f32; 2] = [[1., 1.], [1., -1.], [-1., -1.], [-1., 1.]][(code - 0x256D) as usize];
|
||||
let (bend, end) = ((cx, cy + dy * r), (cx + dx * r, cy));
|
||||
let (lead_in, lead_out) = (
|
||||
(cx, if dy > 0. { hf } else { 0. }),
|
||||
(if dx > 0. { wf } else { 0. }, cy),
|
||||
);
|
||||
let mut path = Vec::new();
|
||||
if dy * (lead_in.1 - bend.1) > 0. {
|
||||
path.push(lead_in);
|
||||
}
|
||||
path.push(bend);
|
||||
path.extend(cubic(
|
||||
bend,
|
||||
[(cx, cy + dy * s * r), (cx + dx * s * r, cy), end],
|
||||
));
|
||||
if dx * (lead_out.0 - end.0) > 0. {
|
||||
path.push(lead_out);
|
||||
}
|
||||
vec![Shape::Fill(stroke(&path, tf / 2.))]
|
||||
}
|
||||
|
||||
/// Corner-to-corner bands, run half a pixel past so neighbors join once clipped.
|
||||
pub(super) fn diagonals(code: u32, w: u32, h: u32, t: u32) -> Vec<Shape> {
|
||||
let (wf, hf, half) = (w as f32, h as f32, t as f32 / 2.);
|
||||
let (sx, sy) = ((wf / hf).min(1.) / 2., (hf / wf).min(1.) / 2.);
|
||||
let rising = [(wf + sx, -sy), (-sx, hf + sy)];
|
||||
let falling = [(-sx, -sy), (wf + sx, hf + sy)];
|
||||
match code {
|
||||
0x2571 => vec![Shape::Fill(stroke(&rising, half))],
|
||||
0x2572 => vec![Shape::Fill(stroke(&falling, half))],
|
||||
_ => {
|
||||
let band = stroke(&falling, half);
|
||||
let n = normal(rising[0], rising[1]);
|
||||
let at = n.0 * rising[0].0 + n.1 * rising[0].1;
|
||||
let before = clip(&band, (-n.0, -n.1), half - at);
|
||||
let after = clip(&band, n, at + half);
|
||||
[stroke(&rising, half), before, after]
|
||||
.into_iter()
|
||||
.filter(|polygon| polygon.len() >= 3)
|
||||
.map(Shape::Fill)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Powerline's arrows and half circles, filling the cell's height.
|
||||
pub(super) fn powerline(code: u32, w: u32, h: u32, t: u32) -> Vec<Shape> {
|
||||
let (wf, hf, tf) = (w as f32, h as f32, t as f32);
|
||||
let arrow = vec![(0., 0.), (wf, hf / 2.), (0., hf)];
|
||||
let polygon = match code {
|
||||
0xE0B0 | 0xE0B2 => arrow,
|
||||
0xE0B1 | 0xE0B3 => stroke(&arrow, tf / 2.),
|
||||
0xE0B4 | 0xE0B6 => half_circle(wf, hf, 0.),
|
||||
_ => stroke(&half_circle(wf, hf, tf / 2.), tf / 2.),
|
||||
};
|
||||
let polygon = if matches!(code, 0xE0B2 | 0xE0B3 | 0xE0B6 | 0xE0B7) {
|
||||
polygon.into_iter().map(|(x, y)| (wf - x, y)).collect()
|
||||
} else {
|
||||
polygon
|
||||
};
|
||||
vec![Shape::Fill(polygon)]
|
||||
}
|
||||
|
||||
/// The right half of a circle against the left edge, inset `by` so a stroke stays inside; its ends run straight a pixel, as Ghostty's do.
|
||||
fn half_circle(w: f32, h: f32, by: f32) -> Vec<Point> {
|
||||
let c = (2f32.sqrt() - 1.) * 4. / 3.;
|
||||
let r = w.min(h / 2.) - by;
|
||||
let (top, bottom) = (by, h - by);
|
||||
let mut path = vec![(0., top), (1., top)];
|
||||
path.extend(cubic(
|
||||
(1., top),
|
||||
[(r * c, top), (r, top + r - r * c), (r, top + r)],
|
||||
));
|
||||
path.push((r, bottom - r));
|
||||
path.extend(cubic(
|
||||
(r, bottom - r),
|
||||
[(r, bottom - r + r * c), (r * c, bottom), (1., bottom)],
|
||||
));
|
||||
path.push((0., bottom));
|
||||
distinct(&path)
|
||||
}
|
||||
|
||||
/// Points along a cubic after `from`, its end the last.
|
||||
fn cubic(from: Point, [a, b, to]: [Point; 3]) -> impl Iterator<Item = Point> {
|
||||
(1..=SEGMENTS).map(move |step| {
|
||||
let t = step as f32 / SEGMENTS as f32;
|
||||
let u = 1. - t;
|
||||
let mix = |p: f32, q: f32, r: f32, s: f32| {
|
||||
u * u * u * p + 3. * u * u * t * q + 3. * u * t * t * r + t * t * t * s
|
||||
};
|
||||
(mix(from.0, a.0, b.0, to.0), mix(from.1, a.1, b.1, to.1))
|
||||
})
|
||||
}
|
||||
|
||||
/// The path without points that repeat the one before.
|
||||
fn distinct(path: &[Point]) -> Vec<Point> {
|
||||
let mut points: Vec<Point> = Vec::with_capacity(path.len());
|
||||
for &point in path {
|
||||
let repeat = points
|
||||
.last()
|
||||
.is_some_and(|last| (point.0 - last.0).hypot(point.1 - last.1) < 1e-3);
|
||||
if !repeat {
|
||||
points.push(point);
|
||||
}
|
||||
}
|
||||
points
|
||||
}
|
||||
|
||||
/// The outline of `path` stroked `half` each side, with butt ends and mitered corners.
|
||||
fn stroke(path: &[Point], half: f32) -> Vec<Point> {
|
||||
let points = distinct(path);
|
||||
assert!(points.len() >= 2, "a stroke needs two distinct points");
|
||||
let last = points.len() - 1;
|
||||
let offsets: Vec<Point> = (0..=last)
|
||||
.map(|index| {
|
||||
let before = normal(points[index.saturating_sub(1)], points[index.max(1)]);
|
||||
let after = normal(points[index.min(last - 1)], points[(index + 1).min(last)]);
|
||||
let mean = ((before.0 + after.0) / 2., (before.1 + after.1) / 2.);
|
||||
let scale = half / (mean.0 * mean.0 + mean.1 * mean.1);
|
||||
(mean.0 * scale, mean.1 * scale)
|
||||
})
|
||||
.collect();
|
||||
let ahead = points
|
||||
.iter()
|
||||
.zip(&offsets)
|
||||
.map(|(p, o)| (p.0 + o.0, p.1 + o.1));
|
||||
let back = points
|
||||
.iter()
|
||||
.zip(&offsets)
|
||||
.rev()
|
||||
.map(|(p, o)| (p.0 - o.0, p.1 - o.1));
|
||||
ahead.chain(back).collect()
|
||||
}
|
||||
|
||||
/// The unit normal of the segment from `from` to `to`.
|
||||
pub(super) fn normal(from: Point, to: Point) -> Point {
|
||||
let (dx, dy) = (to.0 - from.0, to.1 - from.1);
|
||||
let length = dx.hypot(dy);
|
||||
(-dy / length, dx / length)
|
||||
}
|
||||
|
||||
/// The part of convex `polygon` where `n · p >= at`.
|
||||
fn clip(polygon: &[Point], n: Point, at: f32) -> Vec<Point> {
|
||||
let side = |p: Point| n.0 * p.0 + n.1 * p.1 - at;
|
||||
let mut kept = Vec::new();
|
||||
for (index, &p) in polygon.iter().enumerate() {
|
||||
let q = polygon[(index + 1) % polygon.len()];
|
||||
let (sp, sq) = (side(p), side(q));
|
||||
if sp >= 0. {
|
||||
kept.push(p);
|
||||
}
|
||||
if (sp >= 0.) != (sq >= 0.) {
|
||||
let k = sp / (sp - sq);
|
||||
kept.push((p.0 + (q.0 - p.0) * k, p.1 + (q.1 - p.1) * k));
|
||||
}
|
||||
}
|
||||
kept
|
||||
}
|
||||
+160
-66
@@ -1,3 +1,5 @@
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use super::*;
|
||||
|
||||
/// Cells as fonts give them: even and odd sides, thin and thick lines.
|
||||
@@ -11,35 +13,49 @@ fn every_drawn() -> impl Iterator<Item = char> {
|
||||
('\u{2500}'..='\u{259F}').chain('\u{E0B0}'..='\u{E0B7}')
|
||||
}
|
||||
|
||||
/// The spans across an edge of the rects touching it; sides go up, right, down, left.
|
||||
fn corners(shape: &Shape) -> Option<Corners> {
|
||||
match *shape {
|
||||
Shape::Rect { x, y, w, h, .. } => Some([x, y, x + w, y + h]),
|
||||
Shape::Fill(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn pixels(boxes: &[Corners]) -> BTreeSet<(u32, u32)> {
|
||||
boxes
|
||||
.iter()
|
||||
.flat_map(|&[x0, y0, x1, y1]| (y0..y1).flat_map(move |y| (x0..x1).map(move |x| (x, y))))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The merged spans across an edge of the rects touching it; sides go up, right, down, left.
|
||||
fn edge(shapes: &[Shape], side: usize, cell_w: u32, cell_h: u32) -> Vec<(u32, u32)> {
|
||||
let mut spans: Vec<(u32, u32)> = shapes
|
||||
.iter()
|
||||
.filter_map(|shape| match *shape {
|
||||
Shape::Rect { x, y, w, h, .. } => {
|
||||
let touches = [y == 0, x + w == cell_w, y + h == cell_h, x == 0][side];
|
||||
let span = if side.is_multiple_of(2) {
|
||||
(x, x + w)
|
||||
} else {
|
||||
(y, y + h)
|
||||
};
|
||||
touches.then_some(span)
|
||||
.filter_map(corners)
|
||||
.filter(|&[x0, y0, x1, y1]| [y0 == 0, x1 == cell_w, y1 == cell_h, x0 == 0][side])
|
||||
.map(|[x0, y0, x1, y1]| {
|
||||
if side.is_multiple_of(2) {
|
||||
(x0, x1)
|
||||
} else {
|
||||
(y0, y1)
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
spans.sort();
|
||||
spans
|
||||
let mut merged: Vec<(u32, u32)> = Vec::new();
|
||||
for span in spans {
|
||||
match merged.last_mut() {
|
||||
Some(last) if span.0 <= last.1 => last.1 = last.1.max(span.1),
|
||||
_ => merged.push(span),
|
||||
}
|
||||
}
|
||||
merged
|
||||
}
|
||||
|
||||
fn points(steps: &[Step]) -> Vec<(f32, f32)> {
|
||||
steps
|
||||
.iter()
|
||||
.flat_map(|step| match *step {
|
||||
Step::Move(x, y) | Step::Line(x, y) => vec![(x, y)],
|
||||
Step::Curve([ax, ay, bx, by, x, y]) => vec![(ax, ay), (bx, by), (x, y)],
|
||||
})
|
||||
.collect()
|
||||
/// Whether segments `a`-`b` and `c`-`d` cross inside both.
|
||||
fn cross(a: Point, b: Point, c: Point, d: Point) -> bool {
|
||||
let turn = |p: Point, q: Point, r: Point| (q.0 - p.0) * (r.1 - p.1) - (q.1 - p.1) * (r.0 - p.0);
|
||||
turn(c, d, a) * turn(c, d, b) < -1e-4 && turn(a, b, c) * turn(a, b, d) < -1e-4
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -67,9 +83,10 @@ fn every_drawn_char_draws_inside_its_cell() {
|
||||
assert!(*rw > 0 && *rh > 0, "{ch} has an empty rect at {w}x{h}");
|
||||
assert!(x + rw <= w && y + rh <= h, "{ch} leaves {w}x{h}");
|
||||
}
|
||||
Shape::Fill(steps) | Shape::Stroke(steps, _) => {
|
||||
let reach = t as f32;
|
||||
for (px, py) in points(steps) {
|
||||
Shape::Fill(polygon) => {
|
||||
assert!(polygon.len() >= 3, "{ch} has a degenerate polygon");
|
||||
let reach = 2. * t as f32;
|
||||
for &(px, py) in polygon {
|
||||
let inside = (-reach..=w as f32 + reach).contains(&px)
|
||||
&& (-reach..=h as f32 + reach).contains(&py);
|
||||
assert!(inside, "{ch} reaches ({px}, {py}) in {w}x{h}");
|
||||
@@ -81,6 +98,90 @@ fn every_drawn_char_draws_inside_its_cell() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_pixel_is_painted_twice() {
|
||||
for (w, h, t) in CELLS {
|
||||
for ch in every_drawn() {
|
||||
let shapes = shapes(ch, w, h, t);
|
||||
let rects: Vec<Corners> = shapes.iter().filter_map(corners).collect();
|
||||
for (index, a) in rects.iter().enumerate() {
|
||||
for b in &rects[index + 1..] {
|
||||
let apart = a[2] <= b[0] || b[2] <= a[0] || a[3] <= b[1] || b[3] <= a[1];
|
||||
assert!(apart, "{ch} overlaps {a:?} and {b:?} at {w}x{h}");
|
||||
}
|
||||
}
|
||||
for shape in &shapes {
|
||||
let Shape::Fill(polygon) = shape else {
|
||||
continue;
|
||||
};
|
||||
let n = polygon.len();
|
||||
for i in 0..n {
|
||||
for j in i + 2..n {
|
||||
if i == 0 && j == n - 1 {
|
||||
continue;
|
||||
}
|
||||
let (a, b) = (polygon[i], polygon[i + 1]);
|
||||
let (c, d) = (polygon[j], polygon[(j + 1) % n]);
|
||||
assert!(
|
||||
!cross(a, b, c, d),
|
||||
"{ch}'s outline crosses itself at {w}x{h}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cutting_overlaps_keeps_every_pixel() {
|
||||
for (w, h, t) in CELLS {
|
||||
for code in 0x2500..=0x259F_u32 {
|
||||
let boxes = match code {
|
||||
0x2504..=0x250B | 0x254C..=0x254F => dashes(code, w, h, t),
|
||||
0x256D..=0x2573 | 0x2591..=0x2593 => continue,
|
||||
0x2500..=0x257F => lines(LINES[(code - 0x2500) as usize], w, h, t),
|
||||
_ => blocks(code, w, h),
|
||||
};
|
||||
let kept: Vec<Corners> = rects(boxes.clone(), 1.)
|
||||
.iter()
|
||||
.filter_map(corners)
|
||||
.collect();
|
||||
assert_eq!(pixels(&kept), pixels(&boxes), "U+{code:04X} at {w}x{h}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_cross_cuts_its_second_band_around_the_first() {
|
||||
for (w, h, t) in CELLS {
|
||||
let shapes = shapes('╳', w, h, t);
|
||||
let [Shape::Fill(first), pieces @ ..] = shapes.as_slice() else {
|
||||
panic!("╳ is polygons");
|
||||
};
|
||||
assert_eq!(
|
||||
pieces.len(),
|
||||
2,
|
||||
"two pieces around the first band at {w}x{h}"
|
||||
);
|
||||
let n = outline::normal(first[0], first[1]);
|
||||
let line = |p: Point| n.0 * p.0 + n.1 * p.1;
|
||||
let centre = (line(first[0]) + line(first[3])) / 2.;
|
||||
for piece in pieces {
|
||||
let Shape::Fill(piece) = piece else {
|
||||
panic!("pieces are polygons");
|
||||
};
|
||||
for &p in piece {
|
||||
let off = (line(p) - centre).abs();
|
||||
assert!(
|
||||
off >= t as f32 / 2. - 1e-3,
|
||||
"{p:?} is inside the first band at {w}x{h}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn straight_lines_sit_centered_at_their_weight() {
|
||||
let (w, h, t) = (16, 32, 2);
|
||||
@@ -136,11 +237,8 @@ fn dashes_tile_with_one_gap_between_cells() {
|
||||
}
|
||||
let mut runs: Vec<(u32, u32)> = drawn
|
||||
.iter()
|
||||
.map(|shape| match *shape {
|
||||
Shape::Rect { y, h: rh, .. } if vertical => (y, y + rh),
|
||||
Shape::Rect { x, w: rw, .. } => (x, x + rw),
|
||||
_ => panic!("{ch} is drawn with rects"),
|
||||
})
|
||||
.filter_map(corners)
|
||||
.map(|[x0, y0, x1, y1]| if vertical { (y0, y1) } else { (x0, x1) })
|
||||
.collect();
|
||||
runs.sort();
|
||||
assert_eq!(runs.len() as u32, count, "{ch} at {w}x{h}");
|
||||
@@ -164,18 +262,13 @@ fn dashes_tile_with_one_gap_between_cells() {
|
||||
#[test]
|
||||
fn halves_cover_the_cell_and_shades_wash_it() {
|
||||
for (w, h, t) in CELLS {
|
||||
let span = |ch: char| match shapes(ch, w, h, t)[0] {
|
||||
Shape::Rect {
|
||||
x, y, w: rw, h: rh, ..
|
||||
} => (x, y, x + rw, y + rh),
|
||||
_ => panic!("{ch} is a rect"),
|
||||
};
|
||||
let span = |ch: char| corners(&shapes(ch, w, h, t)[0]).expect("a rect");
|
||||
let (upper, lower) = (span('▀'), span('▄'));
|
||||
assert_eq!((upper.1, lower.3), (0, h));
|
||||
assert!(upper.3 >= lower.1, "a gap between ▀ and ▄ at {w}x{h}");
|
||||
assert_eq!((upper[1], lower[3]), (0, h));
|
||||
assert!(upper[3] >= lower[1], "a gap between ▀ and ▄ at {w}x{h}");
|
||||
let (left, right) = (span('▌'), span('▐'));
|
||||
assert_eq!((left.0, right.2), (0, w));
|
||||
assert!(left.2 >= right.0, "a gap between ▌ and ▐ at {w}x{h}");
|
||||
assert_eq!((left[0], right[2]), (0, w));
|
||||
assert!(left[2] >= right[0], "a gap between ▌ and ▐ at {w}x{h}");
|
||||
for (ch, alpha) in [('░', 0.25), ('▒', 0.5), ('▓', 0.75)] {
|
||||
let want = Shape::Rect {
|
||||
x: 0,
|
||||
@@ -186,44 +279,45 @@ fn halves_cover_the_cell_and_shades_wash_it() {
|
||||
};
|
||||
assert_eq!(shapes(ch, w, h, t), [want]);
|
||||
}
|
||||
assert_eq!(shapes('▚', w, h, t).len(), 2);
|
||||
assert_eq!(shapes('▟', w, h, t).len(), 3);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn powerline_fills_the_cell_and_mirrors() {
|
||||
let (w, h) = (16., 32.);
|
||||
let right = vec![Step::Move(0., 0.), Step::Line(w, h / 2.), Step::Line(0., h)];
|
||||
let left = vec![Step::Move(w, 0.), Step::Line(0., h / 2.), Step::Line(w, h)];
|
||||
assert_eq!(shapes('\u{E0B0}', 16, 32, 2), [Shape::Fill(right.clone())]);
|
||||
assert_eq!(shapes('\u{E0B2}', 16, 32, 2), [Shape::Fill(left)]);
|
||||
assert_eq!(shapes('\u{E0B1}', 16, 32, 2), [Shape::Stroke(right, 2.)]);
|
||||
let Shape::Fill(round) = &shapes('\u{E0B6}', 16, 32, 2)[0] else {
|
||||
panic!("a filled half circle");
|
||||
let fill = |ch: char| match shapes(ch, 16, 32, 2).as_slice() {
|
||||
[Shape::Fill(polygon)] => polygon.clone(),
|
||||
other => panic!("{ch} is one polygon, not {other:?}"),
|
||||
};
|
||||
assert_eq!(round.first(), Some(&Step::Move(16., 0.)));
|
||||
let Shape::Stroke(outline, 2.) = &shapes('\u{E0B5}', 16, 32, 2)[0] else {
|
||||
panic!("a stroked half circle");
|
||||
};
|
||||
assert_eq!(
|
||||
outline.first(),
|
||||
Some(&Step::Move(0., 1.)),
|
||||
"inset by half a line"
|
||||
assert_eq!(fill('\u{E0B0}'), [(0., 0.), (16., 16.), (0., 32.)]);
|
||||
assert_eq!(fill('\u{E0B2}'), [(16., 0.), (0., 16.), (16., 32.)]);
|
||||
assert_eq!(fill('\u{E0B1}').len(), 6, "a mitered chevron");
|
||||
assert_eq!(fill('\u{E0B6}')[0], (16., 0.));
|
||||
let outline = fill('\u{E0B5}');
|
||||
let low = outline.iter().map(|p| p.1).fold(f32::MAX, f32::min);
|
||||
let high = outline.iter().map(|p| p.1).fold(f32::MIN, f32::max);
|
||||
let right = outline.iter().map(|p| p.0).fold(f32::MIN, f32::max);
|
||||
assert!(
|
||||
low.abs() < 1e-3 && (high - 32.).abs() < 1e-3,
|
||||
"{low}..{high}"
|
||||
);
|
||||
assert!(
|
||||
right <= 16.01,
|
||||
"the outline stays in the half circle: {right}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arcs_end_on_the_light_lines_centers() {
|
||||
fn arcs_end_on_the_light_lines() {
|
||||
let (w, h, t) = (16, 32, 2);
|
||||
let light = (w - t) / 2;
|
||||
let Shape::Stroke(path, width) = &shapes('╭', w, h, t)[0] else {
|
||||
panic!("╭ is a stroke");
|
||||
let drawn = shapes('╭', w, h, t);
|
||||
let [Shape::Fill(outline)] = drawn.as_slice() else {
|
||||
panic!("╭ is one polygon");
|
||||
};
|
||||
assert_eq!(*width, t as f32);
|
||||
assert_eq!(path.first(), Some(&Step::Move(light as f32 + 1., h as f32)));
|
||||
assert_eq!(
|
||||
path.last(),
|
||||
Some(&Step::Line(w as f32, ((h - t) / 2) as f32 + 1.))
|
||||
);
|
||||
let near = |x: f32, y: f32| {
|
||||
outline
|
||||
.iter()
|
||||
.any(|p| (p.0 - x).abs() < 0.05 && (p.1 - y).abs() < 0.05)
|
||||
};
|
||||
assert!(near(7., 32.) && near(9., 32.), "the bottom end spans │");
|
||||
assert!(near(16., 15.) && near(16., 17.), "the right end spans ─");
|
||||
}
|
||||
Reference in new issue
Block a user