| 1 |
|
| 2 |
|
| 3 |
|
| 4 |
|
| 5 |
|
| 6 |
|
| 7 |
|
| 8 |
|
| 9 |
|
| 10 |
|
| 11 |
|
| 12 |
use kurbo::BezPath; |
| 13 |
|
| 14 |
use crate::base::BaseParams; |
| 15 |
use crate::manifest::{Anchor, Dim, Direction, Shape}; |
| 16 |
|
| 17 |
|
| 18 |
pub struct Drawing { |
| 19 |
pub contours: Vec<Vec<(f64, f64)>>, |
| 20 |
} |
| 21 |
|
| 22 |
impl Drawing { |
| 23 |
|
| 24 |
|
| 25 |
|
| 26 |
|
| 27 |
|
| 28 |
|
| 29 |
pub fn to_bezpath(&self) -> BezPath { |
| 30 |
let mut path = BezPath::new(); |
| 31 |
for contour in &self.contours { |
| 32 |
let mut points = contour.clone(); |
| 33 |
if signed_area(&points) > 0.0 { |
| 34 |
points.reverse(); |
| 35 |
} |
| 36 |
let Some(&(x, y)) = points.first() else { |
| 37 |
continue; |
| 38 |
}; |
| 39 |
path.move_to((x, y)); |
| 40 |
for &(x, y) in &points[1..] { |
| 41 |
path.line_to((x, y)); |
| 42 |
} |
| 43 |
path.close_path(); |
| 44 |
} |
| 45 |
path |
| 46 |
} |
| 47 |
} |
| 48 |
|
| 49 |
|
| 50 |
fn stroke_of(params: &BaseParams) -> f64 { |
| 51 |
f64::from(params.stroke) |
| 52 |
} |
| 53 |
|
| 54 |
fn signed_area(points: &[(f64, f64)]) -> f64 { |
| 55 |
let n = points.len(); |
| 56 |
let mut area = 0.0; |
| 57 |
for i in 0..n { |
| 58 |
let (x0, y0) = points[i]; |
| 59 |
let (x1, y1) = points[(i + 1) % n]; |
| 60 |
area += x0 * y1 - x1 * y0; |
| 61 |
} |
| 62 |
area / 2.0 |
| 63 |
} |
| 64 |
|
| 65 |
pub fn draw(shape: &Shape, params: &BaseParams) -> Drawing { |
| 66 |
match shape { |
| 67 |
|
| 68 |
|
| 69 |
|
| 70 |
Shape::Cell(cell) => crate::cells::draw(*cell, params), |
| 71 |
Shape::Triangle { |
| 72 |
direction, |
| 73 |
span, |
| 74 |
depth, |
| 75 |
anchor, |
| 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 |
), |
| 93 |
Shape::Cross { |
| 94 |
width, |
| 95 |
height, |
| 96 |
stroke, |
| 97 |
} => cross(params, *width, *height, *stroke), |
| 98 |
Shape::OpenBox { |
| 99 |
width, |
| 100 |
height, |
| 101 |
bottom, |
| 102 |
stroke, |
| 103 |
} => open_box(params, *width, *height, *bottom, *stroke), |
| 104 |
Shape::ReturnArrow { |
| 105 |
top, |
| 106 |
shaft, |
| 107 |
head_span, |
| 108 |
head_depth, |
| 109 |
stroke, |
| 110 |
} => return_arrow(params, *top, *shaft, *head_span, *head_depth, *stroke), |
| 111 |
} |
| 112 |
} |
| 113 |
|
| 114 |
fn anchor_y(params: &BaseParams, anchor: Anchor) -> f64 { |
| 115 |
match anchor { |
| 116 |
Anchor::BandCenter => params.band_center_y(), |
| 117 |
Anchor::XHeight => params.x_height_center_y(), |
| 118 |
} |
| 119 |
} |
| 120 |
|
| 121 |
|
| 122 |
|
| 123 |
|
| 124 |
|
| 125 |
|
| 126 |
|
| 127 |
|
| 128 |
|
| 129 |
|
| 130 |
|
| 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 |
|
| 143 |
|
| 144 |
|
| 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 |
|
| 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 |
|
| 169 |
|
| 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 |
|
| 197 |
fn triangle( |
| 198 |
params: &BaseParams, |
| 199 |
direction: Direction, |
| 200 |
span: Dim, |
| 201 |
depth: f64, |
| 202 |
anchor: Anchor, |
| 203 |
) -> Drawing { |
| 204 |
let cx = params.center_x(); |
| 205 |
let cy = anchor_y(params, anchor); |
| 206 |
let (span_px, depth_px) = match direction { |
| 207 |
|
| 208 |
|
| 209 |
Direction::Up | Direction::Down => { |
| 210 |
let s = span.resolve(params.band_width(), stroke_of(params)); |
| 211 |
(s, s * depth) |
| 212 |
} |
| 213 |
Direction::Left | Direction::Right => { |
| 214 |
let s = span.resolve(params.band_height(), stroke_of(params)); |
| 215 |
(s, s * depth) |
| 216 |
} |
| 217 |
}; |
| 218 |
let half_span = span_px / 2.0; |
| 219 |
let half_depth = depth_px / 2.0; |
| 220 |
|
| 221 |
let points = match direction { |
| 222 |
Direction::Up => vec![ |
| 223 |
(cx - half_span, cy - half_depth), |
| 224 |
(cx + half_span, cy - half_depth), |
| 225 |
(cx, cy + half_depth), |
| 226 |
], |
| 227 |
Direction::Down => vec![ |
| 228 |
(cx - half_span, cy + half_depth), |
| 229 |
(cx + half_span, cy + half_depth), |
| 230 |
(cx, cy - half_depth), |
| 231 |
], |
| 232 |
Direction::Right => vec![ |
| 233 |
(cx - half_depth, cy - half_span), |
| 234 |
(cx - half_depth, cy + half_span), |
| 235 |
(cx + half_depth, cy), |
| 236 |
], |
| 237 |
Direction::Left => vec![ |
| 238 |
(cx + half_depth, cy - half_span), |
| 239 |
(cx + half_depth, cy + half_span), |
| 240 |
(cx - half_depth, cy), |
| 241 |
], |
| 242 |
}; |
| 243 |
Drawing { |
| 244 |
contours: vec![points], |
| 245 |
} |
| 246 |
} |
| 247 |
|
| 248 |
|
| 249 |
|
| 250 |
|
| 251 |
|
| 252 |
fn cross(params: &BaseParams, width: Dim, height: Dim, stroke: f64) -> Drawing { |
| 253 |
let cx = params.center_x(); |
| 254 |
let cy = params.band_center_y(); |
| 255 |
let base_stroke = stroke_of(params); |
| 256 |
let half_w = width.resolve(params.band_width(), base_stroke) / 2.0; |
| 257 |
let half_h = height.resolve(params.band_height(), base_stroke) / 2.0; |
| 258 |
let weight = base_stroke * stroke; |
| 259 |
|
| 260 |
|
| 261 |
|
| 262 |
|
| 263 |
|
| 264 |
|
| 265 |
let diagonal = half_w.hypot(half_h); |
| 266 |
let gap_y = weight / 2.0 * diagonal / half_w; |
| 267 |
let gap_x = weight / 2.0 * diagonal / half_h; |
| 268 |
|
| 269 |
let points = vec![ |
| 270 |
(cx - half_w, cy + half_h - gap_y), |
| 271 |
(cx - half_w + gap_x, cy + half_h), |
| 272 |
(cx, cy + gap_y), |
| 273 |
(cx + half_w - gap_x, cy + half_h), |
| 274 |
(cx + half_w, cy + half_h - gap_y), |
| 275 |
(cx + gap_x, cy), |
| 276 |
(cx + half_w, cy - half_h + gap_y), |
| 277 |
(cx + half_w - gap_x, cy - half_h), |
| 278 |
(cx, cy - gap_y), |
| 279 |
(cx - half_w + gap_x, cy - half_h), |
| 280 |
(cx - half_w, cy - half_h + gap_y), |
| 281 |
(cx - gap_x, cy), |
| 282 |
]; |
| 283 |
Drawing { |
| 284 |
contours: vec![points], |
| 285 |
} |
| 286 |
} |
| 287 |
|
| 288 |
|
| 289 |
fn open_box(params: &BaseParams, width: Dim, height: Dim, bottom: f64, stroke: f64) -> Drawing { |
| 290 |
let cx = params.center_x(); |
| 291 |
let base_stroke = stroke_of(params); |
| 292 |
let half_w = width.resolve(params.band_width(), base_stroke) / 2.0; |
| 293 |
let h = height.resolve(params.band_height(), base_stroke); |
| 294 |
let y0 = params.band_height() * bottom; |
| 295 |
let y1 = y0 + h; |
| 296 |
|
| 297 |
|
| 298 |
let riser = f64::from(params.stem) * stroke; |
| 299 |
let floor = f64::from(params.stroke) * stroke; |
| 300 |
|
| 301 |
let points = vec![ |
| 302 |
(cx - half_w, y1), |
| 303 |
(cx - half_w + riser, y1), |
| 304 |
(cx - half_w + riser, y0 + floor), |
| 305 |
(cx + half_w - riser, y0 + floor), |
| 306 |
(cx + half_w - riser, y1), |
| 307 |
(cx + half_w, y1), |
| 308 |
(cx + half_w, y0), |
| 309 |
(cx - half_w, y0), |
| 310 |
]; |
| 311 |
Drawing { |
| 312 |
contours: vec![points], |
| 313 |
} |
| 314 |
} |
| 315 |
|
| 316 |
|
| 317 |
|
| 318 |
fn return_arrow( |
| 319 |
params: &BaseParams, |
| 320 |
top: f64, |
| 321 |
shaft: f64, |
| 322 |
head_span: Dim, |
| 323 |
head_depth: Dim, |
| 324 |
stroke: f64, |
| 325 |
) -> Drawing { |
| 326 |
let x0 = f64::from(params.band_x0); |
| 327 |
let x1 = f64::from(params.band_x1); |
| 328 |
let y_floor = f64::from(params.band_y0); |
| 329 |
let band_h = params.band_height(); |
| 330 |
|
| 331 |
let weight = f64::from(params.stroke) * stroke; |
| 332 |
let riser_weight = f64::from(params.stem) * stroke; |
| 333 |
let half = weight / 2.0; |
| 334 |
let shaft_y = y_floor + band_h * shaft; |
| 335 |
let cap_y = y_floor + band_h * top; |
| 336 |
|
| 337 |
|
| 338 |
|
| 339 |
let head_half = head_span.resolve(band_h, f64::from(params.stroke)) / 2.0; |
| 340 |
let head_x = x0 + head_depth.resolve(x1 - x0, f64::from(params.stroke)); |
| 341 |
|
| 342 |
let points = vec![ |
| 343 |
|
| 344 |
(x0, shaft_y), |
| 345 |
(head_x, shaft_y + head_half), |
| 346 |
(head_x, shaft_y + half), |
| 347 |
|
| 348 |
(x1 - riser_weight, shaft_y + half), |
| 349 |
(x1 - riser_weight, cap_y), |
| 350 |
(x1, cap_y), |
| 351 |
|
| 352 |
(x1, shaft_y - half), |
| 353 |
(head_x, shaft_y - half), |
| 354 |
(head_x, shaft_y - head_half), |
| 355 |
]; |
| 356 |
Drawing { |
| 357 |
contours: vec![points], |
| 358 |
} |
| 359 |
} |
| 360 |
|
| 361 |
#[cfg(test)] |
| 362 |
mod tests { |
| 363 |
use super::*; |
| 364 |
use crate::manifest::{Manifest, WeightResponse}; |
| 365 |
|
| 366 |
|
| 367 |
|
| 368 |
|
| 369 |
|
| 370 |
|
| 371 |
|
| 372 |
|
| 373 |
fn light() -> BaseParams { |
| 374 |
BaseParams { |
| 375 |
upem: 1000, |
| 376 |
advance: 632, |
| 377 |
cap_height: 668, |
| 378 |
x_height: 496, |
| 379 |
stem: 54, |
| 380 |
stroke: 55, |
| 381 |
band_x0: 68, |
| 382 |
band_x1: 564, |
| 383 |
band_y0: 0, |
| 384 |
band_y1: 496, |
| 385 |
ascent: 984, |
| 386 |
descent: -316, |
| 387 |
} |
| 388 |
} |
| 389 |
|
| 390 |
|
| 391 |
|
| 392 |
|
| 393 |
fn heavy() -> BaseParams { |
| 394 |
BaseParams { |
| 395 |
stem: 158, |
| 396 |
stroke: 139, |
| 397 |
|
| 398 |
band_x0: 58, |
| 399 |
band_x1: 574, |
| 400 |
..light() |
| 401 |
} |
| 402 |
} |
| 403 |
|
| 404 |
fn bounds(drawing: &Drawing) -> (f64, f64, f64, f64) { |
| 405 |
let points = drawing.contours.concat(); |
| 406 |
let xs: Vec<f64> = points.iter().map(|p| p.0).collect(); |
| 407 |
let ys: Vec<f64> = points.iter().map(|p| p.1).collect(); |
| 408 |
( |
| 409 |
xs.iter().copied().fold(f64::MAX, f64::min), |
| 410 |
ys.iter().copied().fold(f64::MAX, f64::min), |
| 411 |
xs.iter().copied().fold(f64::MIN, f64::max), |
| 412 |
ys.iter().copied().fold(f64::MIN, f64::max), |
| 413 |
) |
| 414 |
} |
| 415 |
|
| 416 |
|
| 417 |
|
| 418 |
|
| 419 |
|
| 420 |
|
| 421 |
fn fill_of(drawing: &Drawing) -> f64 { |
| 422 |
let ink: f64 = drawing.contours.iter().map(|c| signed_area(c).abs()).sum(); |
| 423 |
let (x0, y0, x1, y1) = bounds(drawing); |
| 424 |
ink / ((x1 - x0) * (y1 - y0)) |
| 425 |
} |
| 426 |
|
| 427 |
fn shape(name: &str) -> Shape { |
| 428 |
let manifest = Manifest::parse(crate::HOUSE_SET).unwrap(); |
| 429 |
let mut glyphs = manifest.glyphs; |
| 430 |
let index = glyphs |
| 431 |
.iter() |
| 432 |
.position(|g| g.name == name) |
| 433 |
.unwrap_or_else(|| panic!("no glyph {name}")); |
| 434 |
glyphs.swap_remove(index).shape |
| 435 |
} |
| 436 |
|
| 437 |
|
| 438 |
|
| 439 |
|
| 440 |
|
| 441 |
|
| 442 |
|
| 443 |
#[test] |
| 444 |
fn an_arrow_with_two_heads_still_has_a_shaft() { |
| 445 |
let params = light(); |
| 446 |
let drawing = draw(&shape("uni2195"), ¶ms); |
| 447 |
let heads: Vec<&Vec<(f64, f64)>> = |
| 448 |
drawing.contours.iter().filter(|c| c.len() == 3).collect(); |
| 449 |
assert_eq!(heads.len(), 2, "`↕` has a head at each end"); |
| 450 |
let top = heads |
| 451 |
.iter() |
| 452 |
.map(|c| c.iter().map(|p| p.1).fold(f64::MIN, f64::max)) |
| 453 |
.fold(f64::MIN, f64::max); |
| 454 |
let bottom = heads |
| 455 |
.iter() |
| 456 |
.map(|c| c.iter().map(|p| p.1).fold(f64::MAX, f64::min)) |
| 457 |
.fold(f64::MAX, f64::min); |
| 458 |
let head_depth = heads |
| 459 |
.iter() |
| 460 |
.map(|c| { |
| 461 |
let ys: Vec<f64> = c.iter().map(|p| p.1).collect(); |
| 462 |
ys.iter().copied().fold(f64::MIN, f64::max) |
| 463 |
- ys.iter().copied().fold(f64::MAX, f64::min) |
| 464 |
}) |
| 465 |
.fold(f64::MIN, f64::max); |
| 466 |
let shaft = (top - bottom) - head_depth * 2.0; |
| 467 |
assert!( |
| 468 |
shaft > (top - bottom) * 0.25, |
| 469 |
"the heads leave {shaft:.0} units of shaft in {:.0} of arrow", |
| 470 |
top - bottom |
| 471 |
); |
| 472 |
} |
| 473 |
|
| 474 |
#[test] |
| 475 |
fn every_mark_fits_inside_the_cell() { |
| 476 |
let manifest = Manifest::parse(crate::HOUSE_SET).unwrap(); |
| 477 |
for params in [light(), heavy()] { |
| 478 |
|
| 479 |
|
| 480 |
for glyph in manifest |
| 481 |
.glyphs |
| 482 |
.iter() |
| 483 |
.filter(|glyph| !glyph.shape.is_cell_furniture()) |
| 484 |
{ |
| 485 |
let drawing = draw(&glyph.shape, ¶ms); |
| 486 |
let (x0, _, x1, _) = bounds(&drawing); |
| 487 |
assert!( |
| 488 |
x0 >= 0.0 && x1 <= f64::from(params.advance), |
| 489 |
"{} runs outside the cell: x[{x0}, {x1}] in {}", |
| 490 |
glyph.name, |
| 491 |
params.advance |
| 492 |
); |
| 493 |
} |
| 494 |
} |
| 495 |
} |
| 496 |
|
| 497 |
#[test] |
| 498 |
fn every_mark_sits_above_the_descender() { |
| 499 |
let manifest = Manifest::parse(crate::HOUSE_SET).unwrap(); |
| 500 |
let params = light(); |
| 501 |
|
| 502 |
|
| 503 |
for glyph in manifest |
| 504 |
.glyphs |
| 505 |
.iter() |
| 506 |
.filter(|glyph| !glyph.shape.is_cell_furniture()) |
| 507 |
{ |
| 508 |
let (_, y0, _, y1) = bounds(&draw(&glyph.shape, ¶ms)); |
| 509 |
assert!( |
| 510 |
y0 > -350.0, |
| 511 |
"{} dips below the base's descender", |
| 512 |
glyph.name |
| 513 |
); |
| 514 |
assert!( |
| 515 |
y1 <= f64::from(params.cap_height) + 60.0, |
| 516 |
"{} rides above cap height", |
| 517 |
glyph.name |
| 518 |
); |
| 519 |
} |
| 520 |
} |
| 521 |
|
| 522 |
#[test] |
| 523 |
fn marks_are_centred_on_the_cell_not_the_band() { |
| 524 |
let params = light(); |
| 525 |
for name in ["uni25B2", "uni25BC", "uni25B8", "uni25C2", "uni2718"] { |
| 526 |
let (x0, _, x1, _) = bounds(&draw(&shape(name), ¶ms)); |
| 527 |
let centre = f64::midpoint(x0, x1); |
| 528 |
assert!( |
| 529 |
(centre - params.center_x()).abs() < 0.51, |
| 530 |
"{name} centres at {centre}, not {}", |
| 531 |
params.center_x() |
| 532 |
); |
| 533 |
} |
| 534 |
} |
| 535 |
|
| 536 |
#[test] |
| 537 |
fn every_mark_answers_a_heavier_base() { |
| 538 |
let manifest = Manifest::parse(crate::HOUSE_SET).unwrap(); |
| 539 |
|
| 540 |
|
| 541 |
for glyph in manifest |
| 542 |
.glyphs |
| 543 |
.iter() |
| 544 |
.filter(|glyph| !glyph.shape.is_cell_furniture()) |
| 545 |
{ |
| 546 |
let light = draw(&glyph.shape, &light()).to_bezpath(); |
| 547 |
let bold = draw(&glyph.shape, &heavy()).to_bezpath(); |
| 548 |
assert_ne!( |
| 549 |
light.to_svg(), |
| 550 |
bold.to_svg(), |
| 551 |
"{} is drawn identically at both weights, so it will read light \ |
| 552 |
inside the Bold face", |
| 553 |
glyph.name |
| 554 |
); |
| 555 |
} |
| 556 |
} |
| 557 |
|
| 558 |
|
| 559 |
|
| 560 |
|
| 561 |
|
| 562 |
|
| 563 |
|
| 564 |
|
| 565 |
|
| 566 |
|
| 567 |
#[test] |
| 568 |
fn a_solid_mark_scales_and_a_stroked_one_thickens() { |
| 569 |
let manifest = Manifest::parse(crate::HOUSE_SET).unwrap(); |
| 570 |
|
| 571 |
|
| 572 |
for glyph in manifest |
| 573 |
.glyphs |
| 574 |
.iter() |
| 575 |
.filter(|glyph| !glyph.shape.is_cell_furniture()) |
| 576 |
{ |
| 577 |
let thin = fill_of(&draw(&glyph.shape, &light())); |
| 578 |
let thick = fill_of(&draw(&glyph.shape, &heavy())); |
| 579 |
match glyph.shape.weight_response() { |
| 580 |
WeightResponse::Grows => { |
| 581 |
let light_bounds = bounds(&draw(&glyph.shape, &light())); |
| 582 |
let heavy_bounds = bounds(&draw(&glyph.shape, &heavy())); |
| 583 |
assert!( |
| 584 |
(thick - thin).abs() < 0.01, |
| 585 |
"{} inks {thin:.3} of its box at the light end and {thick:.3} at the \ |
| 586 |
heavy one. A solid mark scales, so its fill is the shape's own constant.", |
| 587 |
glyph.name |
| 588 |
); |
| 589 |
assert!( |
| 590 |
(heavy_bounds.2 - heavy_bounds.0) > (light_bounds.2 - light_bounds.0) + 0.5, |
| 591 |
"{} has no stroke to thicken, so it has to grow", |
| 592 |
glyph.name |
| 593 |
); |
| 594 |
} |
| 595 |
|
| 596 |
|
| 597 |
WeightResponse::Holds => unreachable!("{} is an authored mark", glyph.name), |
| 598 |
WeightResponse::Thickens => assert!( |
| 599 |
thick > thin * 1.3, |
| 600 |
"{} inks {thin:.3} of its box at the light end and {thick:.3} at the heavy \ |
| 601 |
one, so its stroke is not following the base's", |
| 602 |
glyph.name |
| 603 |
), |
| 604 |
} |
| 605 |
} |
| 606 |
} |
| 607 |
|
| 608 |
|
| 609 |
|
| 610 |
|
| 611 |
|
| 612 |
|
| 613 |
|
| 614 |
|
| 615 |
#[test] |
| 616 |
fn the_space_render_keeps_its_counter_open() { |
| 617 |
let Shape::OpenBox { |
| 618 |
width, |
| 619 |
height, |
| 620 |
stroke, |
| 621 |
.. |
| 622 |
} = shape("uni2423") |
| 623 |
else { |
| 624 |
panic!("uni2423 is not an open box"); |
| 625 |
}; |
| 626 |
for params in [light(), heavy()] { |
| 627 |
let bar = f64::from(params.stroke) * stroke; |
| 628 |
let box_width = width.resolve(params.band_width(), f64::from(params.stroke)); |
| 629 |
let box_height = height.resolve(params.band_height(), f64::from(params.stroke)); |
| 630 |
|
| 631 |
let across = (box_width - 2.0 * bar) / box_width; |
| 632 |
let up = (box_height - bar) / box_height; |
| 633 |
assert!( |
| 634 |
across > 0.45 && up > 0.45, |
| 635 |
"the space render's counter is {:.0}% of its width and {:.0}% of its height at \ |
| 636 |
stroke {}, which closes up into a blob rather than reading as a box", |
| 637 |
across * 100.0, |
| 638 |
up * 100.0, |
| 639 |
params.stroke |
| 640 |
); |
| 641 |
} |
| 642 |
} |
| 643 |
|
| 644 |
|
| 645 |
|
| 646 |
|
| 647 |
|
| 648 |
|
| 649 |
#[test] |
| 650 |
fn the_return_arrows_head_clears_its_shaft() { |
| 651 |
let Shape::ReturnArrow { |
| 652 |
head_span, stroke, .. |
| 653 |
} = shape("uni23CE") |
| 654 |
else { |
| 655 |
panic!("uni23CE is not a return arrow"); |
| 656 |
}; |
| 657 |
for params in [light(), heavy()] { |
| 658 |
let band = params.band_height(); |
| 659 |
let weight = f64::from(params.stroke) * stroke; |
| 660 |
let head = head_span.resolve(band, f64::from(params.stroke)); |
| 661 |
let proud = (head - weight) / 2.0; |
| 662 |
assert!( |
| 663 |
proud >= weight * 0.5, |
| 664 |
"the head stands {proud:.0} proud of a {weight:.0} shaft" |
| 665 |
); |
| 666 |
} |
| 667 |
} |
| 668 |
|
| 669 |
#[test] |
| 670 |
fn the_carets_are_reflections_of_each_other() { |
| 671 |
let params = light(); |
| 672 |
let up = bounds(&draw(&shape("uni25B2"), ¶ms)); |
| 673 |
let down = bounds(&draw(&shape("uni25BC"), ¶ms)); |
| 674 |
assert!((up.0 - down.0).abs() < 0.01 && (up.2 - down.2).abs() < 0.01); |
| 675 |
let cy = params.band_center_y(); |
| 676 |
assert!( |
| 677 |
((up.1 - cy) + (down.3 - cy)).abs() < 0.01, |
| 678 |
"▲ and ▼ are not mirrored about the band" |
| 679 |
); |
| 680 |
} |
| 681 |
|
| 682 |
#[test] |
| 683 |
fn every_contour_comes_out_wound_for_glyf() { |
| 684 |
let manifest = Manifest::parse(crate::HOUSE_SET).unwrap(); |
| 685 |
let params = light(); |
| 686 |
for glyph in &manifest.glyphs { |
| 687 |
let drawing = draw(&glyph.shape, ¶ms); |
| 688 |
for contour in &drawing.contours { |
| 689 |
let mut points = contour.clone(); |
| 690 |
if signed_area(&points) > 0.0 { |
| 691 |
points.reverse(); |
| 692 |
} |
| 693 |
assert!( |
| 694 |
signed_area(&points) < 0.0, |
| 695 |
"{} has a degenerate contour", |
| 696 |
glyph.name |
| 697 |
); |
| 698 |
} |
| 699 |
} |
| 700 |
} |
| 701 |
|
| 702 |
|
| 703 |
|
| 704 |
|
| 705 |
|
| 706 |
|
| 707 |
|
| 708 |
|
| 709 |
|
| 710 |
|
| 711 |
|
| 712 |
#[test] |
| 713 |
fn the_cross_is_heavier_than_the_bases_own_and_no_heavier() { |
| 714 |
for (params, base_fill) in [(light(), 0.300), (heavy(), 0.569)] { |
| 715 |
let drawing = draw(&shape("uni2718"), ¶ms); |
| 716 |
let ink = signed_area(&drawing.contours[0]).abs(); |
| 717 |
let (x0, y0, x1, y1) = bounds(&drawing); |
| 718 |
let fill = ink / ((x1 - x0) * (y1 - y0)); |
| 719 |
assert!( |
| 720 |
fill > base_fill, |
| 721 |
"the X fills {:.1}%, lighter than the base's own cross at {:.1}%", |
| 722 |
fill * 100.0, |
| 723 |
base_fill * 100.0 |
| 724 |
); |
| 725 |
assert!( |
| 726 |
fill < base_fill + 0.04, |
| 727 |
"the X fills {:.1}%, well past the base's {:.1}%", |
| 728 |
fill * 100.0, |
| 729 |
base_fill * 100.0 |
| 730 |
); |
| 731 |
} |
| 732 |
} |
| 733 |
} |
| 734 |
|