//! Cell primitives: box drawing and block elements. //! //! The second tier of the house set, and a different kind of thing from the //! first. The marks in `glyphs/manifest.toml` are *symbols*: they sit in the //! band the base fits its own symbols into, they are sized against it, and each //! one is authored. These are *cell furniture*: they fill the box a terminal //! gives a character, they are sized against that box and never against the //! band, and there are 160 of them. //! //! ## Why they are generated rather than drawn //! //! Because they must tile. A vertical bar has to meet the bar in the cell above //! it exactly, and a horizontal one has to meet the cell beside it, or a table //! border shows a seam at every join. That is arithmetic on the cell, and //! arithmetic is what a generator is good at and what a hand is bad at. //! Generating is the correct method here rather than the cheap one. //! //! It is also the only method that answers the axis. The recipes are evaluated //! at each `gvar` master, so a bold border draws at the bold stroke weight //! without a second drawing existing anywhere. //! //! ## The cell //! //! `hhea` ascender to descender, and the full advance. Not the band, which is //! near the x-height: a rule on the band's centre would run through the middle //! of the text beside it rather than between the lines. //! //! A base need not agree with itself here: a face can draw its box glyphs over //! one vertical range while its own `hhea` says another, so its borders do not //! line up with a cell laid out from its metrics, which is why terminals so //! often stretch box-drawing glyphs to the cell. Ours is built on the metrics a //! terminal actually lays out with, so it needs no stretching. //! //! ## The stroke vocabulary //! //! One light bar is the base's own: `-`'s height for horizontals, `|`'s width //! for verticals, so a border sits at the weight of the text it surrounds. //! Heavy is twice that. Double is two light rails with a light gap, which is a //! total of three light bars and is what Plex draws (its double horizontal //! spans 204 units against the light bar's 68). mod table; use crate::base::BaseParams; use crate::draw::Drawing; use crate::manifest::{GlyphSpec, Purpose, Shape, WeightResponse, format_codepoint}; /// How heavy one arm of a box-drawing glyph is. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Arm { /// No arm in this direction. Spelled `N` in the generated table. N, L, H, D, } impl Arm { fn present(self) -> bool { self != Self::N } /// Half the arm's total extent across itself, in units. /// /// What a crossing arm has to reach past to close a joint, and what a rail /// stops at. Double counts the whole three-bar span, not one rail. fn half_extent(self, light: f64) -> f64 { match self { Self::N => 0.0, Self::L => light / 2.0, Self::H => light, Self::D => light * 1.5, } } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Dash { None, Double, Triple, Quadruple, } impl Dash { /// How many dashes the bar is broken into. const fn count(self) -> usize { match self { Self::None => 1, Self::Double => 2, Self::Triple => 3, Self::Quadruple => 4, } } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Diagonal { /// `╱`, lower left to upper right. Rising, /// `╲`, upper left to lower right. Falling, /// `╳`, both. Cross, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Shade { Light, Medium, Dark, } impl Shade { /// Filled squares out of the [`SHADE_GRID`] squared, so the three read as a /// progression rather than three unrelated textures. const fn filled(self) -> u32 { match self { Self::Light => 1, Self::Medium => 2, Self::Dark => 3, } } } /// The shade pattern's grid, per axis. /// /// Eight, so a shade's texture lines up with the eighths the block elements are /// already cut on: `░` beside `▄` should not look like a different unit. const SHADE_GRID: u32 = 8; /// One generated cell primitive. #[derive(Debug, Clone, Copy)] pub enum Cell { /// Arms from the cell's edges to its centre, in the order left, right, up, /// down. Stems { arms: [Arm; 4], dash: Dash, }, /// A rounded corner: the same two arms, turned through a quarter circle. Arc { arms: [Arm; 4], }, Diagonal(Diagonal), /// Filled rectangles, in eighths of the cell, `y` up. Fill(&'static [(u8, u8, u8, u8)]), Shade(Shade), } impl Cell { pub fn kind(self) -> &'static str { match self { Self::Stems { .. } => "stems", Self::Arc { .. } => "arc", Self::Diagonal(_) => "diagonal", Self::Fill(_) => "fill", Self::Shade(_) => "shade", } } /// Cell furniture divides on this exactly where the manifest's marks do. /// /// A stem is a stroke and thickens with the base, the way the base thickens /// `+`. A fill has no stroke and no extent to grow into — it is already the /// whole cell or an exact fraction of it — so it holds, which is what the /// base does with `█` and `░` and the reason the manifest's weight term /// exists at all. pub fn weight_response(self) -> WeightResponse { match self { Self::Stems { .. } | Self::Arc { .. } | Self::Diagonal(_) => WeightResponse::Thickens, Self::Fill(_) | Self::Shade(_) => WeightResponse::Holds, } } } /// The whole generated tier, in codepoint order. pub fn cells() -> Vec<(u32, Cell)> { let mut out: Vec<(u32, Cell)> = Vec::with_capacity(160); for &(cp, arms, dash) in &table::STEMS { out.push((cp, Cell::Stems { arms, dash })); } for &(cp, arms) in &table::ARCS { out.push((cp, Cell::Arc { arms })); } for &(cp, kind) in &table::DIAGONALS { out.push((cp, Cell::Diagonal(kind))); } for &(cp, rects) in &table::FILLS { out.push((cp, Cell::Fill(rects))); } for &(cp, level) in &table::SHADES { out.push((cp, Cell::Shade(level))); } out.sort_by_key(|&(cp, _)| cp); out } /// The generated tier as manifest entries, so nothing downstream has to know /// these were not authored one by one. pub fn specs(block: Block) -> Vec { cells() .into_iter() .filter(|&(cp, _)| block.contains(cp)) .map(|(codepoint, cell)| GlyphSpec { codepoint, name: format!("uni{codepoint:04X}"), role: format!("{} ({})", block.role(), cell.kind()), source: Some(format!( "generated from the Unicode name of {}", format_codepoint(codepoint) )), // Never house: a base that ships box drawing ships a designed set // of it, and ours exists for the bases that do not. purpose: Purpose::Coverage, shape: Shape::Cell(cell), }) .collect() } /// A block a schema may ask for by name. #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize)] #[serde(rename_all = "kebab-case")] pub enum Block { BoxDrawing, BlockElements, } impl Block { const fn range(self) -> (u32, u32) { match self { Self::BoxDrawing => (0x2500, 0x257F), Self::BlockElements => (0x2580, 0x259F), } } fn contains(self, codepoint: u32) -> bool { let (lo, hi) = self.range(); (lo..=hi).contains(&codepoint) } const fn role(self) -> &'static str { match self { Self::BoxDrawing => "box drawing", Self::BlockElements => "block element", } } } // --------------------------------------------------------------------------- // Drawing // --------------------------------------------------------------------------- /// The cell a primitive is drawn into: the advance, and ascender to descender. struct CellBox { x0: f64, x1: f64, y0: f64, y1: f64, cx: f64, cy: f64, /// A light horizontal bar's thickness: the base's own `-`. light_h: f64, /// A light vertical bar's thickness: the base's own `|`. light_v: f64, } impl CellBox { fn of(params: &BaseParams) -> Self { Self { x0: 0.0, x1: f64::from(params.advance), y0: f64::from(params.descent), y1: f64::from(params.ascent), cx: params.center_x(), cy: params.cell_center_y(), light_h: f64::from(params.stroke), light_v: f64::from(params.stem), } } fn rect(x0: f64, y0: f64, x1: f64, y1: f64) -> Vec<(f64, f64)> { vec![(x0, y0), (x1, y0), (x1, y1), (x0, y1)] } } pub fn draw(cell: Cell, params: &BaseParams) -> Drawing { let c = CellBox::of(params); let contours = match cell { Cell::Stems { arms, dash } => stems(&c, arms, dash), Cell::Arc { arms } => arc(&c, arms), Cell::Diagonal(kind) => diagonal(&c, kind), Cell::Fill(rects) => fill(&c, rects), Cell::Shade(level) => shade(&c, level), }; Drawing { contours } } /// Arms in the table's order. const LEFT: usize = 0; const RIGHT: usize = 1; const UP: usize = 2; const DOWN: usize = 3; fn stems(c: &CellBox, arms: [Arm; 4], dash: Dash) -> Vec> { let mut out = Vec::new(); // A dashed glyph is one bar across the whole cell rather than two arms // meeting in the middle. Unicode has no dashed junction — every dashed // codepoint in the block is a plain horizontal or vertical — and drawing it // as two arms would dash each half separately, so `┄` would come out with // six marks and a seam in the middle instead of the three it names. if dash != Dash::None { let horizontal = arms[LEFT].present(); let arm = if horizontal { arms[LEFT] } else { arms[UP] }; let light = if horizontal { c.light_h } else { c.light_v }; let thickness = if arm == Arm::H { light * 2.0 } else { light }; let (from, to, across) = if horizontal { (c.x0, c.x1, c.cy) } else { (c.y0, c.y1, c.cx) }; return bar(c, horizontal, from, to, across, thickness, dash); } // What a horizontal arm has to reach past to close a joint, and the other // way round. The heavier of the two arms on the crossing axis wins: a // junction is as wide as the widest thing passing through it. let vertical_half = arms[UP] .half_extent(c.light_v) .max(arms[DOWN].half_extent(c.light_v)); let horizontal_half = arms[LEFT] .half_extent(c.light_h) .max(arms[RIGHT].half_extent(c.light_h)); for (index, arm) in arms.iter().enumerate() { if !arm.present() { continue; } let horizontal = index == LEFT || index == RIGHT; let light = if horizontal { c.light_h } else { c.light_v }; // Which way the arm runs, and how far past the centre it reaches. let sign = if index == LEFT || index == DOWN { -1.0 } else { 1.0 }; let reach = if horizontal { vertical_half } else { horizontal_half }; let edge = match index { LEFT => c.x0, RIGHT => c.x1, UP => c.y1, _ => c.y0, }; let centre = if horizontal { c.cx } else { c.cy }; let across = if horizontal { c.cy } else { c.cx }; match arm { Arm::N => {} Arm::L | Arm::H => { let thickness = if *arm == Arm::H { light * 2.0 } else { light }; // A single arm runs through the joint and out the far side of // whatever crosses it, so the corner is square rather than // notched. With nothing crossing, `reach` is zero and the arm // stops at the centre, which is what a half-line is. let stop = centre - reach * sign; out.extend(bar(c, horizontal, edge, stop, across, thickness, dash)); } Arm::D => { // Two rails, and each one stops in a different place. A rail // whose side is occupied by a crossing arm stops short of it; // the other continues to the far rail and closes the corner. // That single rule draws every corner, tee and cross in the // double family, including `╬`, whose four arms all stop short // and leave the middle open. let gap = light; for side in [1.0, -1.0] { let blocked = if horizontal { arms[if side > 0.0 { UP } else { DOWN }].present() } else { arms[if side > 0.0 { RIGHT } else { LEFT }].present() }; let stop = if blocked { centre + reach * sign } else { centre - reach * sign }; out.extend(bar( c, horizontal, edge, stop, across + side * gap, light, dash, )); } } } } out } /// One bar from `edge` to `stop`, `thickness` across, centred on `across`. /// /// Dashes are cut here rather than by the caller so a dashed arm and a solid /// one are the same code with a different count. The gap is one light bar wide, /// which keeps the dash reading as the same weight as its neighbours. fn bar( c: &CellBox, horizontal: bool, edge: f64, stop: f64, across: f64, thickness: f64, dash: Dash, ) -> Vec> { let half = thickness / 2.0; let (lo, hi) = if edge < stop { (edge, stop) } else { (stop, edge) }; let count = dash.count(); let gap = if count > 1 { if horizontal { c.light_h } else { c.light_v } } else { 0.0 }; // Length is shared between the dashes and the gaps between them, so a // quadruple dash is four shorter marks rather than four of the triple's and // a longer glyph. let span = (hi - lo - gap * (count as f64 - 1.0)) / count as f64; (0..count) .map(|i| { let a = lo + (span + gap) * i as f64; let b = a + span; if horizontal { CellBox::rect(a, across - half, b, across + half) } else { CellBox::rect(across - half, a, across + half, b) } }) .collect() } /// Segments in a quarter turn. /// /// Twelve: enough that the curve reads as one at a terminal's size, and a fixed /// count so every master has the same points, which `gvar` requires. const ARC_SEGMENTS: usize = 12; fn arc(c: &CellBox, arms: [Arm; 4]) -> Vec> { // Built as a centre line and then given a thickness, rather than by // computing the two edges directly. The direct version needs a sign per // quadrant per edge and gets one of them wrong in a way a bounding box // cannot see, which is what happened here first time. // // The geometry comes off the tangent points rather than off an angle // sweep, for the same reason. The two straight runs lie on `y = cy` and // `x = cx` and cross at the cell's centre; a circle tangent to both has its // own centre one radius along each of them, and touches them at // `(ox, cy)` and `(cx, oy)`. Those two points are where the straights stop // and the turn starts, so nothing has to be reasoned about twice. let horizontal = if arms[LEFT].present() { LEFT } else { RIGHT }; let vertical = if arms[UP].present() { UP } else { DOWN }; let x_edge = if horizontal == LEFT { c.x0 } else { c.x1 }; let y_edge = if vertical == UP { c.y1 } else { c.y0 }; let x_sign = if horizontal == LEFT { -1.0 } else { 1.0 }; let y_sign = if vertical == UP { 1.0 } else { -1.0 }; // A third of the half-cell, so the turn reads as a corner rather than a // bow and both arms keep a straight run to meet their neighbours squarely. let radius = ((c.cx - c.x0).abs() / 3.0).min((c.cy - c.y0).abs() / 3.0); let (ox, oy) = (c.cx + radius * x_sign, c.cy + radius * y_sign); let mut centre: Vec<(f64, f64)> = vec![(x_edge, c.cy)]; for i in 0..=ARC_SEGMENTS { let t = i as f64 / ARC_SEGMENTS as f64 * std::f64::consts::FRAC_PI_2; let (sin, cos) = t.sin_cos(); centre.push((ox - radius * x_sign * sin, oy - radius * y_sign * cos)); } centre.push((c.cx, y_edge)); vec![stroke(¢re, c.light_h / 2.0)] } /// A polyline given a thickness: one closed contour, out along one side and /// back along the other. /// /// The offset at each point is perpendicular to the average of the segments /// meeting there, which keeps the width even around a turn instead of pinching /// on the inside of it. fn stroke(centre: &[(f64, f64)], half: f64) -> Vec<(f64, f64)> { let normals: Vec<(f64, f64)> = (0..centre.len()) .map(|i| { let before = i.saturating_sub(1); let after = (i + 1).min(centre.len() - 1); let (dx, dy) = ( centre[after].0 - centre[before].0, centre[after].1 - centre[before].1, ); let len = dx.hypot(dy); if len == 0.0 { (0.0, 0.0) } else { (-dy / len, dx / len) } }) .collect(); let side = |sign: f64| -> Vec<(f64, f64)> { centre .iter() .zip(&normals) .map(|(&(x, y), &(nx, ny))| (x + nx * half * sign, y + ny * half * sign)) .collect() }; let mut contour = side(1.0); let mut back = side(-1.0); back.reverse(); contour.extend(back); contour } fn diagonal(c: &CellBox, kind: Diagonal) -> Vec> { let mut out = Vec::new(); let rising = matches!(kind, Diagonal::Rising | Diagonal::Cross); let falling = matches!(kind, Diagonal::Falling | Diagonal::Cross); // Thickness measured across the stroke rather than along an axis, so a // diagonal reads the same weight as the horizontal beside it instead of // thinner by the angle. let dx = c.x1 - c.x0; let dy = c.y1 - c.y0; let length = dx.hypot(dy); let half = c.light_h / 2.0 * length / dy; if rising { out.push(vec![ (c.x0 - half, c.y0), (c.x0 + half, c.y0), (c.x1 + half, c.y1), (c.x1 - half, c.y1), ]); } if falling { out.push(vec![ (c.x0 - half, c.y1), (c.x0 + half, c.y1), (c.x1 + half, c.y0), (c.x1 - half, c.y0), ]); } out } fn fill(c: &CellBox, rects: &[(u8, u8, u8, u8)]) -> Vec> { rects .iter() .map(|&(x0, y0, x1, y1)| { CellBox::rect( eighth(c.x0, c.x1, x0), eighth(c.y0, c.y1, y0), eighth(c.x0, c.x1, x1), eighth(c.y0, c.y1, y1), ) }) .collect() } /// The `n`th eighth between two edges, rounded to a whole unit. /// /// Rounded through one function so adjacent fills agree: `▀` and `▄` meet /// exactly because both ask this for the fourth eighth and get the same answer, /// which subtracting one from the other would not guarantee. fn eighth(lo: f64, hi: f64, n: u8) -> f64 { (lo + (hi - lo) * f64::from(n) / 8.0).round() } fn shade(c: &CellBox, level: Shade) -> Vec> { // A regular grid rather than a stipple. The three shades are the same // pattern at three densities so they read as a progression, and the squares // land on the eighths the block elements are cut on so a shade beside a // bar does not look like a different unit. let mut out = Vec::new(); let filled = level.filled(); for row in 0..SHADE_GRID { for col in 0..SHADE_GRID { // Diagonal phase, so no shade reads as rows or columns of dots. if (row * 3 + col * 5) % 4 >= filled { continue; } out.push(CellBox::rect( eighth(c.x0, c.x1, col as u8), eighth(c.y0, c.y1, row as u8), eighth(c.x0, c.x1, col as u8 + 1), eighth(c.y0, c.y1, row as u8 + 1), )); } } out } #[cfg(test)] mod tests { use super::*; /// Atkinson Hyperlegible Mono at `wght` 200, measured. fn params() -> BaseParams { BaseParams { upem: 1000, advance: 632, cap_height: 668, x_height: 496, stem: 54, stroke: 55, band_x0: 68, band_x1: 564, band_y0: 0, band_y1: 496, ascent: 984, descent: -316, } } fn cell(codepoint: u32) -> Cell { cells() .into_iter() .find(|&(cp, _)| cp == codepoint) .unwrap_or_else(|| { panic!( "{} is not in the generated tier", format_codepoint(codepoint) ) }) .1 } /// Exact by construction: every cell edge is rounded to a whole unit, so /// these compare integers that happen to be typed `f64`. A tolerance here /// would hide the one thing the tier has to get right. #[track_caller] fn same(a: f64, b: f64) { assert!((a - b).abs() < f64::EPSILON, "{a} is not {b}"); } fn bbox(codepoint: u32) -> (f64, f64, f64, f64) { let drawing = draw(cell(codepoint), ¶ms()); let points: Vec<(f64, f64)> = drawing.contours.concat(); let xs: Vec = points.iter().map(|p| p.0).collect(); let ys: Vec = points.iter().map(|p| p.1).collect(); ( xs.iter().copied().fold(f64::MAX, f64::min), ys.iter().copied().fold(f64::MAX, f64::min), xs.iter().copied().fold(f64::MIN, f64::max), ys.iter().copied().fold(f64::MIN, f64::max), ) } #[test] fn the_tier_is_both_blocks_and_nothing_else() { let cells = cells(); assert_eq!(cells.len(), 160); assert_eq!(cells.first().unwrap().0, 0x2500); assert_eq!(cells.last().unwrap().0, 0x259F); for (index, &(cp, _)) in cells.iter().enumerate() { assert_eq!(cp, 0x2500 + index as u32, "the range has a hole in it"); } } // The property the whole tier exists for. A vertical bar has to reach the // top and bottom of the cell, or every row boundary shows a seam. #[test] fn a_vertical_meets_the_cell_above_and_below() { let (_, y0, _, y1) = bbox(0x2502); same(y0, -316.0); same(y1, 984.0); } #[test] fn a_horizontal_meets_the_cell_either_side() { let (x0, _, x1, _) = bbox(0x2500); same(x0, 0.0); same(x1, 632.0); } // And the two have to cross at the same place, or a `+` junction is not // where the lines that make it are. #[test] fn the_rule_sits_at_the_cells_middle_not_the_bands() { let (_, y0, _, y1) = bbox(0x2500); let params = params(); assert!((y0.midpoint(y1) - params.cell_center_y()).abs() < 1.0); assert!( (y0.midpoint(y1) - params.band_center_y()).abs() > 50.0, "the band's centre is near the x-height and is the wrong place" ); } #[test] fn heavy_is_twice_light_and_double_is_three_times() { let light = bbox(0x2500).3 - bbox(0x2500).1; let heavy = bbox(0x2501).3 - bbox(0x2501).1; let double = bbox(0x2550).3 - bbox(0x2550).1; assert!((heavy - light * 2.0).abs() < 1.0, "{heavy} vs {light}"); assert!((double - light * 3.0).abs() < 1.0, "{double} vs {light}"); } // A half-line stops at the centre; a through-line does not. #[test] fn a_half_line_stops_where_a_junction_would_be() { let (x0, _, x1, _) = bbox(0x2574); same(x0, 0.0); assert!((x1 - params().center_x()).abs() < 1.0, "ends at the centre"); } // The corner has to be square. A right arm that stopped at the centre // rather than at the far side of the vertical would leave a notch. #[test] fn a_corner_closes_rather_than_notching() { let params = params(); let (x0, _, x1, y1) = bbox(0x250C); assert!( x0 < params.center_x(), "the arm reaches back through the joint" ); same(x1, 632.0); assert!((y1 - (params.cell_center_y() + f64::from(params.stroke) / 2.0)).abs() < 1.0); } // `╬` is the case the double rule exists for: all four arms stop short and // the middle stays open. #[test] fn a_double_cross_leaves_its_middle_open() { let drawing = draw(cell(0x256C), ¶ms()); let params = params(); let (cx, cy) = (params.center_x(), params.cell_center_y()); for contour in &drawing.contours { let xs: Vec = contour.iter().map(|p| p.0).collect(); let ys: Vec = contour.iter().map(|p| p.1).collect(); let covers_centre = xs.iter().copied().fold(f64::MAX, f64::min) < cx && xs.iter().copied().fold(f64::MIN, f64::max) > cx && ys.iter().copied().fold(f64::MAX, f64::min) < cy && ys.iter().copied().fold(f64::MIN, f64::max) > cy; assert!(!covers_centre, "a rail runs through the middle of `╬`"); } } // `╔` is the other half of the rule: the outer rail of each arm has to // reach the far rail of the other, or the corner is open. #[test] fn a_double_corner_closes() { let drawing = draw(cell(0x2554), ¶ms()); let params = params(); let (cx, cy) = (params.center_x(), params.cell_center_y()); let gap = f64::from(params.stroke); // The outer corner is up and to the left of the centre by one gap. let corner = (cx - gap, cy + gap); let covered = drawing.contours.iter().any(|contour| { let xs: Vec = contour.iter().map(|p| p.0).collect(); let ys: Vec = contour.iter().map(|p| p.1).collect(); xs.iter().copied().fold(f64::MAX, f64::min) <= corner.0 && xs.iter().copied().fold(f64::MIN, f64::max) >= corner.0 && ys.iter().copied().fold(f64::MAX, f64::min) <= corner.1 && ys.iter().copied().fold(f64::MIN, f64::max) >= corner.1 }); assert!(covered, "`╔`'s outer corner is open"); } #[test] fn the_full_block_is_the_whole_cell_and_the_halves_meet_in_it() { let full = bbox(0x2588); same(full.0, 0.0); same(full.1, -316.0); same(full.2, 632.0); same(full.3, 984.0); let upper = bbox(0x2580); let lower = bbox(0x2584); // The halves meet with no seam and no overlap. same(upper.1, lower.3); same(lower.0, 0.0); same(lower.1, -316.0); same(upper.2, 632.0); same(upper.3, 984.0); } #[test] fn the_eighths_are_a_progression_and_the_ends_are_exact() { same(bbox(0x258F).2, bbox(0x2588).2 / 8.0); // From the cell floor, which is below the baseline: an eighth of a // block is not a positive coordinate. let mut last = f64::from(params().descent); for cp in [ 0x2581, 0x2582, 0x2583, 0x2584, 0x2585, 0x2586, 0x2587, 0x2588, ] { let top = bbox(cp).3; assert!(top > last, "{cp:#06X} is not taller than the one before"); last = top; } } #[test] fn a_quadrant_is_a_quarter_in_the_right_corner() { let params = params(); let (x0, y0, x1, y1) = bbox(0x2598); // upper left same(x0, 0.0); same(y1, f64::from(params.ascent)); assert!((x1 - params.center_x()).abs() <= 1.0); assert!((y0 - params.cell_center_y()).abs() <= 1.0); } // A dashed line is one bar across the cell, not two arms meeting in the // middle. Drawn as arms, `┄` came out with six marks and a seam where the // junction would have been, which is not what its name says. #[test] fn a_dash_has_the_number_of_marks_its_name_says() { for (cp, marks) in [ (0x2504, 3), // light triple dash horizontal (0x2505, 3), // heavy triple dash horizontal (0x2506, 3), // light triple dash vertical (0x2508, 4), // light quadruple dash horizontal (0x254C, 2), // light double dash horizontal (0x254E, 2), // light double dash vertical ] { let drawing = draw(cell(cp), ¶ms()); assert_eq!( drawing.contours.len(), marks, "{} draws {} marks", format_codepoint(cp), drawing.contours.len() ); } // And it still spans the whole cell, or a dashed rule would not meet // the one in the next cell along. same(bbox(0x2504).0, 0.0); same(bbox(0x2504).2, 632.0); } // The arc's two straight runs have to reach their own edges and its turn // has to join them. Checked as coverage along each run rather than as a // bounding box, which an arc that curled the wrong way would also satisfy — // and one did, until it was rasterised. #[test] fn an_arc_runs_to_both_its_edges_and_turns_between_them() { let params = params(); let contour = &draw(cell(0x256D), ¶ms).contours[0]; // `╭`, right and down let on_x = |x: f64| contour.iter().any(|p| (p.0 - x).abs() < 1.0); let on_y = |y: f64| contour.iter().any(|p| (p.1 - y).abs() < 1.0); assert!(on_x(f64::from(params.advance)), "no run to the right edge"); assert!(on_y(f64::from(params.descent)), "no run to the bottom edge"); // The turn is between the two, so points exist off both centre lines. assert!( contour.iter().any(|p| p.0 < f64::from(params.advance) - 1.0 && p.0 > params.center_x() + 1.0 && p.1 < params.cell_center_y() - 1.0), "the corner is square rather than turned" ); } #[test] fn the_shades_are_a_progression_in_coverage() { let area = |cp: u32| -> f64 { draw(cell(cp), ¶ms()) .contours .iter() .map(|contour| { let (x0, y0) = contour[0]; let (x1, y1) = contour[2]; (x1 - x0).abs() * (y1 - y0).abs() }) .sum() }; let (light, medium, dark) = (area(0x2591), area(0x2592), area(0x2593)); assert!(light < medium && medium < dark, "{light} {medium} {dark}"); let cell_area = 632.0 * 1300.0; assert!(dark < cell_area, "dark shade is not a full block"); assert!(light > 0.0); } // `gvar` needs the same points at every master, so a recipe must not change // its topology with the base's weight. Checked against a heavier params // rather than argued. #[test] fn every_recipe_keeps_its_topology_when_the_base_gets_heavier() { let light = params(); let heavy = BaseParams { stem: 158, stroke: 139, band_x0: 58, band_x1: 574, ..light }; for (cp, cell) in cells() { let a = draw(cell, &light); let b = draw(cell, &heavy); assert_eq!( a.contours.len(), b.contours.len(), "{} changes contour count with weight", format_codepoint(cp) ); for (i, (ca, cb)) in a.contours.iter().zip(b.contours.iter()).enumerate() { assert_eq!( ca.len(), cb.len(), "{} contour {i} changes point count with weight", format_codepoint(cp) ); } } } // Nothing may spill outside the cell, or a glyph paints over its neighbour. #[test] fn nothing_draws_outside_its_own_cell() { let params = params(); for (cp, cell) in cells() { if matches!(cell, Cell::Diagonal(_)) { // The diagonals are the one exception and are drawn to overrun // deliberately: a stroke at an angle has to pass the corner to // meet the one in the next cell. continue; } for (x, y) in draw(cell, ¶ms).contours.concat() { assert!( x >= -1.0 && x <= f64::from(params.advance) + 1.0, "{} runs to x={x}", format_codepoint(cp) ); assert!( y >= f64::from(params.descent) - 1.0 && y <= f64::from(params.ascent) + 1.0, "{} runs to y={y}", format_codepoint(cp) ); } } } }