max / quasi-type
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
14 files changed,
+1489 insertions,
-45 deletions
| @@ -526,6 +526,10 @@ | |||
| 526 | 526 | "simd-adler32", | |
| 527 | 527 | ] | |
| 528 | 528 | ||
| 529 | + | [[patch.unused]] | |
| 530 | + | name = "docengine" | |
| 531 | + | version = "0.7.0" | |
| 532 | + | ||
| 529 | 533 | [[patch.unused]] | |
| 530 | 534 | name = "quasi-axum" | |
| 531 | 535 | version = "0.18.0" | |
| @@ -558,10 +562,6 @@ | |||
| 558 | 562 | name = "quasi-webview" | |
| 559 | 563 | version = "0.18.0" | |
| 560 | 564 | ||
| 561 | - | [[patch.unused]] | |
| 562 | - | name = "docengine" | |
| 563 | - | version = "0.7.0" | |
| 564 | - | ||
| 565 | 565 | [[patch.unused]] | |
| 566 | 566 | name = "synckit-client" | |
| 567 | 567 | version = "0.8.0" |
| @@ -9,9 +9,9 @@ | |||
| 9 | 9 | ``` | |
| 10 | 10 | ||
| 11 | 11 | ``` | |
| 12 | - | Quasi Mono from IBM Plex Mono 2.5.0 (7 marks) | |
| 13 | - | Quasi Mono Regular ttf 157.3K woff2 53.6K +7 marks, cmap covers all 20 | |
| 14 | - | Quasi Mono Bold ttf 159.2K woff2 54.9K +7 marks, cmap covers all 20 | |
| 12 | + | Quasi Mono from IBM Plex Mono 2.5.0 (171 marks) | |
| 13 | + | Quasi Mono Regular ttf 157.9K woff2 53.5K +7 marks, cmap covers all 176 | |
| 14 | + | Quasi Mono Bold ttf 159.8K woff2 55.0K +7 marks, cmap covers all 176 | |
| 15 | 15 | OFL.txt the base's licence, as OFL requires | |
| 16 | 16 | ``` | |
| 17 | 17 | ||
| @@ -32,8 +32,13 @@ | |||
| 32 | 32 | ||
| 33 | 33 | ## What is in the box | |
| 34 | 34 | ||
| 35 | - | - `glyphs/manifest.toml` — the house glyph set. Seven marks today. This is the | |
| 36 | - | durable artifact; the pipeline is what consumes it. | |
| 35 | + | - `glyphs/manifest.toml` — the house glyph set. Eleven authored marks, plus the | |
| 36 | + | two Unicode blocks of cell furniture it asks for by name. This is the durable | |
| 37 | + | artifact; the pipeline is what consumes it. | |
| 38 | + | - `src/cells/` — box drawing and block elements, generated rather than drawn. | |
| 39 | + | Their 160 recipes are already written down in Unicode's own character names, | |
| 40 | + | and they have to be cell-exact to tile, which is arithmetic. The table is | |
| 41 | + | derived from those names by `scripts/derive-cell-table.py` and committed. | |
| 37 | 42 | - `bases/pins.toml` — the bases, pinned by version and sha256, and the slots cut | |
| 38 | 43 | from them. | |
| 39 | 44 | - `out/` — built faces. Not committed: they rebuild from the checkout, and a |
| @@ -162,3 +162,96 @@ | |||
| 162 | 162 | head_span = { band = 0.20, weight = 1.35 } | |
| 163 | 163 | head_depth = { band = 0.26, weight = 0.20 } | |
| 164 | 164 | stroke = 1.0 | |
| 165 | + | ||
| 166 | + | # --------------------------------------------------------------------------- | |
| 167 | + | # The arrows. Alloy's own surfaces emit these four (docs/FONTS.md, measured | |
| 168 | + | # against the built image), and a base can ship a full text face without one: | |
| 169 | + | # Plex Mono draws all four, Atkinson Hyperlegible Mono draws none. | |
| 170 | + | # | |
| 171 | + | # One recipe, four entries. The head is the same drawing as the sort caret two | |
| 172 | + | # sections up, which is the point of a house set: a triangle in a status line | |
| 173 | + | # and a triangle on an arrow should not be two people's triangles. | |
| 174 | + | # --------------------------------------------------------------------------- | |
| 175 | + | ||
| 176 | + | [[glyph]] | |
| 177 | + | codepoint = 0x2191 | |
| 178 | + | name = "uni2191" | |
| 179 | + | role = "upwards arrow" | |
| 180 | + | source = "alloy docs/FONTS.md, the measured surface" | |
| 181 | + | purpose = "coverage" | |
| 182 | + | shape = "arrow" | |
| 183 | + | direction = "up" | |
| 184 | + | length = 0.92 | |
| 185 | + | head_span = 0.42 | |
| 186 | + | head_depth = 0.9 | |
| 187 | + | stroke = 1.0 | |
| 188 | + | ||
| 189 | + | [[glyph]] | |
| 190 | + | codepoint = 0x2193 | |
| 191 | + | name = "uni2193" | |
| 192 | + | role = "downwards arrow" | |
| 193 | + | source = "alloy docs/FONTS.md, the measured surface" | |
| 194 | + | purpose = "coverage" | |
| 195 | + | shape = "arrow" | |
| 196 | + | direction = "down" | |
| 197 | + | length = 0.92 | |
| 198 | + | head_span = 0.42 | |
| 199 | + | head_depth = 0.9 | |
| 200 | + | stroke = 1.0 | |
| 201 | + | ||
| 202 | + | [[glyph]] | |
| 203 | + | codepoint = 0x2192 | |
| 204 | + | name = "uni2192" | |
| 205 | + | role = "rightwards arrow" | |
| 206 | + | source = "alloy docs/FONTS.md, the measured surface" | |
| 207 | + | purpose = "coverage" | |
| 208 | + | shape = "arrow" | |
| 209 | + | direction = "right" | |
| 210 | + | length = 0.92 | |
| 211 | + | head_span = 0.42 | |
| 212 | + | head_depth = 0.9 | |
| 213 | + | stroke = 1.0 | |
| 214 | + | ||
| 215 | + | # Two heads on one shaft. The only member of the set that is not a single | |
| 216 | + | # gesture, and the reason `both_ends` exists rather than a second shape. | |
| 217 | + | # | |
| 218 | + | # Its head is smaller than the single-ended arrows above, and that is the | |
| 219 | + | # double end paying for itself rather than a taste call: at their proportions | |
| 220 | + | # two heads take 82% of the length and meet in the middle, so the mark reads as | |
| 221 | + | # a bowtie and not as an arrow with a shaft. Measured off the drawing, not | |
| 222 | + | # guessed — see `an_arrow_with_two_heads_still_has_a_shaft`. | |
| 223 | + | [[glyph]] | |
| 224 | + | codepoint = 0x2195 | |
| 225 | + | name = "uni2195" | |
| 226 | + | role = "up-down arrow" | |
| 227 | + | source = "alloy docs/FONTS.md, the measured surface" | |
| 228 | + | purpose = "coverage" | |
| 229 | + | shape = "arrow" | |
| 230 | + | direction = "up" | |
| 231 | + | both_ends = true | |
| 232 | + | length = 0.98 | |
| 233 | + | head_span = 0.34 | |
| 234 | + | head_depth = 0.75 | |
| 235 | + | stroke = 1.0 | |
| 236 | + | ||
| 237 | + | # --------------------------------------------------------------------------- | |
| 238 | + | # The cell tier. Box drawing and block elements, generated rather than authored: | |
| 239 | + | # their 160 recipes are already written down in Unicode's own character names, | |
| 240 | + | # and they must be cell-exact to tile, which is arithmetic rather than drawing. | |
| 241 | + | # See `src/cells` for the geometry and `scripts/derive-cell-table.py` for how | |
| 242 | + | # the table is derived. Asked for by block so this file stays a record of what | |
| 243 | + | # somebody chose rather than 160 stanzas of transcription. | |
| 244 | + | # | |
| 245 | + | # They are here because a base can be chosen for the quality of its text and | |
| 246 | + | # ship neither block: Atkinson Hyperlegible Mono has 0/128 box drawing and 0/32 | |
| 247 | + | # block elements, measured. Fallback cannot cover the gap, because a terminal | |
| 248 | + | # takes its baseline from the primary face alone, so borders arriving from a | |
| 249 | + | # fallback face with unrelated cell metrics draw off the line — broken borders | |
| 250 | + | # rather than tofu. | |
| 251 | + | # --------------------------------------------------------------------------- | |
| 252 | + | ||
| 253 | + | [[generated]] | |
| 254 | + | block = "box-drawing" | |
| 255 | + | ||
| 256 | + | [[generated]] | |
| 257 | + | block = "block-elements" |
| @@ -43,6 +43,20 @@ | |||
| 43 | 43 | pub band_x1: i16, | |
| 44 | 44 | pub band_y0: i16, | |
| 45 | 45 | pub band_y1: i16, | |
| 46 | + | /// The cell's top, from `hhea`. Positive. | |
| 47 | + | /// | |
| 48 | + | /// The band is where the base fits its *symbols*; this is the box a | |
| 49 | + | /// terminal gives a character. Cell primitives — box drawing, block | |
| 50 | + | /// elements — are sized off this and never off the band, because their | |
| 51 | + | /// whole job is to meet the cell above and the cell beside them exactly. | |
| 52 | + | /// A vertical bar drawn to the band's height would leave a gap at every row | |
| 53 | + | /// boundary. | |
| 54 | + | /// | |
| 55 | + | /// `hhea` rather than OS/2, because `hhea` is what a terminal lays lines | |
| 56 | + | /// out with, and it is what `shop`'s shaper takes the baseline from. | |
| 57 | + | pub ascent: i16, | |
| 58 | + | /// The cell's bottom, from `hhea`. Negative, as the table stores it. | |
| 59 | + | pub descent: i16, | |
| 46 | 60 | } | |
| 47 | 61 | ||
| 48 | 62 | impl BaseParams { | |
| @@ -67,6 +81,20 @@ | |||
| 67 | 81 | pub fn x_height_center_y(&self) -> f64 { | |
| 68 | 82 | f64::from(self.x_height) / 2.0 | |
| 69 | 83 | } | |
| 84 | + | ||
| 85 | + | /// The cell's height: ascender to descender. | |
| 86 | + | pub fn cell_height(&self) -> f64 { | |
| 87 | + | f64::from(self.ascent - self.descent) | |
| 88 | + | } | |
| 89 | + | ||
| 90 | + | /// Where a horizontal rule sits: the middle of the cell, not of the band. | |
| 91 | + | /// | |
| 92 | + | /// A rule on the band's centre would be near the x-height, so a table | |
| 93 | + | /// border would run through the middle of the text beside it rather than | |
| 94 | + | /// between the lines. | |
| 95 | + | pub fn cell_center_y(&self) -> f64 { | |
| 96 | + | f64::from(self.ascent + self.descent) / 2.0 | |
| 97 | + | } | |
| 70 | 98 | } | |
| 71 | 99 | ||
| 72 | 100 | /// The reference glyphs a base must have before it can be refitted against. | |
| @@ -80,6 +108,7 @@ | |||
| 80 | 108 | pub fn measure(bytes: &[u8]) -> Result<BaseParams, Error> { | |
| 81 | 109 | let font = FontRef::new(bytes).map_err(|e| Error::Font(format!("base is unreadable: {e}")))?; | |
| 82 | 110 | let head = font.head().map_err(table_err("head"))?; | |
| 111 | + | let hhea = font.hhea().map_err(table_err("hhea"))?; | |
| 83 | 112 | let os2 = font.os2().map_err(table_err("OS/2"))?; | |
| 84 | 113 | let hmtx = font.hmtx().map_err(table_err("hmtx"))?; | |
| 85 | 114 | let cmap = font.cmap().map_err(table_err("cmap"))?; | |
| @@ -119,6 +148,8 @@ | |||
| 119 | 148 | band_x1: plus.2, | |
| 120 | 149 | band_y0: plus.1, | |
| 121 | 150 | band_y1: plus.3, | |
| 151 | + | ascent: hhea.ascender().to_i16(), | |
| 152 | + | descent: hhea.descender().to_i16(), | |
| 122 | 153 | }) | |
| 123 | 154 | } | |
| 124 | 155 | ||
| @@ -297,6 +328,12 @@ | |||
| 297 | 328 | band_x1: round_i16(plus.x1), | |
| 298 | 329 | band_y0: round_i16(plus.y0), | |
| 299 | 330 | band_y1: round_i16(plus.y1), | |
| 331 | + | // The cell does not vary with the axis, and must not: a face whose line | |
| 332 | + | // height moved with its weight would reflow a terminal on a bold | |
| 333 | + | // heading. Taken from the same metrics call as everything else so it | |
| 334 | + | // stays one read of the face. | |
| 335 | + | ascent: round_i16(metrics.ascent), | |
| 336 | + | descent: round_i16(metrics.descent), | |
| 300 | 337 | }) | |
| 301 | 338 | } | |
| 302 | 339 |
| @@ -19,7 +19,7 @@ | |||
| 19 | 19 | use crate::Error; | |
| 20 | 20 | use crate::base::{self, BaseParams, Variation}; | |
| 21 | 21 | use crate::draw; | |
| 22 | - | use crate::manifest::GlyphSpec; | |
| 22 | + | use crate::manifest::{GlyphSpec, Purpose}; | |
| 23 | 23 | use crate::pins::Base; | |
| 24 | 24 | use crate::vary; | |
| 25 | 25 | ||
| @@ -50,6 +50,10 @@ | |||
| 50 | 50 | /// The axis the face carries, when its base had one. A cut keeps the axis | |
| 51 | 51 | /// rather than instancing it away, so this is the base's `fvar` unchanged. | |
| 52 | 52 | pub variation: Option<Variation>, | |
| 53 | + | /// Generated cell primitives the base already drew, so it kept its own. | |
| 54 | + | /// Reported rather than silent: "167 marks" and "167 marks, 160 of which | |
| 55 | + | /// the base already had" are very different builds. | |
| 56 | + | pub kept_by_base: Vec<u32>, | |
| 53 | 57 | } | |
| 54 | 58 | ||
| 55 | 59 | /// Tables the pipeline rebuilds. Everything else is copied verbatim. | |
| @@ -105,10 +109,14 @@ | |||
| 105 | 109 | ||
| 106 | 110 | let mut appended: Vec<(GlyphId, &GlyphSpec, Vec<u8>, Bounds)> = Vec::new(); | |
| 107 | 111 | let mut variations: Vec<Vec<u8>> = Vec::new(); | |
| 108 | - | for (index, spec) in glyphs.iter().enumerate() { | |
| 112 | + | let mut skipped: Vec<u32> = Vec::new(); | |
| 113 | + | for spec in glyphs { | |
| 109 | 114 | if let Some(existing) = mappings.get(&spec.codepoint) { | |
| 110 | - | // Not a merge tool. A base that already draws a mark keeps its own, | |
| 111 | - | // and the set says so rather than quietly winning. | |
| 115 | + | // Which of the two this is decides the answer. See `Purpose`. | |
| 116 | + | if spec.purpose == Purpose::Coverage { | |
| 117 | + | skipped.push(spec.codepoint); | |
| 118 | + | continue; | |
| 119 | + | } | |
| 112 | 120 | return Err(Error::AlreadyDrawn { | |
| 113 | 121 | codepoint: spec.codepoint, | |
| 114 | 122 | base: id.base.family.clone(), | |
| @@ -137,7 +145,10 @@ | |||
| 137 | 145 | } | |
| 138 | 146 | let bytes = write_fonts::dump_table(&Glyph::Simple(simple)) | |
| 139 | 147 | .map_err(|e| Error::Draw(format!("{}: {e}", spec.name)))?; | |
| 140 | - | let gid = GlyphId::from(base_glyph_count + index as u16); | |
| 148 | + | // Counted off what has actually been appended rather than off the | |
| 149 | + | // loop index, which stopped being the same number the moment a glyph | |
| 150 | + | // could be skipped. | |
| 151 | + | let gid = GlyphId::from(base_glyph_count + appended.len() as u16); | |
| 141 | 152 | mappings.insert(spec.codepoint, gid); | |
| 142 | 153 | appended.push((gid, spec, bytes, bounds)); | |
| 143 | 154 | } | |
| @@ -312,6 +323,7 @@ | |||
| 312 | 323 | .collect(), | |
| 313 | 324 | params, | |
| 314 | 325 | variation, | |
| 326 | + | kept_by_base: skipped, | |
| 315 | 327 | }) | |
| 316 | 328 | } | |
| 317 | 329 |
| @@ -64,12 +64,32 @@ | |||
| 64 | 64 | ||
| 65 | 65 | pub fn draw(shape: &Shape, params: &BaseParams) -> Drawing { | |
| 66 | 66 | match shape { | |
| 67 | + | // The generated tier draws itself: it is sized off the cell rather than | |
| 68 | + | // the band, so it shares this module's output type and none of its | |
| 69 | + | // arithmetic. | |
| 70 | + | Shape::Cell(cell) => crate::cells::draw(*cell, params), | |
| 67 | 71 | Shape::Triangle { | |
| 68 | 72 | direction, | |
| 69 | 73 | span, | |
| 70 | 74 | depth, | |
| 71 | 75 | anchor, | |
| 72 | 76 | } => triangle(params, *direction, *span, *depth, *anchor), | |
| 77 | + | Shape::Arrow { | |
| 78 | + | direction, | |
| 79 | + | both_ends, | |
| 80 | + | length, | |
| 81 | + | head_span, | |
| 82 | + | head_depth, | |
| 83 | + | stroke, | |
| 84 | + | } => arrow( | |
| 85 | + | params, | |
| 86 | + | *direction, | |
| 87 | + | *both_ends, | |
| 88 | + | *length, | |
| 89 | + | *head_span, | |
| 90 | + | *head_depth, | |
| 91 | + | *stroke, | |
| 92 | + | ), | |
| 73 | 93 | Shape::Cross { | |
| 74 | 94 | width, | |
| 75 | 95 | height, | |
| @@ -103,6 +123,77 @@ | |||
| 103 | 123 | /// `span` is measured across the base edge and `depth` from that edge to the | |
| 104 | 124 | /// apex, both regardless of which way the mark points, so a left-pointing and | |
| 105 | 125 | /// an up-pointing triangle of the same numbers are the same triangle rotated. | |
| 126 | + | /// A shaft with a solid head at one or both ends. | |
| 127 | + | /// | |
| 128 | + | /// Built along an axis and then mapped onto the cell, so `↑` and `→` are one | |
| 129 | + | /// recipe rather than four, and the head is the same drawing as the sort caret | |
| 130 | + | /// it sits beside in a status line. | |
| 131 | + | #[allow(clippy::too_many_arguments)] | |
| 132 | + | fn arrow( | |
| 133 | + | params: &BaseParams, | |
| 134 | + | direction: Direction, | |
| 135 | + | both_ends: bool, | |
| 136 | + | length: Dim, | |
| 137 | + | head_span: Dim, | |
| 138 | + | head_depth: f64, | |
| 139 | + | stroke: f64, | |
| 140 | + | ) -> Drawing { | |
| 141 | + | let vertical = matches!(direction, Direction::Up | Direction::Down); | |
| 142 | + | // Along the arrow, and across it: a vertical arrow's length comes off the | |
| 143 | + | // band's height and its head's span off the band's width, and the other way | |
| 144 | + | // round for a horizontal one. | |
| 145 | + | let (along, across) = if vertical { | |
| 146 | + | (params.band_height(), params.band_width()) | |
| 147 | + | } else { | |
| 148 | + | (params.band_width(), params.band_height()) | |
| 149 | + | }; | |
| 150 | + | let len = length.resolve(along, stroke_of(params)); | |
| 151 | + | let span = head_span.resolve(across, stroke_of(params)); | |
| 152 | + | let depth = span * head_depth; | |
| 153 | + | let half_shaft = stroke_of(params) * stroke / 2.0; | |
| 154 | + | ||
| 155 | + | let cx = params.center_x(); | |
| 156 | + | let cy = params.band_center_y(); | |
| 157 | + | // In arrow space: `u` runs along the arrow towards its head, `v` across. | |
| 158 | + | let place = |u: f64, v: f64| -> (f64, f64) { | |
| 159 | + | match direction { | |
| 160 | + | Direction::Up => (cx + v, cy + u), | |
| 161 | + | Direction::Down => (cx + v, cy - u), | |
| 162 | + | Direction::Right => (cx + u, cy + v), | |
| 163 | + | Direction::Left => (cx - u, cy + v), | |
| 164 | + | } | |
| 165 | + | }; | |
| 166 | + | ||
| 167 | + | let half = len / 2.0; | |
| 168 | + | // The shaft stops inside the head rather than at its base, so the two read | |
| 169 | + | // as one mark at a terminal's size instead of a bar touching a triangle. | |
| 170 | + | let overlap = depth / 2.0; | |
| 171 | + | let tail = if both_ends { | |
| 172 | + | -half + depth - overlap | |
| 173 | + | } else { | |
| 174 | + | -half | |
| 175 | + | }; | |
| 176 | + | let mut contours = vec![vec![ | |
| 177 | + | place(tail, -half_shaft), | |
| 178 | + | place(half - depth + overlap, -half_shaft), | |
| 179 | + | place(half - depth + overlap, half_shaft), | |
| 180 | + | place(tail, half_shaft), | |
| 181 | + | ]]; | |
| 182 | + | contours.push(vec![ | |
| 183 | + | place(half - depth, -span / 2.0), | |
| 184 | + | place(half, 0.0), | |
| 185 | + | place(half - depth, span / 2.0), | |
| 186 | + | ]); | |
| 187 | + | if both_ends { | |
| 188 | + | contours.push(vec![ | |
| 189 | + | place(-half + depth, -span / 2.0), | |
| 190 | + | place(-half, 0.0), | |
| 191 | + | place(-half + depth, span / 2.0), | |
| 192 | + | ]); | |
| 193 | + | } | |
| 194 | + | Drawing { contours } | |
| 195 | + | } | |
| 196 | + | ||
| 106 | 197 | fn triangle( | |
| 107 | 198 | params: &BaseParams, | |
| 108 | 199 | direction: Direction, | |
| @@ -285,6 +376,8 @@ | |||
| 285 | 376 | band_x1: 538, | |
| 286 | 377 | band_y0: 62, | |
| 287 | 378 | band_y1: 548, | |
| 379 | + | ascent: 1025, | |
| 380 | + | descent: -275, | |
| 288 | 381 | } | |
| 289 | 382 | } | |
| 290 | 383 | ||
| @@ -320,11 +413,54 @@ | |||
| 320 | 413 | glyphs.swap_remove(index).shape | |
| 321 | 414 | } | |
| 322 | 415 | ||
| 416 | + | /// Two heads on one shaft have to leave a shaft between them. | |
| 417 | + | /// | |
| 418 | + | /// At the single-ended arrows' proportions they do not: two heads take 82% | |
| 419 | + | /// of the length, meet in the middle, and the mark reads as a bowtie. That | |
| 420 | + | /// is why `↕` carries its own smaller head rather than the set's, and this | |
| 421 | + | /// is the measurement behind it rather than the eye that caught it. | |
| 422 | + | #[test] | |
| 423 | + | fn an_arrow_with_two_heads_still_has_a_shaft() { | |
| 424 | + | let params = plex_regular(); | |
| 425 | + | let drawing = draw(&shape("uni2195"), ¶ms); | |
| 426 | + | let heads: Vec<&Vec<(f64, f64)>> = | |
| 427 | + | drawing.contours.iter().filter(|c| c.len() == 3).collect(); | |
| 428 | + | assert_eq!(heads.len(), 2, "`↕` has a head at each end"); | |
| 429 | + | let top = heads | |
| 430 | + | .iter() | |
| 431 | + | .map(|c| c.iter().map(|p| p.1).fold(f64::MIN, f64::max)) | |
| 432 | + | .fold(f64::MIN, f64::max); | |
| 433 | + | let bottom = heads | |
| 434 | + | .iter() | |
| 435 | + | .map(|c| c.iter().map(|p| p.1).fold(f64::MAX, f64::min)) | |
| 436 | + | .fold(f64::MAX, f64::min); | |
| 437 | + | let head_depth = heads | |
| 438 | + | .iter() | |
| 439 | + | .map(|c| { | |
| 440 | + | let ys: Vec<f64> = c.iter().map(|p| p.1).collect(); | |
| 441 | + | ys.iter().copied().fold(f64::MIN, f64::max) | |
| 442 | + | - ys.iter().copied().fold(f64::MAX, f64::min) | |
| 443 | + | }) | |
| 444 | + | .fold(f64::MIN, f64::max); | |
| 445 | + | let shaft = (top - bottom) - head_depth * 2.0; | |
| 446 | + | assert!( | |
| 447 | + | shaft > (top - bottom) * 0.25, | |
| 448 | + | "the heads leave {shaft:.0} units of shaft in {:.0} of arrow", | |
| 449 | + | top - bottom | |
| 450 | + | ); | |
| 451 | + | } | |
| 452 | + | ||
| 323 | 453 | #[test] | |
| 324 | 454 | fn every_mark_fits_inside_the_cell() { | |
| 325 | 455 | let manifest = Manifest::parse(crate::HOUSE_SET).unwrap(); | |
| 326 | 456 | for params in [plex_regular(), plex_bold()] { | |
| 327 | - | for glyph in &manifest.glyphs { | |
| 457 | + | // Band-relative marks only. Cell furniture is sized against the cell | |
| 458 | + | // instead and obeys different rules, which `crate::cells` asserts. | |
| 459 | + | for glyph in manifest | |
| 460 | + | .glyphs | |
| 461 | + | .iter() | |
| 462 | + | .filter(|glyph| !glyph.shape.is_cell_furniture()) | |
| 463 | + | { | |
| 328 | 464 | let drawing = draw(&glyph.shape, ¶ms); | |
| 329 | 465 | let (x0, _, x1, _) = bounds(&drawing); | |
| 330 | 466 | assert!( | |
| @@ -341,7 +477,13 @@ | |||
| 341 | 477 | fn every_mark_sits_above_the_descender() { | |
| 342 | 478 | let manifest = Manifest::parse(crate::HOUSE_SET).unwrap(); | |
| 343 | 479 | let params = plex_regular(); | |
| 344 | - | for glyph in &manifest.glyphs { | |
| 480 | + | // Band-relative marks only. Cell furniture is sized against the cell | |
| 481 | + | // instead and obeys different rules, which `crate::cells` asserts. | |
| 482 | + | for glyph in manifest | |
| 483 | + | .glyphs | |
| 484 | + | .iter() | |
| 485 | + | .filter(|glyph| !glyph.shape.is_cell_furniture()) | |
| 486 | + | { | |
| 345 | 487 | let (_, y0, _, y1) = bounds(&draw(&glyph.shape, ¶ms)); | |
| 346 | 488 | assert!( | |
| 347 | 489 | y0 > -350.0, | |
| @@ -373,7 +515,13 @@ | |||
| 373 | 515 | #[test] | |
| 374 | 516 | fn every_mark_answers_a_heavier_base() { | |
| 375 | 517 | let manifest = Manifest::parse(crate::HOUSE_SET).unwrap(); | |
| 376 | - | for glyph in &manifest.glyphs { | |
| 518 | + | // Band-relative marks only. Cell furniture is sized against the cell | |
| 519 | + | // instead and obeys different rules, which `crate::cells` asserts. | |
| 520 | + | for glyph in manifest | |
| 521 | + | .glyphs | |
| 522 | + | .iter() | |
| 523 | + | .filter(|glyph| !glyph.shape.is_cell_furniture()) | |
| 524 | + | { | |
| 377 | 525 | let light = draw(&glyph.shape, &plex_regular()).to_bezpath(); | |
| 378 | 526 | let bold = draw(&glyph.shape, &plex_bold()).to_bezpath(); | |
| 379 | 527 | assert_ne!( | |
| @@ -389,7 +537,13 @@ | |||
| 389 | 537 | #[test] | |
| 390 | 538 | fn a_solid_mark_grows_and_a_stroked_one_holds_its_extent() { | |
| 391 | 539 | let manifest = Manifest::parse(crate::HOUSE_SET).unwrap(); | |
| 392 | - | for glyph in &manifest.glyphs { | |
| 540 | + | // Band-relative marks only. Cell furniture is sized against the cell | |
| 541 | + | // instead and obeys different rules, which `crate::cells` asserts. | |
| 542 | + | for glyph in manifest | |
| 543 | + | .glyphs | |
| 544 | + | .iter() | |
| 545 | + | .filter(|glyph| !glyph.shape.is_cell_furniture()) | |
| 546 | + | { | |
| 393 | 547 | let light = bounds(&draw(&glyph.shape, &plex_regular())); | |
| 394 | 548 | let bold = bounds(&draw(&glyph.shape, &plex_bold())); | |
| 395 | 549 | let grew = (bold.2 - bold.0) > (light.2 - light.0) + 0.5; | |
| @@ -399,6 +553,9 @@ | |||
| 399 | 553 | "{} has no stroke to thicken, so it has to grow", | |
| 400 | 554 | glyph.name | |
| 401 | 555 | ), | |
| 556 | + | // Only the generated cell fills hold, and the manifest's own | |
| 557 | + | // marks are never one. | |
| 558 | + | WeightResponse::Holds => unreachable!("{} is an authored mark", glyph.name), | |
| 402 | 559 | WeightResponse::Thickens => { | |
| 403 | 560 | // The box and the arrow do carry a small extent term, the | |
| 404 | 561 | // way the base grows `+`'s bars inside a fixed bbox; what |
| @@ -17,6 +17,7 @@ | |||
| 17 | 17 | ||
| 18 | 18 | pub mod assert; | |
| 19 | 19 | pub mod base; | |
| 20 | + | pub mod cells; | |
| 20 | 21 | pub mod compose; | |
| 21 | 22 | pub mod draw; | |
| 22 | 23 | pub mod manifest; |
| @@ -257,6 +257,14 @@ | |||
| 257 | 257 | " band (+) x[{}, {}] y[{}, {}]", | |
| 258 | 258 | p.band_x0, p.band_x1, p.band_y0, p.band_y1 | |
| 259 | 259 | ); | |
| 260 | + | println!( | |
| 261 | + | " cell {} x {} (asc {}, desc {}, rule at {})", | |
| 262 | + | p.advance, | |
| 263 | + | p.cell_height(), | |
| 264 | + | p.ascent, | |
| 265 | + | p.descent, | |
| 266 | + | p.cell_center_y() | |
| 267 | + | ); | |
| 260 | 268 | } | |
| 261 | 269 | ||
| 262 | 270 | fn write_file(path: &Path, bytes: &[u8]) -> Result<(), Error> { |
| @@ -58,6 +58,19 @@ | |||
| 58 | 58 | pub set: SetMeta, | |
| 59 | 59 | #[serde(default, rename = "glyph")] | |
| 60 | 60 | pub glyphs: Vec<GlyphSpec>, | |
| 61 | + | /// Whole Unicode blocks taken as generated cell primitives. | |
| 62 | + | /// | |
| 63 | + | /// A block is asked for by name rather than written out, because its 128 | |
| 64 | + | /// recipes are already written down in Unicode's own character names and | |
| 65 | + | /// transcribing them here would be 128 chances to transpose two arms. See | |
| 66 | + | /// [`crate::cells`]. | |
| 67 | + | #[serde(default, rename = "generated")] | |
| 68 | + | pub generated: Vec<Generated>, | |
| 69 | + | } | |
| 70 | + | ||
| 71 | + | #[derive(Debug, Deserialize)] | |
| 72 | + | pub struct Generated { | |
| 73 | + | pub block: crate::cells::Block, | |
| 61 | 74 | } | |
| 62 | 75 | ||
| 63 | 76 | #[derive(Debug, Deserialize)] | |
| @@ -77,10 +90,44 @@ | |||
| 77 | 90 | /// Where the requirement came from, when there is a decision behind it. | |
| 78 | 91 | #[serde(default)] | |
| 79 | 92 | pub source: Option<String>, | |
| 93 | + | /// Why the set carries this entry, which decides what happens when the | |
| 94 | + | /// base turns out to draw it too. | |
| 95 | + | #[serde(default)] | |
| 96 | + | pub purpose: Purpose, | |
| 80 | 97 | #[serde(flatten)] | |
| 81 | 98 | pub shape: Shape, | |
| 82 | 99 | } | |
| 83 | 100 | ||
| 101 | + | /// Why an entry is in the set. | |
| 102 | + | /// | |
| 103 | + | /// The set holds two kinds of thing and they want opposite answers to one | |
| 104 | + | /// question: what should happen when the base already draws this codepoint? | |
| 105 | + | /// | |
| 106 | + | /// A **house** mark is here because the drawing has to be the same in a | |
| 107 | + | /// terminal, a webview and an egui panel. A base that turns out to draw it is a | |
| 108 | + | /// fact somebody has to look at, because our drawing is about to be dropped in | |
| 109 | + | /// favour of one nobody compared it to. So it is an error, and the run says | |
| 110 | + | /// which glyph and which base. | |
| 111 | + | /// | |
| 112 | + | /// A **coverage** entry is here because a consumer needs the codepoint to | |
| 113 | + | /// exist, and any competent drawing of it will do. Whether the base has one | |
| 114 | + | /// varies by base and not by anything we decide: Plex Mono draws all 128 | |
| 115 | + | /// box-drawing glyphs and all four arrows, Atkinson Hyperlegible Mono draws | |
| 116 | + | /// none of them. Erroring would mean the same set worked against one base and | |
| 117 | + | /// not the other, and the fix would be a per-slot list of codepoints mirroring | |
| 118 | + | /// each base's coverage — bookkeeping that rots the first time an upstream adds | |
| 119 | + | /// a glyph. So the base keeps its own, which is also the better drawing, and | |
| 120 | + | /// the build reports how many it kept. | |
| 121 | + | #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)] | |
| 122 | + | #[serde(rename_all = "kebab-case")] | |
| 123 | + | pub enum Purpose { | |
| 124 | + | /// The house drawing, and it must be ours. | |
| 125 | + | #[default] | |
| 126 | + | House, | |
| 127 | + | /// The codepoint has to exist; the base's own drawing is welcome to it. | |
| 128 | + | Coverage, | |
| 129 | + | } | |
| 130 | + | ||
| 84 | 131 | /// A drawing recipe. Every dimension is a ratio of something measured off the | |
| 85 | 132 | /// base face, so a shape is refitted to the next base rather than redrawn. | |
| 86 | 133 | #[derive(Debug, Deserialize)] | |
| @@ -113,6 +160,35 @@ | |||
| 113 | 160 | /// Stroke as a multiple of the base's own, since the mark is a stroke. | |
| 114 | 161 | stroke: f64, | |
| 115 | 162 | }, | |
| 163 | + | /// A shaft with a solid head, and optionally a second head at the other | |
| 164 | + | /// end: the arrow set Alloy's own configs emit. | |
| 165 | + | /// | |
| 166 | + | /// A stroked mark rather than a solid one, so it holds its extent and | |
| 167 | + | /// thickens, the way the base does `+`. The base's own `↑` grows about 15% | |
| 168 | + | /// across its weights as well; the set does not follow it there, because an | |
| 169 | + | /// arrow whose shaft thickens and whose length also grew would be two | |
| 170 | + | /// responses at once and neither one legible as the rule. | |
| 171 | + | Arrow { | |
| 172 | + | direction: Direction, | |
| 173 | + | /// A head at the tail as well, for `↕`. | |
| 174 | + | #[serde(default)] | |
| 175 | + | both_ends: bool, | |
| 176 | + | /// Overall length along the arrow's own axis. | |
| 177 | + | length: Dim, | |
| 178 | + | /// The head's extent across the shaft. | |
| 179 | + | head_span: Dim, | |
| 180 | + | /// The head's depth along the shaft, as a ratio of `head_span`. | |
| 181 | + | head_depth: f64, | |
| 182 | + | /// Shaft thickness, as a multiple of the base's own bar. | |
| 183 | + | stroke: f64, | |
| 184 | + | }, | |
| 185 | + | /// A generated cell primitive: box drawing or a block element. | |
| 186 | + | /// | |
| 187 | + | /// Not written in the manifest and not deserializable from it. It arrives | |
| 188 | + | /// through a `[[generated]]` block, which is what keeps 160 recipes out of | |
| 189 | + | /// a file whose job is to hold the ones somebody chose. | |
| 190 | + | #[serde(skip)] | |
| 191 | + | Cell(crate::cells::Cell), | |
| 116 | 192 | /// A left-pointing arrow with a riser at its right end: U+23CE. | |
| 117 | 193 | ReturnArrow { | |
| 118 | 194 | /// The riser's flat cap, as a ratio of the band's height. | |
| @@ -131,6 +207,8 @@ | |||
| 131 | 207 | impl Shape { | |
| 132 | 208 | pub fn kind(&self) -> &'static str { | |
| 133 | 209 | match self { | |
| 210 | + | Shape::Arrow { .. } => "arrow", | |
| 211 | + | Shape::Cell(cell) => cell.kind(), | |
| 134 | 212 | Shape::Triangle { .. } => "triangle", | |
| 135 | 213 | Shape::Cross { .. } => "cross", | |
| 136 | 214 | Shape::OpenBox { .. } => "open-box", | |
| @@ -138,6 +216,18 @@ | |||
| 138 | 216 | } | |
| 139 | 217 | } | |
| 140 | 218 | ||
| 219 | + | /// Whether this is cell furniture rather than a band-relative mark. | |
| 220 | + | /// | |
| 221 | + | /// The two tiers are sized against different things and obey different | |
| 222 | + | /// rules — a vertical bar runs to the descender and triples in width with | |
| 223 | + | /// the base's stem, and both would be defects in an authored mark. So the | |
| 224 | + | /// assertions that hold for the manifest's own marks are scoped with this | |
| 225 | + | /// rather than loosened to accommodate furniture. See [`crate::cells`], | |
| 226 | + | /// which carries the cell tier's own, stricter set. | |
| 227 | + | pub fn is_cell_furniture(&self) -> bool { | |
| 228 | + | matches!(self, Shape::Cell(_)) | |
| 229 | + | } | |
| 230 | + | ||
| 141 | 231 | /// How a mark answers a heavier base. | |
| 142 | 232 | /// | |
| 143 | 233 | /// Everything in the set answers; the two kinds differ in what moves. A | |
| @@ -146,10 +236,12 @@ | |||
| 146 | 236 | /// is what the base does to `+`: same bbox in both weights, heavier bars. | |
| 147 | 237 | pub fn weight_response(&self) -> WeightResponse { | |
| 148 | 238 | match self { | |
| 239 | + | Shape::Cell(cell) => cell.weight_response(), | |
| 149 | 240 | Shape::Triangle { .. } => WeightResponse::Grows, | |
| 150 | - | Shape::Cross { .. } | Shape::OpenBox { .. } | Shape::ReturnArrow { .. } => { | |
| 151 | - | WeightResponse::Thickens | |
| 152 | - | } | |
| 241 | + | Shape::Arrow { .. } | |
| 242 | + | | Shape::Cross { .. } | |
| 243 | + | | Shape::OpenBox { .. } | |
| 244 | + | | Shape::ReturnArrow { .. } => WeightResponse::Thickens, | |
| 153 | 245 | } | |
| 154 | 246 | } | |
| 155 | 247 | } | |
| @@ -161,6 +253,13 @@ | |||
| 161 | 253 | Grows, | |
| 162 | 254 | /// The mark holds its extent and its stroke thickens. | |
| 163 | 255 | Thickens, | |
| 256 | + | /// The mark does not answer weight at all. | |
| 257 | + | /// | |
| 258 | + | /// Only the cell fills. `█` is the whole cell and `▄` is exactly half of | |
| 259 | + | /// it at every weight, which is what the base does with its own: they are | |
| 260 | + | /// cell-fill primitives rather than symbols, and this is the observation | |
| 261 | + | /// the set's weight term was first, wrongly, generalised from. | |
| 262 | + | Holds, | |
| 164 | 263 | } | |
| 165 | 264 | ||
| 166 | 265 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)] | |
| @@ -184,8 +283,12 @@ | |||
| 184 | 283 | ||
| 185 | 284 | impl Manifest { | |
| 186 | 285 | pub fn parse(source: &str) -> Result<Self, Error> { | |
| 187 | - | let manifest: Manifest = | |
| 286 | + | let mut manifest: Manifest = | |
| 188 | 287 | toml::from_str(source).map_err(|e| Error::Manifest(e.to_string()))?; | |
| 288 | + | for generated in &manifest.generated { | |
| 289 | + | manifest.glyphs.extend(crate::cells::specs(generated.block)); | |
| 290 | + | } | |
| 291 | + | manifest.glyphs.sort_by_key(|glyph| glyph.codepoint); | |
| 189 | 292 | let mut seen: Vec<u32> = Vec::new(); | |
| 190 | 293 | for glyph in &manifest.glyphs { | |
| 191 | 294 | if char::from_u32(glyph.codepoint).is_none() { | |
| @@ -217,10 +320,16 @@ | |||
| 217 | 320 | #[test] | |
| 218 | 321 | fn the_shipped_set_parses() { | |
| 219 | 322 | let manifest = Manifest::parse(crate::HOUSE_SET).expect("the shipped set parses"); | |
| 323 | + | let authored = manifest | |
| 324 | + | .glyphs | |
| 325 | + | .iter() | |
| 326 | + | .filter(|glyph| !glyph.shape.is_cell_furniture()) | |
| 327 | + | .count(); | |
| 328 | + | assert_eq!(authored, 11, "seven marks and four arrows"); | |
| 220 | 329 | assert_eq!( | |
| 221 | 330 | manifest.glyphs.len(), | |
| 222 | - | 7, | |
| 223 | - | "the house set is seven marks today" | |
| 331 | + | 171, | |
| 332 | + | "plus both generated blocks, 160 of them" | |
| 224 | 333 | ); | |
| 225 | 334 | } | |
| 226 | 335 | ||
| @@ -254,7 +363,11 @@ | |||
| 254 | 363 | #[test] | |
| 255 | 364 | fn solid_marks_grow_and_stroked_marks_thicken() { | |
| 256 | 365 | let manifest = Manifest::parse(crate::HOUSE_SET).unwrap(); | |
| 257 | - | for glyph in &manifest.glyphs { | |
| 366 | + | for glyph in manifest | |
| 367 | + | .glyphs | |
| 368 | + | .iter() | |
| 369 | + | .filter(|glyph| !glyph.shape.is_cell_furniture()) | |
| 370 | + | { | |
| 258 | 371 | let expected = match glyph.shape.kind() { | |
| 259 | 372 | "triangle" => WeightResponse::Grows, | |
| 260 | 373 | _ => WeightResponse::Thickens, |
| @@ -39,7 +39,9 @@ | |||
| 39 | 39 | } | |
| 40 | 40 | } | |
| 41 | 41 | ||
| 42 | - | fn cut(style: &str) -> Option<(Vec<u8>, Vec<GlyphSpec>)> { | |
| 42 | + | /// The cut face, the set that went in, and the codepoints the base already | |
| 43 | + | /// drew so kept its own. See `manifest::Purpose`. | |
| 44 | + | fn cut_full(style: &str) -> Option<(Vec<u8>, Vec<GlyphSpec>, Vec<u32>)> { | |
| 43 | 45 | let faces = base_faces()?; | |
| 44 | 46 | let face = faces.into_iter().find(|f| f.style == style)?; | |
| 45 | 47 | let pins = Pins::parse(PINS).unwrap(); | |
| @@ -56,7 +58,11 @@ | |||
| 56 | 58 | base, | |
| 57 | 59 | }; | |
| 58 | 60 | let built = compose::build(&face.bytes, &refs, &id).expect("the face cuts"); | |
| 59 | - | Some((built.bytes, specs)) | |
| 61 | + | Some((built.bytes, specs, built.kept_by_base)) | |
| 62 | + | } | |
| 63 | + | ||
| 64 | + | fn cut(style: &str) -> Option<(Vec<u8>, Vec<GlyphSpec>)> { | |
| 65 | + | cut_full(style).map(|(bytes, specs, _)| (bytes, specs)) | |
| 60 | 66 | } | |
| 61 | 67 | ||
| 62 | 68 | #[test] | |
| @@ -76,8 +82,16 @@ | |||
| 76 | 82 | .map(|c| format!("U+{c:04X}")) | |
| 77 | 83 | .collect::<Vec<_>>() | |
| 78 | 84 | ); | |
| 79 | - | // The done condition names the number: Alloy's eighteen plus the two carets. | |
| 80 | - | assert_eq!(result.required.len(), 20); | |
| 85 | + | // Alloy's eighteen, the two carets, and the whole house set, deduplicated. | |
| 86 | + | // The number moved when the set grew a cell tier; what it is checking is | |
| 87 | + | // that the floor is the union of the two and not one of them. | |
| 88 | + | assert_eq!(result.required.len(), 176); | |
| 89 | + | assert!( | |
| 90 | + | coverage::ALLOY_SURFACE | |
| 91 | + | .iter() | |
| 92 | + | .all(|cp| result.required.contains(cp)), | |
| 93 | + | "the consumer floor is not a subset of what is required" | |
| 94 | + | ); | |
| 81 | 95 | } | |
| 82 | 96 | ||
| 83 | 97 | #[test] | |
| @@ -94,7 +108,15 @@ | |||
| 94 | 108 | "U+{codepoint:04X} moved or was dropped" | |
| 95 | 109 | ); | |
| 96 | 110 | } | |
| 97 | - | assert_eq!(after.len(), before.len() + specs.len()); | |
| 111 | + | // Only what we actually added. Plex draws all 128 box-drawing glyphs and | |
| 112 | + | // all four arrows itself, so most of the set is `Purpose::Coverage` already | |
| 113 | + | // met and the face gains the eleven marks it was missing. | |
| 114 | + | let added = specs | |
| 115 | + | .iter() | |
| 116 | + | .filter(|spec| !before.contains_key(&spec.codepoint)) | |
| 117 | + | .count(); | |
| 118 | + | assert_eq!(after.len(), before.len() + added); | |
| 119 | + | assert_eq!(added, 7, "Plex has everything but the seven house marks"); | |
| 98 | 120 | } | |
| 99 | 121 | ||
| 100 | 122 | #[test] | |
| @@ -239,7 +261,24 @@ | |||
| 239 | 261 | return; | |
| 240 | 262 | }; | |
| 241 | 263 | let (bold, _) = cut("Bold").unwrap(); | |
| 264 | + | let base_had = base::mappings( | |
| 265 | + | &base_faces() | |
| 266 | + | .unwrap() | |
| 267 | + | .into_iter() | |
| 268 | + | .find(|f| f.style == "Regular") | |
| 269 | + | .unwrap() | |
| 270 | + | .bytes, | |
| 271 | + | ) | |
| 272 | + | .unwrap(); | |
| 242 | 273 | for spec in &specs { | |
| 274 | + | // Only the marks this pipeline drew. A codepoint the base already had | |
| 275 | + | // is the base's own drawing at both weights, and Plex deliberately | |
| 276 | + | // draws `█` and `░` identically in Regular and Bold — they are | |
| 277 | + | // cell-fill primitives, which is the observation the set's own weight | |
| 278 | + | // rule was built on. | |
| 279 | + | if base_had.contains_key(&spec.codepoint) { | |
| 280 | + | continue; | |
| 281 | + | } | |
| 243 | 282 | assert_ne!( | |
| 244 | 283 | outline(®ular, spec.codepoint), | |
| 245 | 284 | outline(&bold, spec.codepoint), |
| @@ -10,6 +10,7 @@ | |||
| 10 | 10 | ||
| 11 | 11 | use quasi_type::base::{self, BaseFace}; | |
| 12 | 12 | use quasi_type::compose::{self, Identity}; | |
| 13 | + | use quasi_type::manifest::WeightResponse; | |
| 13 | 14 | use quasi_type::manifest::{GlyphSpec, Manifest}; | |
| 14 | 15 | use quasi_type::pins::{Base, Pins}; | |
| 15 | 16 | use quasi_type::{HOUSE_SET, PINS, assert as coverage, woff2}; | |
| @@ -92,10 +93,16 @@ | |||
| 92 | 93 | "the base's seven named weights" | |
| 93 | 94 | ); | |
| 94 | 95 | ||
| 95 | - | // Every mark has variation data, and it is data the base did not have. | |
| 96 | + | // Every mark that answers weight has variation data, and it is data the | |
| 97 | + | // base did not have. A cell fill is exactly half the cell at every weight, | |
| 98 | + | // so it correctly carries none: `gvar` says "does not vary" with an empty | |
| 99 | + | // entry, and that is the honest record rather than a gap. | |
| 96 | 100 | let mappings = compose::coverage(&bytes).unwrap(); | |
| 97 | 101 | let gvar = font.gvar().unwrap(); | |
| 98 | 102 | for spec in &specs { | |
| 103 | + | if spec.shape.weight_response() == WeightResponse::Holds { | |
| 104 | + | continue; | |
| 105 | + | } | |
| 99 | 106 | let gid = mappings[&spec.codepoint]; | |
| 100 | 107 | let data = gvar | |
| 101 | 108 | .glyph_variation_data(gid) | |
| @@ -117,6 +124,12 @@ | |||
| 117 | 124 | fn a_mark_at_the_heavy_end_is_heavier_than_at_the_light_end() { | |
| 118 | 125 | let Some((bytes, specs)) = cut() else { return }; | |
| 119 | 126 | for spec in &specs { | |
| 127 | + | // Except the cell fills, which are the one thing in the set that | |
| 128 | + | // deliberately does not answer weight: `▀` is exactly half the cell at | |
| 129 | + | // wght 200 and at 800, and a half that grew would stop tiling. | |
| 130 | + | if spec.shape.weight_response() == WeightResponse::Holds { | |
| 131 | + | continue; | |
| 132 | + | } | |
| 120 | 133 | let light = ink(&bytes, spec.codepoint, 200.0); | |
| 121 | 134 | let heavy = ink(&bytes, spec.codepoint, 800.0); | |
| 122 | 135 | assert!( | |
| @@ -144,9 +157,26 @@ | |||
| 144 | 157 | ||
| 145 | 158 | let Some((bytes, specs)) = cut() else { return }; | |
| 146 | 159 | for spec in &specs { | |
| 160 | + | // Cell furniture is measured by the test below instead. A bar's | |
| 161 | + | // bounding box *is* the bar, so it inks all of it at every weight, and | |
| 162 | + | // a fill fraction says nothing about whether it thickened. | |
| 163 | + | if spec.shape.is_cell_furniture() { | |
| 164 | + | continue; | |
| 165 | + | } | |
| 147 | 166 | let light = fill(&bytes, spec.codepoint, 200.0); | |
| 148 | 167 | let heavy = fill(&bytes, spec.codepoint, 800.0); | |
| 149 | 168 | match spec.shape.weight_response() { | |
| 169 | + | // Cell fills are exact fractions of the cell at every weight, so | |
| 170 | + | // they neither grow nor thicken. Nothing to assert but that. | |
| 171 | + | WeightResponse::Holds => { | |
| 172 | + | assert!( | |
| 173 | + | (width(&bytes, spec.codepoint, 800.0) - width(&bytes, spec.codepoint, 200.0)) | |
| 174 | + | .abs() | |
| 175 | + | < 1.0, | |
| 176 | + | "{} is a cell fill and must not move with the axis", | |
| 177 | + | spec.name | |
| 178 | + | ); | |
| 179 | + | } | |
| 150 | 180 | WeightResponse::Grows => { | |
| 151 | 181 | assert!( | |
| 152 | 182 | (heavy - light).abs() < 0.01, | |
| @@ -173,6 +203,50 @@ | |||
| 173 | 203 | } | |
| 174 | 204 | } | |
| 175 | 205 | ||
| 206 | + | /// The done condition of the variable work, in the words it was written in: a | |
| 207 | + | /// bold table border draws at the bold stroke weight. | |
| 208 | + | /// | |
| 209 | + | /// Measured on the bar itself rather than on a fill fraction. A box-drawing | |
| 210 | + | /// stem is a rectangle, so its bounding box is the bar and it inks all of it at | |
| 211 | + | /// every weight; what has to move is the bar's own thickness, and it has to | |
| 212 | + | /// move by what the base moved — `-` goes 55 units to 139 across this axis. | |
| 213 | + | #[test] | |
| 214 | + | fn a_bold_border_draws_at_the_bold_stroke_weight() { | |
| 215 | + | let Some((bytes, _)) = cut() else { return }; | |
| 216 | + | // A horizontal rule's thickness is its height; a vertical's is its width. | |
| 217 | + | let thin = height(&bytes, 0x2500, 200.0); | |
| 218 | + | let thick = height(&bytes, 0x2500, 800.0); | |
| 219 | + | let ratio = thick / thin; | |
| 220 | + | let base_ratio = 139.0 / 55.0; | |
| 221 | + | assert!( | |
| 222 | + | (ratio - base_ratio).abs() < 0.15, | |
| 223 | + | "the light horizontal goes {thin:.0} to {thick:.0} ({ratio:.2}x) while the base's \ | |
| 224 | + | own bar goes 55 to 139 ({base_ratio:.2}x)" | |
| 225 | + | ); | |
| 226 | + | let thin = width(&bytes, 0x2502, 200.0); | |
| 227 | + | let thick = width(&bytes, 0x2502, 800.0); | |
| 228 | + | assert!( | |
| 229 | + | thick > thin * 2.0, | |
| 230 | + | "the light vertical goes {thin:.0} to {thick:.0}, and the base's stem goes 54 to 158" | |
| 231 | + | ); | |
| 232 | + | // And heavy stays twice light at both ends of the axis, or the two weights | |
| 233 | + | // of border stop being distinguishable at one end. | |
| 234 | + | for wght in [200.0, 800.0] { | |
| 235 | + | let light = height(&bytes, 0x2500, wght); | |
| 236 | + | let heavy = height(&bytes, 0x2501, wght); | |
| 237 | + | assert!( | |
| 238 | + | (heavy / light - 2.0).abs() < 0.1, | |
| 239 | + | "at wght {wght} heavy is {:.2}x light", | |
| 240 | + | heavy / light | |
| 241 | + | ); | |
| 242 | + | } | |
| 243 | + | // A cell fill holds: `█` is the whole cell at every weight. | |
| 244 | + | assert!( | |
| 245 | + | (height(&bytes, 0x2588, 800.0) - height(&bytes, 0x2588, 200.0)).abs() < 1.0, | |
| 246 | + | "the full block moved with the axis" | |
| 247 | + | ); | |
| 248 | + | } | |
| 249 | + | ||
| 176 | 250 | /// The ExtraLight trap, as a gate rather than as a warning in a comment. | |
| 177 | 251 | /// | |
| 178 | 252 | /// Keeping the axis means keeping the base's default instance, and this base's | |
| @@ -225,30 +299,36 @@ | |||
| 225 | 299 | assert!(names(10).contains("Atkinson Hyperlegible Mono 2.001")); | |
| 226 | 300 | } | |
| 227 | 301 | ||
| 228 | - | /// What the box-drawing task is for, stated as a measurement rather than as a | |
| 229 | - | /// note. Atkinson ships no box drawing and no block elements, so a face cut from | |
| 230 | - | /// it today fails the floor every consumer is entitled to assume — which is why | |
| 231 | - | /// `aab673c1` waits on `872fd945` rather than pointing the slot at this base. | |
| 302 | + | /// The done condition of `872fd945`, stated as a measurement rather than as a | |
| 303 | + | /// note. Atkinson ships no box drawing, no block elements and none of the four | |
| 304 | + | /// arrows; a face cut from it now covers the whole floor a consumer is entitled | |
| 305 | + | /// to assume, which is what unblocks pointing the mono slot at this base. | |
| 306 | + | /// | |
| 307 | + | /// This test used to assert the opposite — that the cut was *not* yet fit — and | |
| 308 | + | /// carried a message saying it would go stale when the gap closed. It has. | |
| 232 | 309 | #[test] | |
| 233 | - | fn the_cut_face_is_not_yet_fit_for_the_mono_slot() { | |
| 310 | + | fn the_cut_face_is_fit_for_the_mono_slot() { | |
| 234 | 311 | let Some((bytes, specs)) = cut() else { return }; | |
| 235 | 312 | let house: Vec<u32> = specs.iter().map(|g| g.codepoint).collect(); | |
| 236 | 313 | let result = coverage::check(&compose::coverage(&bytes).unwrap(), &house); | |
| 237 | 314 | assert!( | |
| 238 | - | !result.ok(), | |
| 239 | - | "the base grew box drawing; this test and its blocker are stale" | |
| 315 | + | result.ok(), | |
| 316 | + | "missing {:?}", | |
| 317 | + | result | |
| 318 | + | .missing | |
| 319 | + | .iter() | |
| 320 | + | .map(|c| format!("U+{c:04X}")) | |
| 321 | + | .collect::<Vec<_>>() | |
| 240 | 322 | ); | |
| 323 | + | // The four the base has none of, one from each tier that had to be built: | |
| 324 | + | // a box-drawing stem, a fill, an eighth, and a shade. | |
| 241 | 325 | for cell_furniture in [0x2502, 0x2588, 0x258F, 0x2591] { | |
| 242 | 326 | assert!( | |
| 243 | - | result.missing.contains(&cell_furniture), | |
| 244 | - | "U+{cell_furniture:04X} is covered, so the gap has moved" | |
| 327 | + | house.contains(&cell_furniture), | |
| 328 | + | "U+{cell_furniture:04X} is not in the set, so the base must have it \ | |
| 329 | + | and Atkinson does not" | |
| 245 | 330 | ); | |
| 246 | 331 | } | |
| 247 | - | // The seven marks are drawn, so the gap is the base's cell furniture and | |
| 248 | - | // nothing the pipeline was asked to draw. | |
| 249 | - | for mark in &house { | |
| 250 | - | assert!(!result.missing.contains(mark)); | |
| 251 | - | } | |
| 252 | 332 | } | |
| 253 | 333 | ||
| 254 | 334 | #[test] | |
| @@ -307,6 +387,10 @@ | |||
| 307 | 387 | outline(bytes, codepoint, wght).0 | |
| 308 | 388 | } | |
| 309 | 389 | ||
| 390 | + | fn height(bytes: &[u8], codepoint: u32, wght: f32) -> f32 { | |
| 391 | + | outline(bytes, codepoint, wght).1 | |
| 392 | + | } | |
| 393 | + | ||
| 310 | 394 | fn ink(bytes: &[u8], codepoint: u32, wght: f32) -> f32 { | |
| 311 | 395 | outline(bytes, codepoint, wght).2 | |
| 312 | 396 | } |
| @@ -1,0 +1,209 @@ | |||
| 1 | + | #!/usr/bin/env python3 | |
| 2 | + | """Derive src/cells/table.rs from Unicode's own character names. | |
| 3 | + | ||
| 4 | + | The Box Drawing and Block Elements blocks are named systematically, so the 160 | |
| 5 | + | recipes are already written down: BOX DRAWINGS DOWN LIGHT AND RIGHT HEAVY says | |
| 6 | + | which arms a glyph has and how heavy each one is, and LEFT THREE EIGHTHS BLOCK | |
| 7 | + | says which fraction of the cell is filled. Typing that out by hand would be 160 | |
| 8 | + | chances to transpose two arms in a way nothing catches until a border looks | |
| 9 | + | wrong in one corner. | |
| 10 | + | ||
| 11 | + | So the table is derived rather than authored, from the `unicodedata` module in | |
| 12 | + | the Python standard library, and the result is committed. Re-run it when the | |
| 13 | + | shape vocabulary changes: | |
| 14 | + | ||
| 15 | + | python3 scripts/derive-cell-table.py > src/cells/table.rs | |
| 16 | + | ||
| 17 | + | Every name must parse. The script fails rather than emitting a partial table, | |
| 18 | + | because a name it did not understand is a glyph that would silently come out | |
| 19 | + | blank. | |
| 20 | + | """ | |
| 21 | + | ||
| 22 | + | import sys | |
| 23 | + | import unicodedata as ud | |
| 24 | + | ||
| 25 | + | WEIGHT = {"LIGHT": "L", "HEAVY": "H", "DOUBLE": "D", "SINGLE": "L"} | |
| 26 | + | DIRS = { | |
| 27 | + | "UP": ["up"], | |
| 28 | + | "DOWN": ["down"], | |
| 29 | + | "LEFT": ["left"], | |
| 30 | + | "RIGHT": ["right"], | |
| 31 | + | "HORIZONTAL": ["left", "right"], | |
| 32 | + | "VERTICAL": ["up", "down"], | |
| 33 | + | } | |
| 34 | + | ARMS = ["left", "right", "up", "down"] | |
| 35 | + | FRACTION = { | |
| 36 | + | "ONE": 1, "TWO": 2, "THREE": 3, "FOUR": 4, | |
| 37 | + | "FIVE": 5, "SIX": 6, "SEVEN": 7, "EIGHT": 8, | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | ||
| 41 | + | def die(msg): | |
| 42 | + | print(f"derive-cell-table: {msg}", file=sys.stderr) | |
| 43 | + | sys.exit(1) | |
| 44 | + | ||
| 45 | + | ||
| 46 | + | def parse_box(cp): | |
| 47 | + | """(kind, dash, arms) for a Box Drawing codepoint.""" | |
| 48 | + | name = ud.name(chr(cp)) | |
| 49 | + | body = name.replace("BOX DRAWINGS ", "") | |
| 50 | + | ||
| 51 | + | if "DIAGONAL" in body: | |
| 52 | + | if "UPPER RIGHT TO LOWER LEFT" in body: | |
| 53 | + | return ("diagonal", "Rising", {}) | |
| 54 | + | if "UPPER LEFT TO LOWER RIGHT" in body: | |
| 55 | + | return ("diagonal", "Falling", {}) | |
| 56 | + | if "CROSS" in body: | |
| 57 | + | return ("diagonal", "Cross", {}) | |
| 58 | + | die(f"U+{cp:04X} {name}: unrecognised diagonal") | |
| 59 | + | ||
| 60 | + | kind = "stems" | |
| 61 | + | if "ARC " in body: | |
| 62 | + | kind = "arc" | |
| 63 | + | body = body.replace("ARC ", "") | |
| 64 | + | ||
| 65 | + | dash = "None" | |
| 66 | + | for spelling, variant in [ | |
| 67 | + | ("QUADRUPLE DASH", "Quadruple"), | |
| 68 | + | ("TRIPLE DASH", "Triple"), | |
| 69 | + | ("DOUBLE DASH", "Double"), | |
| 70 | + | ]: | |
| 71 | + | if spelling in body: | |
| 72 | + | dash = variant | |
| 73 | + | body = body.replace(spelling + " ", "") | |
| 74 | + | ||
| 75 | + | arms = {} | |
| 76 | + | for clause in body.split(" AND "): | |
| 77 | + | tokens = clause.split() | |
| 78 | + | weights = [t for t in tokens if t in WEIGHT] | |
| 79 | + | directions = [t for t in tokens if t in DIRS] | |
| 80 | + | leftover = [t for t in tokens if t not in WEIGHT and t not in DIRS] | |
| 81 | + | if leftover or not directions: | |
| 82 | + | die(f"U+{cp:04X} {name}: cannot read clause {clause!r}") | |
| 83 | + | weight = WEIGHT[weights[0]] if weights else None | |
| 84 | + | for direction in directions: | |
| 85 | + | for arm in DIRS[direction]: | |
| 86 | + | arms[arm] = weight | |
| 87 | + | ||
| 88 | + | # A leading weight applies to every clause that named none of its own: | |
| 89 | + | # "LIGHT DOWN AND RIGHT" weighs both arms, "DOWN LIGHT AND RIGHT HEAVY" | |
| 90 | + | # weighs them separately. | |
| 91 | + | stated = [w for w in arms.values() if w] | |
| 92 | + | if not stated: | |
| 93 | + | die(f"U+{cp:04X} {name}: no weight anywhere") | |
| 94 | + | for arm, weight in arms.items(): | |
| 95 | + | if weight is None: | |
| 96 | + | arms[arm] = stated[0] | |
| 97 | + | return (kind, dash, arms) | |
| 98 | + | ||
| 99 | + | ||
| 100 | + | def parse_block(cp): | |
| 101 | + | """(kind, payload) for a Block Elements codepoint.""" | |
| 102 | + | name = ud.name(chr(cp)) | |
| 103 | + | if name == "FULL BLOCK": | |
| 104 | + | return ("fill", [(0, 0, 8, 8)]) | |
| 105 | + | if name.endswith("SHADE"): | |
| 106 | + | return ("shade", name.split()[0].capitalize()) | |
| 107 | + | if name.startswith("QUADRANT"): | |
| 108 | + | quads = { | |
| 109 | + | "UPPER LEFT": (0, 4, 4, 8), | |
| 110 | + | "UPPER RIGHT": (4, 4, 8, 8), | |
| 111 | + | "LOWER LEFT": (0, 0, 4, 4), | |
| 112 | + | "LOWER RIGHT": (4, 0, 8, 4), | |
| 113 | + | } | |
| 114 | + | rects = [] | |
| 115 | + | for part in name[len("QUADRANT "):].split(" AND "): | |
| 116 | + | if part not in quads: | |
| 117 | + | die(f"U+{cp:04X} {name}: unknown quadrant {part!r}") | |
| 118 | + | rects.append(quads[part]) | |
| 119 | + | return ("fill", rects) | |
| 120 | + | ||
| 121 | + | tokens = name.split() | |
| 122 | + | side, count, unit = tokens[0], tokens[1], tokens[2] | |
| 123 | + | if unit.startswith("QUARTER"): | |
| 124 | + | eighths = FRACTION[count] * 2 | |
| 125 | + | elif unit.startswith("EIGHTH"): | |
| 126 | + | eighths = FRACTION[count] | |
| 127 | + | elif count == "HALF": | |
| 128 | + | eighths = 4 | |
| 129 | + | unit = "HALF" | |
| 130 | + | else: | |
| 131 | + | die(f"U+{cp:04X} {name}: unknown fraction {count} {unit}") | |
| 132 | + | if count == "HALF": | |
| 133 | + | eighths = 4 | |
| 134 | + | ||
| 135 | + | if side == "UPPER": | |
| 136 | + | return ("fill", [(0, 8 - eighths, 8, 8)]) | |
| 137 | + | if side == "LOWER": | |
| 138 | + | return ("fill", [(0, 0, 8, eighths)]) | |
| 139 | + | if side == "LEFT": | |
| 140 | + | return ("fill", [(0, 0, eighths, 8)]) | |
| 141 | + | if side == "RIGHT": | |
| 142 | + | return ("fill", [(8 - eighths, 0, 8, 8)]) | |
| 143 | + | die(f"U+{cp:04X} {name}: unknown side {side!r}") | |
| 144 | + | ||
| 145 | + | ||
| 146 | + | def main(): | |
| 147 | + | stems, arcs, diagonals, fills, shades = [], [], [], [], [] | |
| 148 | + | ||
| 149 | + | for cp in range(0x2500, 0x2580): | |
| 150 | + | kind, payload, arms = parse_box(cp) | |
| 151 | + | if kind == "diagonal": | |
| 152 | + | diagonals.append((cp, payload)) | |
| 153 | + | continue | |
| 154 | + | row = (cp, [arms.get(a, "N") for a in ARMS], payload) | |
| 155 | + | (arcs if kind == "arc" else stems).append(row) | |
| 156 | + | ||
| 157 | + | for cp in range(0x2580, 0x25A0): | |
| 158 | + | kind, payload = parse_block(cp) | |
| 159 | + | (shades if kind == "shade" else fills).append((cp, payload)) | |
| 160 | + | ||
| 161 | + | counts = (len(stems), len(arcs), len(diagonals), len(fills), len(shades)) | |
| 162 | + | if sum(counts) != 160: | |
| 163 | + | die(f"expected 160 codepoints, derived {sum(counts)}") | |
| 164 | + | ||
| 165 | + | out = [] | |
| 166 | + | w = out.append | |
| 167 | + | w("// Generated by scripts/derive-cell-table.py from Unicode character names.") | |
| 168 | + | w("// Do not edit by hand: re-run the script instead. See its header for why") | |
| 169 | + | w("// the table is derived rather than authored.") | |
| 170 | + | w("//") | |
| 171 | + | w(f"// {counts[0]} stems, {counts[1]} arcs, {counts[2]} diagonals," | |
| 172 | + | f" {counts[3]} fills, {counts[4]} shades.") | |
| 173 | + | w("") | |
| 174 | + | w("use super::Arm::{D, H, L, N};") | |
| 175 | + | w("use super::{Dash, Diagonal, Shade};") | |
| 176 | + | w("") | |
| 177 | + | w("/// Arms in the order left, right, up, down.") | |
| 178 | + | w(f"pub(super) const STEMS: [(u32, [super::Arm; 4], Dash); {counts[0]}] = [") | |
| 179 | + | for cp, arms, dash in stems: | |
| 180 | + | w(f" (0x{cp:04X}, [{', '.join(arms)}], Dash::{dash}),") | |
| 181 | + | w("];") | |
| 182 | + | w("") | |
| 183 | + | w("/// The rounded corners. Same arms as a stem, drawn as a quarter turn.") | |
| 184 | + | w(f"pub(super) const ARCS: [(u32, [super::Arm; 4]); {counts[1]}] = [") | |
| 185 | + | for cp, arms, _ in arcs: | |
| 186 | + | w(f" (0x{cp:04X}, [{', '.join(arms)}]),") | |
| 187 | + | w("];") | |
| 188 | + | w("") | |
| 189 | + | w(f"pub(super) const DIAGONALS: [(u32, Diagonal); {counts[2]}] = [") | |
| 190 | + | for cp, kind in diagonals: | |
| 191 | + | w(f" (0x{cp:04X}, Diagonal::{kind}),") | |
| 192 | + | w("];") | |
| 193 | + | w("") | |
| 194 | + | w("/// Filled rectangles in eighths of the cell: x0, y0, x1, y1, with y up.") | |
| 195 | + | w(f"pub(super) const FILLS: [(u32, &[(u8, u8, u8, u8)]); {counts[3]}] = [") | |
| 196 | + | for cp, rects in fills: | |
| 197 | + | body = ", ".join(f"({a}, {b}, {c}, {d})" for a, b, c, d in rects) | |
| 198 | + | w(f" (0x{cp:04X}, &[{body}]),") | |
| 199 | + | w("];") | |
| 200 | + | w("") | |
| 201 | + | w(f"pub(super) const SHADES: [(u32, Shade); {counts[4]}] = [") | |
| 202 | + | for cp, level in shades: | |
| 203 | + | w(f" (0x{cp:04X}, Shade::{level}),") | |
| 204 | + | w("];") | |
| 205 | + | print("\n".join(out)) | |
| 206 | + | ||
| 207 | + | ||
| 208 | + | if __name__ == "__main__": | |
| 209 | + | main() |
| @@ -1,0 +1,940 @@ | |||
| 1 | + | //! Cell primitives: box drawing and block elements. | |
| 2 | + | //! | |
| 3 | + | //! The second tier of the house set, and a different kind of thing from the | |
| 4 | + | //! first. The marks in `glyphs/manifest.toml` are *symbols*: they sit in the | |
| 5 | + | //! band the base fits its own symbols into, they are sized against it, and each | |
| 6 | + | //! one is authored. These are *cell furniture*: they fill the box a terminal | |
| 7 | + | //! gives a character, they are sized against that box and never against the | |
| 8 | + | //! band, and there are 160 of them. | |
| 9 | + | //! | |
| 10 | + | //! ## Why they are generated rather than drawn | |
| 11 | + | //! | |
| 12 | + | //! Because they must tile. A vertical bar has to meet the bar in the cell above | |
| 13 | + | //! it exactly, and a horizontal one has to meet the cell beside it, or a table | |
| 14 | + | //! border shows a seam at every join. That is arithmetic on the cell, and | |
| 15 | + | //! arithmetic is what a generator is good at and what a hand is bad at. | |
| 16 | + | //! Generating is the correct method here rather than the cheap one. | |
| 17 | + | //! | |
| 18 | + | //! It is also the only method that answers the axis. The recipes are evaluated | |
| 19 | + | //! at each `gvar` master, so a bold border draws at the bold stroke weight | |
| 20 | + | //! without a second drawing existing anywhere. | |
| 21 | + | //! | |
| 22 | + | //! ## The cell | |
| 23 | + | //! | |
| 24 | + | //! `hhea` ascender to descender, and the full advance. Not the band, which is | |
| 25 | + | //! near the x-height: a rule on the band's centre would run through the middle | |
| 26 | + | //! of the text beside it rather than between the lines. | |
| 27 | + | //! | |
| 28 | + | //! Worth knowing, because it differs from what the old base did. Plex Mono | |
| 29 | + | //! draws its box glyphs over y -350..950 while its own `hhea` says -275..1025 — | |
| 30 | + | //! the same 1300-unit height, positioned 75 units lower. So Plex's borders do | |
| 31 | + | //! not line up with a cell laid out from its own metrics, which is why | |
| 32 | + | //! terminals so often stretch box-drawing glyphs to the cell. Ours is built on | |
| 33 | + | //! the metrics a terminal actually lays out with, so it needs no stretching. | |
| 34 | + | //! | |
| 35 | + | //! ## The stroke vocabulary | |
| 36 | + | //! | |
| 37 | + | //! One light bar is the base's own: `-`'s height for horizontals, `|`'s width | |
| 38 | + | //! for verticals, so a border sits at the weight of the text it surrounds. | |
| 39 | + | //! Heavy is twice that. Double is two light rails with a light gap, which is a | |
| 40 | + | //! total of three light bars and is what Plex draws (its double horizontal | |
| 41 | + | //! spans 204 units against the light bar's 68). | |
| 42 | + | ||
| 43 | + | mod table; | |
| 44 | + | ||
| 45 | + | use crate::base::BaseParams; | |
| 46 | + | use crate::draw::Drawing; | |
| 47 | + | use crate::manifest::{GlyphSpec, Purpose, Shape, WeightResponse, format_codepoint}; | |
| 48 | + | ||
| 49 | + | /// How heavy one arm of a box-drawing glyph is. | |
| 50 | + | #[derive(Debug, Clone, Copy, PartialEq, Eq)] | |
| 51 | + | pub enum Arm { | |
| 52 | + | /// No arm in this direction. Spelled `N` in the generated table. | |
| 53 | + | N, | |
| 54 | + | L, | |
| 55 | + | H, | |
| 56 | + | D, | |
| 57 | + | } | |
| 58 | + | ||
| 59 | + | impl Arm { | |
| 60 | + | fn present(self) -> bool { | |
| 61 | + | self != Self::N | |
| 62 | + | } | |
| 63 | + | ||
| 64 | + | /// Half the arm's total extent across itself, in units. | |
| 65 | + | /// | |
| 66 | + | /// What a crossing arm has to reach past to close a joint, and what a rail | |
| 67 | + | /// stops at. Double counts the whole three-bar span, not one rail. | |
| 68 | + | fn half_extent(self, light: f64) -> f64 { | |
| 69 | + | match self { | |
| 70 | + | Self::N => 0.0, | |
| 71 | + | Self::L => light / 2.0, | |
| 72 | + | Self::H => light, | |
| 73 | + | Self::D => light * 1.5, | |
| 74 | + | } | |
| 75 | + | } | |
| 76 | + | } | |
| 77 | + | ||
| 78 | + | #[derive(Debug, Clone, Copy, PartialEq, Eq)] | |
| 79 | + | pub enum Dash { | |
| 80 | + | None, | |
| 81 | + | Double, | |
| 82 | + | Triple, | |
| 83 | + | Quadruple, | |
| 84 | + | } | |
| 85 | + | ||
| 86 | + | impl Dash { | |
| 87 | + | /// How many dashes the bar is broken into. | |
| 88 | + | const fn count(self) -> usize { | |
| 89 | + | match self { | |
| 90 | + | Self::None => 1, | |
| 91 | + | Self::Double => 2, | |
| 92 | + | Self::Triple => 3, | |
| 93 | + | Self::Quadruple => 4, | |
| 94 | + | } | |
| 95 | + | } | |
| 96 | + | } | |
| 97 | + | ||
| 98 | + | #[derive(Debug, Clone, Copy, PartialEq, Eq)] | |
| 99 | + | pub enum Diagonal { | |
| 100 | + | /// `╱`, lower left to upper right. | |
| 101 | + | Rising, | |
| 102 | + | /// `╲`, upper left to lower right. | |
| 103 | + | Falling, | |
| 104 | + | /// `╳`, both. | |
| 105 | + | Cross, | |
| 106 | + | } | |
| 107 | + | ||
| 108 | + | #[derive(Debug, Clone, Copy, PartialEq, Eq)] | |
| 109 | + | pub enum Shade { | |
| 110 | + | Light, | |
| 111 | + | Medium, | |
| 112 | + | Dark, | |
| 113 | + | } | |
| 114 | + | ||
| 115 | + | impl Shade { | |
| 116 | + | /// Filled squares out of the [`SHADE_GRID`] squared, so the three read as a | |
| 117 | + | /// progression rather than three unrelated textures. | |
| 118 | + | const fn filled(self) -> u32 { | |
| 119 | + | match self { | |
| 120 | + | Self::Light => 1, | |
| 121 | + | Self::Medium => 2, | |
| 122 | + | Self::Dark => 3, | |
| 123 | + | } | |
| 124 | + | } | |
| 125 | + | } | |
| 126 | + | ||
| 127 | + | /// The shade pattern's grid, per axis. | |
| 128 | + | /// | |
| 129 | + | /// Eight, so a shade's texture lines up with the eighths the block elements are | |
| 130 | + | /// already cut on: `░` beside `▄` should not look like a different unit. | |
| 131 | + | const SHADE_GRID: u32 = 8; | |
| 132 | + | ||
| 133 | + | /// One generated cell primitive. | |
| 134 | + | #[derive(Debug, Clone, Copy)] | |
| 135 | + | pub enum Cell { | |
| 136 | + | /// Arms from the cell's edges to its centre, in the order left, right, up, | |
| 137 | + | /// down. | |
| 138 | + | Stems { | |
| 139 | + | arms: [Arm; 4], | |
| 140 | + | dash: Dash, | |
| 141 | + | }, | |
| 142 | + | /// A rounded corner: the same two arms, turned through a quarter circle. | |
| 143 | + | Arc { | |
| 144 | + | arms: [Arm; 4], | |
| 145 | + | }, | |
| 146 | + | Diagonal(Diagonal), | |
| 147 | + | /// Filled rectangles, in eighths of the cell, `y` up. | |
| 148 | + | Fill(&'static [(u8, u8, u8, u8)]), | |
| 149 | + | Shade(Shade), | |
| 150 | + | } | |
| 151 | + | ||
| 152 | + | impl Cell { | |
| 153 | + | pub fn kind(self) -> &'static str { | |
| 154 | + | match self { | |
| 155 | + | Self::Stems { .. } => "stems", | |
| 156 | + | Self::Arc { .. } => "arc", | |
| 157 | + | Self::Diagonal(_) => "diagonal", | |
| 158 | + | Self::Fill(_) => "fill", | |
| 159 | + | Self::Shade(_) => "shade", | |
| 160 | + | } | |
| 161 | + | } | |
| 162 | + | ||
| 163 | + | /// Cell furniture divides on this exactly where the manifest's marks do. | |
| 164 | + | /// | |
| 165 | + | /// A stem is a stroke and thickens with the base, the way the base thickens | |
| 166 | + | /// `+`. A fill has no stroke and no extent to grow into — it is already the | |
| 167 | + | /// whole cell or an exact fraction of it — so it holds, which is what the | |
| 168 | + | /// base does with `█` and `░` and the reason the manifest's weight term | |
| 169 | + | /// exists at all. | |
| 170 | + | pub fn weight_response(self) -> WeightResponse { | |
| 171 | + | match self { | |
| 172 | + | Self::Stems { .. } | Self::Arc { .. } | Self::Diagonal(_) => WeightResponse::Thickens, | |
| 173 | + | Self::Fill(_) | Self::Shade(_) => WeightResponse::Holds, | |
| 174 | + | } | |
| 175 | + | } | |
| 176 | + | } | |
| 177 | + | ||
| 178 | + | /// The whole generated tier, in codepoint order. | |
| 179 | + | pub fn cells() -> Vec<(u32, Cell)> { | |
| 180 | + | let mut out: Vec<(u32, Cell)> = Vec::with_capacity(160); | |
| 181 | + | for &(cp, arms, dash) in &table::STEMS { | |
| 182 | + | out.push((cp, Cell::Stems { arms, dash })); | |
| 183 | + | } | |
| 184 | + | for &(cp, arms) in &table::ARCS { | |
| 185 | + | out.push((cp, Cell::Arc { arms })); | |
| 186 | + | } | |
| 187 | + | for &(cp, kind) in &table::DIAGONALS { | |
| 188 | + | out.push((cp, Cell::Diagonal(kind))); | |
| 189 | + | } | |
| 190 | + | for &(cp, rects) in &table::FILLS { | |
| 191 | + | out.push((cp, Cell::Fill(rects))); | |
| 192 | + | } | |
| 193 | + | for &(cp, level) in &table::SHADES { | |
| 194 | + | out.push((cp, Cell::Shade(level))); | |
| 195 | + | } | |
| 196 | + | out.sort_by_key(|&(cp, _)| cp); | |
| 197 | + | out | |
| 198 | + | } | |
| 199 | + | ||
| 200 | + | /// The generated tier as manifest entries, so nothing downstream has to know | |
| 201 | + | /// these were not authored one by one. | |
| 202 | + | pub fn specs(block: Block) -> Vec<GlyphSpec> { | |
| 203 | + | cells() | |
| 204 | + | .into_iter() | |
| 205 | + | .filter(|&(cp, _)| block.contains(cp)) | |
| 206 | + | .map(|(codepoint, cell)| GlyphSpec { | |
| 207 | + | codepoint, | |
| 208 | + | name: format!("uni{codepoint:04X}"), | |
| 209 | + | role: format!("{} ({})", block.role(), cell.kind()), | |
| 210 | + | source: Some(format!( | |
| 211 | + | "generated from the Unicode name of {}", | |
| 212 | + | format_codepoint(codepoint) | |
| 213 | + | )), | |
| 214 | + | // Never house: a base that ships box drawing ships a designed set | |
| 215 | + | // of it, and ours exists for the bases that do not. | |
| 216 | + | purpose: Purpose::Coverage, | |
| 217 | + | shape: Shape::Cell(cell), | |
| 218 | + | }) | |
| 219 | + | .collect() | |
| 220 | + | } | |
| 221 | + | ||
| 222 | + | /// A block a schema may ask for by name. | |
| 223 | + | #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize)] | |
| 224 | + | #[serde(rename_all = "kebab-case")] | |
| 225 | + | pub enum Block { | |
| 226 | + | BoxDrawing, | |
| 227 | + | BlockElements, | |
| 228 | + | } | |
| 229 | + | ||
| 230 | + | impl Block { | |
| 231 | + | const fn range(self) -> (u32, u32) { | |
| 232 | + | match self { | |
| 233 | + | Self::BoxDrawing => (0x2500, 0x257F), | |
| 234 | + | Self::BlockElements => (0x2580, 0x259F), | |
| 235 | + | } | |
| 236 | + | } | |
| 237 | + | ||
| 238 | + | fn contains(self, codepoint: u32) -> bool { | |
| 239 | + | let (lo, hi) = self.range(); | |
| 240 | + | (lo..=hi).contains(&codepoint) | |
| 241 | + | } | |
| 242 | + | ||
| 243 | + | const fn role(self) -> &'static str { | |
| 244 | + | match self { | |
| 245 | + | Self::BoxDrawing => "box drawing", | |
| 246 | + | Self::BlockElements => "block element", | |
| 247 | + | } | |
| 248 | + | } | |
| 249 | + | } | |
| 250 | + | ||
| 251 | + | // --------------------------------------------------------------------------- | |
| 252 | + | // Drawing | |
| 253 | + | // --------------------------------------------------------------------------- | |
| 254 | + | ||
| 255 | + | /// The cell a primitive is drawn into: the advance, and ascender to descender. | |
| 256 | + | struct CellBox { | |
| 257 | + | x0: f64, | |
| 258 | + | x1: f64, | |
| 259 | + | y0: f64, | |
| 260 | + | y1: f64, | |
| 261 | + | cx: f64, | |
| 262 | + | cy: f64, | |
| 263 | + | /// A light horizontal bar's thickness: the base's own `-`. | |
| 264 | + | light_h: f64, | |
| 265 | + | /// A light vertical bar's thickness: the base's own `|`. | |
| 266 | + | light_v: f64, | |
| 267 | + | } | |
| 268 | + | ||
| 269 | + | impl CellBox { | |
| 270 | + | fn of(params: &BaseParams) -> Self { | |
| 271 | + | Self { | |
| 272 | + | x0: 0.0, | |
| 273 | + | x1: f64::from(params.advance), | |
| 274 | + | y0: f64::from(params.descent), | |
| 275 | + | y1: f64::from(params.ascent), | |
| 276 | + | cx: params.center_x(), | |
| 277 | + | cy: params.cell_center_y(), | |
| 278 | + | light_h: f64::from(params.stroke), | |
| 279 | + | light_v: f64::from(params.stem), | |
| 280 | + | } | |
| 281 | + | } | |
| 282 | + | ||
| 283 | + | fn rect(x0: f64, y0: f64, x1: f64, y1: f64) -> Vec<(f64, f64)> { | |
| 284 | + | vec![(x0, y0), (x1, y0), (x1, y1), (x0, y1)] | |
| 285 | + | } | |
| 286 | + | } | |
| 287 | + | ||
| 288 | + | pub fn draw(cell: Cell, params: &BaseParams) -> Drawing { | |
| 289 | + | let c = CellBox::of(params); | |
| 290 | + | let contours = match cell { | |
| 291 | + | Cell::Stems { arms, dash } => stems(&c, arms, dash), | |
| 292 | + | Cell::Arc { arms } => arc(&c, arms), | |
| 293 | + | Cell::Diagonal(kind) => diagonal(&c, kind), | |
| 294 | + | Cell::Fill(rects) => fill(&c, rects), | |
| 295 | + | Cell::Shade(level) => shade(&c, level), | |
| 296 | + | }; | |
| 297 | + | Drawing { contours } | |
| 298 | + | } | |
| 299 | + | ||
| 300 | + | /// Arms in the table's order. | |
| 301 | + | const LEFT: usize = 0; | |
| 302 | + | const RIGHT: usize = 1; | |
| 303 | + | const UP: usize = 2; | |
| 304 | + | const DOWN: usize = 3; | |
| 305 | + | ||
| 306 | + | fn stems(c: &CellBox, arms: [Arm; 4], dash: Dash) -> Vec<Vec<(f64, f64)>> { | |
| 307 | + | let mut out = Vec::new(); | |
| 308 | + | ||
| 309 | + | // A dashed glyph is one bar across the whole cell rather than two arms | |
| 310 | + | // meeting in the middle. Unicode has no dashed junction — every dashed | |
| 311 | + | // codepoint in the block is a plain horizontal or vertical — and drawing it | |
| 312 | + | // as two arms would dash each half separately, so `┄` would come out with | |
| 313 | + | // six marks and a seam in the middle instead of the three it names. | |
| 314 | + | if dash != Dash::None { | |
| 315 | + | let horizontal = arms[LEFT].present(); | |
| 316 | + | let arm = if horizontal { arms[LEFT] } else { arms[UP] }; | |
| 317 | + | let light = if horizontal { c.light_h } else { c.light_v }; | |
| 318 | + | let thickness = if arm == Arm::H { light * 2.0 } else { light }; | |
| 319 | + | let (from, to, across) = if horizontal { | |
| 320 | + | (c.x0, c.x1, c.cy) | |
| 321 | + | } else { | |
| 322 | + | (c.y0, c.y1, c.cx) | |
| 323 | + | }; | |
| 324 | + | return bar(c, horizontal, from, to, across, thickness, dash); | |
| 325 | + | } | |
| 326 | + | ||
| 327 | + | // What a horizontal arm has to reach past to close a joint, and the other | |
| 328 | + | // way round. The heavier of the two arms on the crossing axis wins: a | |
| 329 | + | // junction is as wide as the widest thing passing through it. | |
| 330 | + | let vertical_half = arms[UP] | |
| 331 | + | .half_extent(c.light_v) | |
| 332 | + | .max(arms[DOWN].half_extent(c.light_v)); | |
| 333 | + | let horizontal_half = arms[LEFT] | |
| 334 | + | .half_extent(c.light_h) | |
| 335 | + | .max(arms[RIGHT].half_extent(c.light_h)); | |
| 336 | + | ||
| 337 | + | for (index, arm) in arms.iter().enumerate() { | |
| 338 | + | if !arm.present() { | |
| 339 | + | continue; | |
| 340 | + | } | |
| 341 | + | let horizontal = index == LEFT || index == RIGHT; | |
| 342 | + | let light = if horizontal { c.light_h } else { c.light_v }; | |
| 343 | + | // Which way the arm runs, and how far past the centre it reaches. | |
| 344 | + | let sign = if index == LEFT || index == DOWN { | |
| 345 | + | -1.0 | |
| 346 | + | } else { | |
| 347 | + | 1.0 | |
| 348 | + | }; | |
| 349 | + | let reach = if horizontal { | |
| 350 | + | vertical_half | |
| 351 | + | } else { | |
| 352 | + | horizontal_half | |
| 353 | + | }; | |
| 354 | + | let edge = match index { | |
| 355 | + | LEFT => c.x0, | |
| 356 | + | RIGHT => c.x1, | |
| 357 | + | UP => c.y1, | |
| 358 | + | _ => c.y0, | |
| 359 | + | }; | |
| 360 | + | let centre = if horizontal { c.cx } else { c.cy }; | |
| 361 | + | let across = if horizontal { c.cy } else { c.cx }; | |
| 362 | + | ||
| 363 | + | match arm { | |
| 364 | + | Arm::N => {} | |
| 365 | + | Arm::L | Arm::H => { | |
| 366 | + | let thickness = if *arm == Arm::H { light * 2.0 } else { light }; | |
| 367 | + | // A single arm runs through the joint and out the far side of | |
| 368 | + | // whatever crosses it, so the corner is square rather than | |
| 369 | + | // notched. With nothing crossing, `reach` is zero and the arm | |
| 370 | + | // stops at the centre, which is what a half-line is. | |
| 371 | + | let stop = centre - reach * sign; | |
| 372 | + | out.extend(bar(c, horizontal, edge, stop, across, thickness, dash)); | |
| 373 | + | } | |
| 374 | + | Arm::D => { | |
| 375 | + | // Two rails, and each one stops in a different place. A rail | |
| 376 | + | // whose side is occupied by a crossing arm stops short of it; | |
| 377 | + | // the other continues to the far rail and closes the corner. | |
| 378 | + | // That single rule draws every corner, tee and cross in the | |
| 379 | + | // double family, including `╬`, whose four arms all stop short | |
| 380 | + | // and leave the middle open. | |
| 381 | + | let gap = light; | |
| 382 | + | for side in [1.0, -1.0] { | |
| 383 | + | let blocked = if horizontal { | |
| 384 | + | arms[if side > 0.0 { UP } else { DOWN }].present() | |
| 385 | + | } else { | |
| 386 | + | arms[if side > 0.0 { RIGHT } else { LEFT }].present() | |
| 387 | + | }; | |
| 388 | + | let stop = if blocked { | |
| 389 | + | centre + reach * sign | |
| 390 | + | } else { | |
| 391 | + | centre - reach * sign | |
| 392 | + | }; | |
| 393 | + | out.extend(bar( | |
| 394 | + | c, | |
| 395 | + | horizontal, | |
| 396 | + | edge, | |
| 397 | + | stop, | |
| 398 | + | across + side * gap, | |
| 399 | + | light, | |
| 400 | + | dash, | |
| 401 | + | )); | |
| 402 | + | } | |
| 403 | + | } | |
| 404 | + | } | |
| 405 | + | } | |
| 406 | + | out | |
| 407 | + | } | |
| 408 | + | ||
| 409 | + | /// One bar from `edge` to `stop`, `thickness` across, centred on `across`. | |
| 410 | + | /// | |
| 411 | + | /// Dashes are cut here rather than by the caller so a dashed arm and a solid | |
| 412 | + | /// one are the same code with a different count. The gap is one light bar wide, | |
| 413 | + | /// which keeps the dash reading as the same weight as its neighbours. | |
| 414 | + | fn bar( | |
| 415 | + | c: &CellBox, | |
| 416 | + | horizontal: bool, | |
| 417 | + | edge: f64, | |
| 418 | + | stop: f64, | |
| 419 | + | across: f64, | |
| 420 | + | thickness: f64, | |
| 421 | + | dash: Dash, | |
| 422 | + | ) -> Vec<Vec<(f64, f64)>> { | |
| 423 | + | let half = thickness / 2.0; | |
| 424 | + | let (lo, hi) = if edge < stop { | |
| 425 | + | (edge, stop) | |
| 426 | + | } else { | |
| 427 | + | (stop, edge) | |
| 428 | + | }; | |
| 429 | + | let count = dash.count(); | |
| 430 | + | let gap = if count > 1 { | |
| 431 | + | if horizontal { c.light_h } else { c.light_v } | |
| 432 | + | } else { | |
| 433 | + | 0.0 | |
| 434 | + | }; | |
| 435 | + | // Length is shared between the dashes and the gaps between them, so a | |
| 436 | + | // quadruple dash is four shorter marks rather than four of the triple's and | |
| 437 | + | // a longer glyph. | |
| 438 | + | let span = (hi - lo - gap * (count as f64 - 1.0)) / count as f64; | |
| 439 | + | (0..count) | |
| 440 | + | .map(|i| { | |
| 441 | + | let a = lo + (span + gap) * i as f64; | |
| 442 | + | let b = a + span; | |
| 443 | + | if horizontal { | |
| 444 | + | CellBox::rect(a, across - half, b, across + half) | |
| 445 | + | } else { | |
| 446 | + | CellBox::rect(across - half, a, across + half, b) | |
| 447 | + | } | |
| 448 | + | }) | |
| 449 | + | .collect() | |
| 450 | + | } | |
| 451 | + | ||
| 452 | + | /// Segments in a quarter turn. | |
| 453 | + | /// | |
| 454 | + | /// Twelve: enough that the curve reads as one at a terminal's size, and a fixed | |
| 455 | + | /// count so every master has the same points, which `gvar` requires. | |
| 456 | + | const ARC_SEGMENTS: usize = 12; | |
| 457 | + | ||
| 458 | + | fn arc(c: &CellBox, arms: [Arm; 4]) -> Vec<Vec<(f64, f64)>> { | |
| 459 | + | // Built as a centre line and then given a thickness, rather than by | |
| 460 | + | // computing the two edges directly. The direct version needs a sign per | |
| 461 | + | // quadrant per edge and gets one of them wrong in a way a bounding box | |
| 462 | + | // cannot see, which is what happened here first time. | |
| 463 | + | // | |
| 464 | + | // The geometry comes off the tangent points rather than off an angle | |
| 465 | + | // sweep, for the same reason. The two straight runs lie on `y = cy` and | |
| 466 | + | // `x = cx` and cross at the cell's centre; a circle tangent to both has its | |
| 467 | + | // own centre one radius along each of them, and touches them at | |
| 468 | + | // `(ox, cy)` and `(cx, oy)`. Those two points are where the straights stop | |
| 469 | + | // and the turn starts, so nothing has to be reasoned about twice. | |
| 470 | + | let horizontal = if arms[LEFT].present() { LEFT } else { RIGHT }; | |
| 471 | + | let vertical = if arms[UP].present() { UP } else { DOWN }; | |
| 472 | + | let x_edge = if horizontal == LEFT { c.x0 } else { c.x1 }; | |
| 473 | + | let y_edge = if vertical == UP { c.y1 } else { c.y0 }; | |
| 474 | + | let x_sign = if horizontal == LEFT { -1.0 } else { 1.0 }; | |
| 475 | + | let y_sign = if vertical == UP { 1.0 } else { -1.0 }; | |
| 476 | + | // A third of the half-cell, so the turn reads as a corner rather than a | |
| 477 | + | // bow and both arms keep a straight run to meet their neighbours squarely. | |
| 478 | + | let radius = ((c.cx - c.x0).abs() / 3.0).min((c.cy - c.y0).abs() / 3.0); | |
| 479 | + | let (ox, oy) = (c.cx + radius * x_sign, c.cy + radius * y_sign); | |
| 480 | + | ||
| 481 | + | let mut centre: Vec<(f64, f64)> = vec![(x_edge, c.cy)]; | |
| 482 | + | for i in 0..=ARC_SEGMENTS { | |
| 483 | + | let t = i as f64 / ARC_SEGMENTS as f64 * std::f64::consts::FRAC_PI_2; | |
| 484 | + | let (sin, cos) = t.sin_cos(); | |
| 485 | + | centre.push((ox - radius * x_sign * sin, oy - radius * y_sign * cos)); | |
| 486 | + | } | |
| 487 | + | centre.push((c.cx, y_edge)); | |
| 488 | + | vec![stroke(¢re, c.light_h / 2.0)] | |
| 489 | + | } | |
| 490 | + | ||
| 491 | + | /// A polyline given a thickness: one closed contour, out along one side and | |
| 492 | + | /// back along the other. | |
| 493 | + | /// | |
| 494 | + | /// The offset at each point is perpendicular to the average of the segments | |
| 495 | + | /// meeting there, which keeps the width even around a turn instead of pinching | |
| 496 | + | /// on the inside of it. | |
| 497 | + | fn stroke(centre: &[(f64, f64)], half: f64) -> Vec<(f64, f64)> { | |
| 498 | + | let normals: Vec<(f64, f64)> = (0..centre.len()) | |
| 499 | + | .map(|i| { | |
| 500 | + | let before = i.saturating_sub(1); |
Lines truncated
| @@ -1,0 +1,186 @@ | |||
| 1 | + | // Generated by scripts/derive-cell-table.py from Unicode character names. | |
| 2 | + | // Do not edit by hand: re-run the script instead. See its header for why | |
| 3 | + | // the table is derived rather than authored. | |
| 4 | + | // | |
| 5 | + | // 121 stems, 4 arcs, 3 diagonals, 29 fills, 3 shades. | |
| 6 | + | ||
| 7 | + | use super::Arm::{D, H, L, N}; | |
| 8 | + | use super::{Dash, Diagonal, Shade}; | |
| 9 | + | ||
| 10 | + | /// Arms in the order left, right, up, down. | |
| 11 | + | pub(super) const STEMS: [(u32, [super::Arm; 4], Dash); 121] = [ | |
| 12 | + | (0x2500, [L, L, N, N], Dash::None), | |
| 13 | + | (0x2501, [H, H, N, N], Dash::None), | |
| 14 | + | (0x2502, [N, N, L, L], Dash::None), | |
| 15 | + | (0x2503, [N, N, H, H], Dash::None), | |
| 16 | + | (0x2504, [L, L, N, N], Dash::Triple), | |
| 17 | + | (0x2505, [H, H, N, N], Dash::Triple), | |
| 18 | + | (0x2506, [N, N, L, L], Dash::Triple), | |
| 19 | + | (0x2507, [N, N, H, H], Dash::Triple), | |
| 20 | + | (0x2508, [L, L, N, N], Dash::Quadruple), | |
| 21 | + | (0x2509, [H, H, N, N], Dash::Quadruple), | |
| 22 | + | (0x250A, [N, N, L, L], Dash::Quadruple), | |
| 23 | + | (0x250B, [N, N, H, H], Dash::Quadruple), | |
| 24 | + | (0x250C, [N, L, N, L], Dash::None), | |
| 25 | + | (0x250D, [N, H, N, L], Dash::None), | |
| 26 | + | (0x250E, [N, L, N, H], Dash::None), | |
| 27 | + | (0x250F, [N, H, N, H], Dash::None), | |
| 28 | + | (0x2510, [L, N, N, L], Dash::None), | |
| 29 | + | (0x2511, [H, N, N, L], Dash::None), | |
| 30 | + | (0x2512, [L, N, N, H], Dash::None), | |
| 31 | + | (0x2513, [H, N, N, H], Dash::None), | |
| 32 | + | (0x2514, [N, L, L, N], Dash::None), | |
| 33 | + | (0x2515, [N, H, L, N], Dash::None), | |
| 34 | + | (0x2516, [N, L, H, N], Dash::None), | |
| 35 | + | (0x2517, [N, H, H, N], Dash::None), | |
| 36 | + | (0x2518, [L, N, L, N], Dash::None), | |
| 37 | + | (0x2519, [H, N, L, N], Dash::None), | |
| 38 | + | (0x251A, [L, N, H, N], Dash::None), | |
| 39 | + | (0x251B, [H, N, H, N], Dash::None), | |
| 40 | + | (0x251C, [N, L, L, L], Dash::None), | |
| 41 | + | (0x251D, [N, H, L, L], Dash::None), | |
| 42 | + | (0x251E, [N, L, H, L], Dash::None), | |
| 43 | + | (0x251F, [N, L, L, H], Dash::None), | |
| 44 | + | (0x2520, [N, L, H, H], Dash::None), | |
| 45 | + | (0x2521, [N, H, H, L], Dash::None), | |
| 46 | + | (0x2522, [N, H, L, H], Dash::None), | |
| 47 | + | (0x2523, [N, H, H, H], Dash::None), | |
| 48 | + | (0x2524, [L, N, L, L], Dash::None), | |
| 49 | + | (0x2525, [H, N, L, L], Dash::None), | |
| 50 | + | (0x2526, [L, N, H, L], Dash::None), | |
| 51 | + | (0x2527, [L, N, L, H], Dash::None), | |
| 52 | + | (0x2528, [L, N, H, H], Dash::None), | |
| 53 | + | (0x2529, [H, N, H, L], Dash::None), | |
| 54 | + | (0x252A, [H, N, L, H], Dash::None), | |
| 55 | + | (0x252B, [H, N, H, H], Dash::None), | |
| 56 | + | (0x252C, [L, L, N, L], Dash::None), | |
| 57 | + | (0x252D, [H, L, N, L], Dash::None), | |
| 58 | + | (0x252E, [L, H, N, L], Dash::None), | |
| 59 | + | (0x252F, [H, H, N, L], Dash::None), | |
| 60 | + | (0x2530, [L, L, N, H], Dash::None), | |
| 61 | + | (0x2531, [H, L, N, H], Dash::None), | |
| 62 | + | (0x2532, [L, H, N, H], Dash::None), | |
| 63 | + | (0x2533, [H, H, N, H], Dash::None), | |
| 64 | + | (0x2534, [L, L, L, N], Dash::None), | |
| 65 | + | (0x2535, [H, L, L, N], Dash::None), | |
| 66 | + | (0x2536, [L, H, L, N], Dash::None), | |
| 67 | + | (0x2537, [H, H, L, N], Dash::None), | |
| 68 | + | (0x2538, [L, L, H, N], Dash::None), | |
| 69 | + | (0x2539, [H, L, H, N], Dash::None), | |
| 70 | + | (0x253A, [L, H, H, N], Dash::None), | |
| 71 | + | (0x253B, [H, H, H, N], Dash::None), | |
| 72 | + | (0x253C, [L, L, L, L], Dash::None), | |
| 73 | + | (0x253D, [H, L, L, L], Dash::None), | |
| 74 | + | (0x253E, [L, H, L, L], Dash::None), | |
| 75 | + | (0x253F, [H, H, L, L], Dash::None), | |
| 76 | + | (0x2540, [L, L, H, L], Dash::None), | |
| 77 | + | (0x2541, [L, L, L, H], Dash::None), | |
| 78 | + | (0x2542, [L, L, H, H], Dash::None), | |
| 79 | + | (0x2543, [H, L, H, L], Dash::None), | |
| 80 | + | (0x2544, [L, H, H, L], Dash::None), | |
| 81 | + | (0x2545, [H, L, L, H], Dash::None), | |
| 82 | + | (0x2546, [L, H, L, H], Dash::None), | |
| 83 | + | (0x2547, [H, H, H, L], Dash::None), | |
| 84 | + | (0x2548, [H, H, L, H], Dash::None), | |
| 85 | + | (0x2549, [H, L, H, H], Dash::None), | |
| 86 | + | (0x254A, [L, H, H, H], Dash::None), | |
| 87 | + | (0x254B, [H, H, H, H], Dash::None), | |
| 88 | + | (0x254C, [L, L, N, N], Dash::Double), | |
| 89 | + | (0x254D, [H, H, N, N], Dash::Double), | |
| 90 | + | (0x254E, [N, N, L, L], Dash::Double), | |
| 91 | + | (0x254F, [N, N, H, H], Dash::Double), | |
| 92 | + | (0x2550, [D, D, N, N], Dash::None), | |
| 93 | + | (0x2551, [N, N, D, D], Dash::None), | |
| 94 | + | (0x2552, [N, D, N, L], Dash::None), | |
| 95 | + | (0x2553, [N, L, N, D], Dash::None), | |
| 96 | + | (0x2554, [N, D, N, D], Dash::None), | |
| 97 | + | (0x2555, [D, N, N, L], Dash::None), | |
| 98 | + | (0x2556, [L, N, N, D], Dash::None), | |
| 99 | + | (0x2557, [D, N, N, D], Dash::None), | |
| 100 | + | (0x2558, [N, D, L, N], Dash::None), | |
| 101 | + | (0x2559, [N, L, D, N], Dash::None), | |
| 102 | + | (0x255A, [N, D, D, N], Dash::None), | |
| 103 | + | (0x255B, [D, N, L, N], Dash::None), | |
| 104 | + | (0x255C, [L, N, D, N], Dash::None), | |
| 105 | + | (0x255D, [D, N, D, N], Dash::None), | |
| 106 | + | (0x255E, [N, D, L, L], Dash::None), | |
| 107 | + | (0x255F, [N, L, D, D], Dash::None), | |
| 108 | + | (0x2560, [N, D, D, D], Dash::None), | |
| 109 | + | (0x2561, [D, N, L, L], Dash::None), | |
| 110 | + | (0x2562, [L, N, D, D], Dash::None), | |
| 111 | + | (0x2563, [D, N, D, D], Dash::None), | |
| 112 | + | (0x2564, [D, D, N, L], Dash::None), | |
| 113 | + | (0x2565, [L, L, N, D], Dash::None), | |
| 114 | + | (0x2566, [D, D, N, D], Dash::None), | |
| 115 | + | (0x2567, [D, D, L, N], Dash::None), | |
| 116 | + | (0x2568, [L, L, D, N], Dash::None), | |
| 117 | + | (0x2569, [D, D, D, N], Dash::None), | |
| 118 | + | (0x256A, [D, D, L, L], Dash::None), | |
| 119 | + | (0x256B, [L, L, D, D], Dash::None), | |
| 120 | + | (0x256C, [D, D, D, D], Dash::None), | |
| 121 | + | (0x2574, [L, N, N, N], Dash::None), | |
| 122 | + | (0x2575, [N, N, L, N], Dash::None), | |
| 123 | + | (0x2576, [N, L, N, N], Dash::None), | |
| 124 | + | (0x2577, [N, N, N, L], Dash::None), | |
| 125 | + | (0x2578, [H, N, N, N], Dash::None), | |
| 126 | + | (0x2579, [N, N, H, N], Dash::None), | |
| 127 | + | (0x257A, [N, H, N, N], Dash::None), | |
| 128 | + | (0x257B, [N, N, N, H], Dash::None), | |
| 129 | + | (0x257C, [L, H, N, N], Dash::None), | |
| 130 | + | (0x257D, [N, N, L, H], Dash::None), | |
| 131 | + | (0x257E, [H, L, N, N], Dash::None), | |
| 132 | + | (0x257F, [N, N, H, L], Dash::None), | |
| 133 | + | ]; | |
| 134 | + | ||
| 135 | + | /// The rounded corners. Same arms as a stem, drawn as a quarter turn. | |
| 136 | + | pub(super) const ARCS: [(u32, [super::Arm; 4]); 4] = [ | |
| 137 | + | (0x256D, [N, L, N, L]), | |
| 138 | + | (0x256E, [L, N, N, L]), | |
| 139 | + | (0x256F, [L, N, L, N]), | |
| 140 | + | (0x2570, [N, L, L, N]), | |
| 141 | + | ]; | |
| 142 | + | ||
| 143 | + | pub(super) const DIAGONALS: [(u32, Diagonal); 3] = [ | |
| 144 | + | (0x2571, Diagonal::Rising), | |
| 145 | + | (0x2572, Diagonal::Falling), | |
| 146 | + | (0x2573, Diagonal::Cross), | |
| 147 | + | ]; | |
| 148 | + | ||
| 149 | + | /// Filled rectangles in eighths of the cell: x0, y0, x1, y1, with y up. | |
| 150 | + | pub(super) const FILLS: [(u32, &[(u8, u8, u8, u8)]); 29] = [ | |
| 151 | + | (0x2580, &[(0, 4, 8, 8)]), | |
| 152 | + | (0x2581, &[(0, 0, 8, 1)]), | |
| 153 | + | (0x2582, &[(0, 0, 8, 2)]), | |
| 154 | + | (0x2583, &[(0, 0, 8, 3)]), | |
| 155 | + | (0x2584, &[(0, 0, 8, 4)]), | |
| 156 | + | (0x2585, &[(0, 0, 8, 5)]), | |
| 157 | + | (0x2586, &[(0, 0, 8, 6)]), | |
| 158 | + | (0x2587, &[(0, 0, 8, 7)]), | |
| 159 | + | (0x2588, &[(0, 0, 8, 8)]), | |
| 160 | + | (0x2589, &[(0, 0, 7, 8)]), | |
| 161 | + | (0x258A, &[(0, 0, 6, 8)]), | |
| 162 | + | (0x258B, &[(0, 0, 5, 8)]), | |
| 163 | + | (0x258C, &[(0, 0, 4, 8)]), | |
| 164 | + | (0x258D, &[(0, 0, 3, 8)]), | |
| 165 | + | (0x258E, &[(0, 0, 2, 8)]), | |
| 166 | + | (0x258F, &[(0, 0, 1, 8)]), | |
| 167 | + | (0x2590, &[(4, 0, 8, 8)]), | |
| 168 | + | (0x2594, &[(0, 7, 8, 8)]), | |
| 169 | + | (0x2595, &[(7, 0, 8, 8)]), | |
| 170 | + | (0x2596, &[(0, 0, 4, 4)]), | |
| 171 | + | (0x2597, &[(4, 0, 8, 4)]), | |
| 172 | + | (0x2598, &[(0, 4, 4, 8)]), | |
| 173 | + | (0x2599, &[(0, 4, 4, 8), (0, 0, 4, 4), (4, 0, 8, 4)]), | |
| 174 | + | (0x259A, &[(0, 4, 4, 8), (4, 0, 8, 4)]), | |
| 175 | + | (0x259B, &[(0, 4, 4, 8), (4, 4, 8, 8), (0, 0, 4, 4)]), | |
| 176 | + | (0x259C, &[(0, 4, 4, 8), (4, 4, 8, 8), (4, 0, 8, 4)]), | |
| 177 | + | (0x259D, &[(4, 4, 8, 8)]), | |
| 178 | + | (0x259E, &[(4, 4, 8, 8), (0, 0, 4, 4)]), | |
| 179 | + | (0x259F, &[(4, 4, 8, 8), (0, 0, 4, 4), (4, 0, 8, 4)]), | |
| 180 | + | ]; | |
| 181 | + | ||
| 182 | + | pub(super) const SHADES: [(u32, Shade); 3] = [ | |
| 183 | + | (0x2591, Shade::Light), | |
| 184 | + | (0x2592, Shade::Medium), | |
| 185 | + | (0x2593, Shade::Dark), | |
| 186 | + | ]; |