1use super::{Draw, Fill, IDENTITY, Order, Shadow, Shape, Srgba, Step};
2use crate::text::RichQuad;
3use pfx_text::{Anchor, Face, Representation, RichParagraph, Span, TextEngine};
4use std::collections::BTreeMap;
5use std::f32::consts::{FRAC_PI_2, PI};
6
7#[derive(Clone, Copy, Debug, PartialEq)]
8pub struct Grid {
9 pub origin: [f32; 2],
10 pub cell: [f32; 2],
11 pub columns: u32,
12 pub rows: u32,
13}
14
15impl Grid {
16 pub fn new(origin: [f32; 2], cell: [f32; 2], columns: u32, rows: u32) -> Result<Self, String> {
17 if !(cell[0] > 0.0 && cell[1] > 0.0 && cell[0].is_finite() && cell[1].is_finite()) {
18 return Err("a grid cell must be finite and positive".into());
19 }
20 if !(origin[0].is_finite() && origin[1].is_finite()) {
21 return Err("the grid origin must be finite".into());
22 }
23 if columns == 0 || rows == 0 {
24 return Err("a grid needs at least one column and one row".into());
25 }
26 Ok(Self {
27 origin,
28 cell,
29 columns,
30 rows,
31 })
32 }
33
34 pub fn cover(layout: [f32; 2], cell: [f32; 2]) -> Result<Self, String> {
35 if !(cell[0] > 0.0 && cell[1] > 0.0) {
36 return Err("a grid cell must be finite and positive".into());
37 }
38 let columns = (layout[0] / cell[0]).floor().max(0.0) as u32;
39 let rows = (layout[1] / cell[1]).floor().max(0.0) as u32;
40 let origin = [
41 (layout[0] - columns as f32 * cell[0]) * 0.5,
42 (layout[1] - rows as f32 * cell[1]) * 0.5,
43 ];
44 Self::new(origin, cell, columns, rows)
45 }
46
47 pub fn measure(engine: &mut TextEngine, face: &Face) -> Result<[f32; 2], String> {
48 const SAMPLE: usize = 16;
49 let block = engine
50 .layout_spans(
51 &[Span {
52 text: "0".repeat(SAMPLE),
53 face: face.clone(),
54 color: [1.0; 4],
55 }],
56 None,
57 1.0,
58 Anchor::Start,
59 )
60 .map_err(|error| format!("the grid font could not be measured: {error:?}"))?;
61 let advance = block.width / SAMPLE as f32;
62 if advance.is_nan() || advance <= 0.0 {
63 return Err("the grid font has no advance".into());
64 }
65 Ok([advance, face.line])
66 }
67
68 pub fn cell_at(&self, point: [f32; 2]) -> Option<[u32; 2]> {
69 let c = ((point[0] - self.origin[0]) / self.cell[0]).floor();
70 let r = ((point[1] - self.origin[1]) / self.cell[1]).floor();
71 (c >= 0.0 && r >= 0.0 && c < self.columns as f32 && r < self.rows as f32)
72 .then_some([c as u32, r as u32])
73 }
74
75 pub fn snap(&self, point: [f32; 2]) -> [i64; 2] {
76 [
77 ((point[0] - self.origin[0]) / self.cell[0]).round() as i64,
78 ((point[1] - self.origin[1]) / self.cell[1]).round() as i64,
79 ]
80 }
81
82 pub fn corner(&self, cell: [u32; 2]) -> [f32; 2] {
83 [
84 self.origin[0] + cell[0] as f32 * self.cell[0],
85 self.origin[1] + cell[1] as f32 * self.cell[1],
86 ]
87 }
88
89 fn span(&self, low: f32, high: f32, axis: usize) -> (i64, i64) {
90 let first = ((low - self.origin[axis]) / self.cell[axis] - 0.5).ceil() as i64;
91 let last = ((high - self.origin[axis]) / self.cell[axis] - 0.5).floor() as i64;
92 let count = i64::from(if axis == 0 { self.columns } else { self.rows });
93 (first.max(0), last.min(count - 1))
94 }
95}
96
97#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
98pub enum Weight {
99 #[default]
100 Light,
101 Heavy,
102 Double,
103}
104
105#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
106pub enum Interior {
107 Blank,
108 #[default]
109 Background,
110 Shade,
111}
112
113#[derive(Clone, Copy, Debug, PartialEq)]
114pub struct Rules {
115 pub line: Weight,
116 pub raised: Option<(f32, Weight)>,
117 pub rounded: f32,
118 pub interior: Interior,
119 pub dot: char,
120 pub ink: Option<Srgba>,
121 pub ground: Option<Srgba>,
122 pub thickness: f32,
123}
124
125impl Default for Rules {
126 fn default() -> Self {
127 Self {
128 line: Weight::Light,
129 raised: None,
130 rounded: f32::INFINITY,
131 interior: Interior::Background,
132 dot: '•',
133 ink: None,
134 ground: None,
135 thickness: 0.12,
136 }
137 }
138}
139
140pub const UP: usize = 0;
141pub const RIGHT: usize = 1;
142pub const DOWN: usize = 2;
143pub const LEFT: usize = 3;
144
145pub type Arms = [Option<Weight>; 4];
146
147#[derive(Clone, Copy, Debug, Default, PartialEq)]
148pub enum Glyph {
149 #[default]
150 Empty,
151 Char(char),
152 Lines {
153 arms: Arms,
154 rounded: bool,
155 },
156 Shade(u8),
157}
158
159#[derive(Clone, Copy, Debug, PartialEq)]
160pub struct Cell {
161 pub glyph: Glyph,
162 pub ink: Srgba,
163 pub ground: Option<Srgba>,
164 pub id: u32,
165}
166
167impl Default for Cell {
168 fn default() -> Self {
169 Self {
170 glyph: Glyph::Empty,
171 ink: Srgba::WHITE,
172 ground: None,
173 id: 0,
174 }
175 }
176}
177
178const LIGHT: [char; 16] = [
179 ' ', '╵', '╶', '└', '╷', '│', '┌', '├', '╴', '┘', '─', '┴', '┐', '┤', '┬', '┼',
180];
181const HEAVY: [char; 16] = [
182 ' ', '╹', '╺', '┗', '╻', '┃', '┏', '┣', '╸', '┛', '━', '┻', '┓', '┫', '┳', '╋',
183];
184const DOUBLE: [char; 16] = [
185 ' ', '║', '═', '╚', '║', '║', '╔', '╠', '═', '╝', '═', '╩', '╗', '╣', '╦', '╬',
186];
187const SHADES: [char; 4] = ['░', '▒', '▓', '█'];
188
189type Run = (i64, i64, [u32; 4], u32);
190
191fn mask(arms: &Arms) -> usize {
192 arms.iter()
193 .enumerate()
194 .filter(|(_, arm)| arm.is_some())
195 .map(|(index, _)| 1 << index)
196 .sum()
197}
198
199fn corner_mask(mask: usize) -> bool {
200 matches!(mask, 3 | 6 | 9 | 12)
201}
202
203pub fn box_char(arms: &Arms, rounded: bool) -> char {
204 let mask = mask(arms);
205 let weight = arms.iter().flatten().max().copied();
206 match weight {
207 None => ' ',
208 Some(Weight::Light) if rounded => match mask {
209 6 => '╭',
210 12 => '╮',
211 9 => '╯',
212 3 => '╰',
213 _ => LIGHT[mask],
214 },
215 Some(Weight::Light) => LIGHT[mask],
216 Some(Weight::Heavy) => HEAVY[mask],
217 Some(Weight::Double) => DOUBLE[mask],
218 }
219}
220
221impl Glyph {
222 pub fn char(&self) -> char {
223 match *self {
224 Glyph::Empty => ' ',
225 Glyph::Char(c) => c,
226 Glyph::Lines { arms, rounded } => box_char(&arms, rounded),
227 Glyph::Shade(level) => SHADES[usize::from(level.clamp(1, 4)) - 1],
228 }
229 }
230}
231
232pub struct CharGrid {
233 pub grid: Grid,
234 pub rules: Rules,
235 cells: Vec<Cell>,
236}
237
238#[derive(Clone, Copy)]
239struct Ink {
240 colour: Srgba,
241 id: u32,
242}
243
244fn fill_colour(fill: Fill) -> Option<Srgba> {
245 match fill {
246 Fill::None => None,
247 Fill::Solid(colour) => Some(colour),
248 Fill::Vertical(top, bottom) => Some(if top.0[3] >= bottom.0[3] { top } else { bottom }),
249 Fill::Gradient(gradient) => gradient
250 .stops()
251 .map(|(_, colour)| Srgba(colour))
252 .reduce(|kept, next| if next.0[3] > kept.0[3] { next } else { kept }),
253 Fill::Image(image) => Some(image.tint),
254 }
255}
256
257fn key(colour: Srgba) -> [u32; 4] {
258 colour.0.map(f32::to_bits)
259}
260
261impl CharGrid {
262 pub fn new(grid: Grid, rules: Rules) -> Self {
263 Self {
264 grid,
265 rules,
266 cells: vec![Cell::default(); (grid.columns * grid.rows) as usize],
267 }
268 }
269
270 pub fn clear(&mut self) {
271 self.cells.fill(Cell::default());
272 }
273
274 fn index(&self, c: i64, r: i64) -> Option<usize> {
275 (c >= 0 && r >= 0 && c < i64::from(self.grid.columns) && r < i64::from(self.grid.rows))
276 .then(|| (r * i64::from(self.grid.columns) + c) as usize)
277 }
278
279 pub fn cell(&self, c: u32, r: u32) -> Option<&Cell> {
280 self.index(i64::from(c), i64::from(r))
281 .map(|index| &self.cells[index])
282 }
283
284 pub fn put(&mut self, cell: [u32; 2], glyph: char, ink: Srgba, id: u32) {
285 if let Some(index) = self.index(i64::from(cell[0]), i64::from(cell[1])) {
286 let ink = self.rules.ink.unwrap_or(ink);
287 let target = &mut self.cells[index];
288 target.glyph = if glyph == ' ' {
289 Glyph::Empty
290 } else {
291 Glyph::Char(glyph)
292 };
293 target.ink = ink;
294 target.id = id;
295 }
296 }
297
298 pub fn text(&mut self, at: [f32; 2], text: &str, ink: Srgba, id: u32) {
299 let [start, mut r] = self.grid.snap(at);
300 let mut c = start;
301 for ch in text.chars() {
302 if ch == '\n' {
303 r += 1;
304 c = start;
305 continue;
306 }
307 if c >= 0 && r >= 0 {
308 self.put([c as u32, r as u32], ch, ink, id);
309 }
310 c += 1;
311 }
312 }
313
314 fn arms(&mut self, c: i64, r: i64, arms: Arms, rounded: bool, ink: Ink, merge: bool) {
315 let Some(index) = self.index(c, r) else {
316 return;
317 };
318 let cell = &mut self.cells[index];
319 let (arms, rounded) = match (merge, cell.glyph) {
320 (
321 true,
322 Glyph::Lines {
323 arms: old,
324 rounded: old_rounded,
325 },
326 ) => {
327 let merged: Arms = std::array::from_fn(|i| old[i].max(arms[i]));
328 let same = mask(&old) == mask(&arms) || mask(&old) == 0;
329 (
330 merged,
331 rounded && (same || old_rounded) && corner_mask(mask(&merged)),
332 )
333 }
334 _ => (arms, rounded),
335 };
336 cell.glyph = Glyph::Lines { arms, rounded };
337 cell.ink = ink.colour;
338 cell.id = ink.id;
339 }
340
341 fn line(&mut self, fixed: i64, from: i64, to: i64, horizontal: bool, weight: Weight, ink: Ink) {
342 for at in from..=to {
343 let mut arms: Arms = [None; 4];
344 let (back, forward) = if horizontal {
345 (LEFT, RIGHT)
346 } else {
347 (UP, DOWN)
348 };
349 if at > from || from == to {
350 arms[back] = Some(weight);
351 }
352 if at < to || from == to {
353 arms[forward] = Some(weight);
354 }
355 let (c, r) = if horizontal { (at, fixed) } else { (fixed, at) };
356 self.arms(c, r, arms, false, ink, true);
357 }
358 }
359
360 pub fn shape(&mut self, draw: &Draw) {
361 let opacity = draw.opacity.clamp(0.0, 1.0);
362 if opacity.is_nan() || opacity <= 0.0 {
363 return;
364 }
365 let extent = match draw.shape {
366 Shape::Rect { half, .. } => half,
367 Shape::Circle { radius } => [radius; 2],
368 Shape::Ring { radius, width } | Shape::Arc { radius, width, .. } => {
369 [radius + width * 0.5; 2]
370 }
371 Shape::Icon(_) | Shape::Text(_) => return,
372 };
373 let stroke = draw
374 .stroke
375 .filter(|stroke| stroke.width > 0.0 && stroke.colour.0[3] > 0.0)
376 .map(|stroke| stroke.colour);
377 let fill = fill_colour(draw.fill).filter(|colour| colour.0[3] > 0.0);
378 let Some(colour) = stroke.or(fill) else {
379 return;
380 };
381 let filled = fill.is_some_and(|fill| fill.0[3] * opacity >= 0.5)
382 && !matches!(draw.shape, Shape::Ring { .. } | Shape::Arc { .. });
383 let m = draw.transform;
384 let mut bounds = [f32::MAX, f32::MAX, f32::MIN, f32::MIN];
385 for corner in [[-1.0, -1.0], [1.0, -1.0], [-1.0, 1.0], [1.0, 1.0]] {
386 let local = [corner[0] * extent[0], corner[1] * extent[1]];
387 let p = [
388 m[0][0] * local[0] + m[1][0] * local[1] + m[3][0],
389 m[0][1] * local[0] + m[1][1] * local[1] + m[3][1],
390 ];
391 bounds = [
392 bounds[0].min(p[0]),
393 bounds[1].min(p[1]),
394 bounds[2].max(p[0]),
395 bounds[3].max(p[1]),
396 ];
397 }
398 if !bounds.iter().all(|value| value.is_finite()) {
399 return;
400 }
401 let ink = Ink {
402 colour: self.rules.ink.unwrap_or(colour),
403 id: draw.id,
404 };
405 let weight = match self.rules.raised {
406 Some((elevation, weight)) if draw.elevation >= elevation => weight,
407 _ => self.rules.line,
408 };
409 let (c0, c1) = self.grid.span(bounds[0], bounds[2], 0);
410 let (r0, r1) = self.grid.span(bounds[1], bounds[3], 1);
411 let centre = [(bounds[0] + bounds[2]) * 0.5, (bounds[1] + bounds[3]) * 0.5];
412 let home = [
413 ((centre[0] - self.grid.origin[0]) / self.grid.cell[0]).floor() as i64,
414 ((centre[1] - self.grid.origin[1]) / self.grid.cell[1]).floor() as i64,
415 ];
416 let columns = c1 - c0 + 1;
417 let rows = r1 - r0 + 1;
418 if columns >= 2 && rows >= 2 {
419 let scale = (m[0][0] * m[1][1] - m[1][0] * m[0][1]).abs().sqrt();
420 let rounded = match draw.shape {
421 Shape::Rect { radii, .. } => {
422 radii.iter().copied().fold(0.0, f32::max) * scale >= self.rules.rounded
423 }
424 _ => true,
425 };
426 let ground = fill.map(|fill| self.rules.ground.unwrap_or(fill));
427 let shade = fill.map_or(0, |fill| {
428 (fill.0[3] * opacity * 4.0).ceil().clamp(1.0, 4.0) as u8
429 });
430 let tone = Ink {
431 colour: self.rules.ink.or(fill).unwrap_or(ink.colour),
432 id: ink.id,
433 };
434 self.boxed(
435 [c0, c1, r0, r1],
436 weight,
437 rounded,
438 (filled, ground, shade, tone),
439 ink,
440 );
441 } else if columns >= 2 {
442 self.line(home[1], c0, c1, true, weight, ink);
443 } else if rows >= 2 {
444 self.line(home[0], r0, r1, false, weight, ink);
445 } else if let Some(index) = self.index(home[0], home[1]) {
446 let cell = &mut self.cells[index];
447 cell.glyph = Glyph::Char(self.rules.dot);
448 cell.ink = ink.colour;
449 cell.id = ink.id;
450 }
451 }
452
453 fn boxed(
454 &mut self,
455 [c0, c1, r0, r1]: [i64; 4],
456 weight: Weight,
457 rounded: bool,
458 (filled, ground, level, tone): (bool, Option<Srgba>, u8, Ink),
459 ink: Ink,
460 ) {
461 let interior = self.rules.interior;
462 for r in r0..=r1 {
463 for c in c0..=c1 {
464 let top = r == r0;
465 let bottom = r == r1;
466 let left = c == c0;
467 let right = c == c1;
468 let edge = top || bottom || left || right;
469 if filled && let Some(index) = self.index(c, r) {
470 let cell = &mut self.cells[index];
471 cell.ground = match interior {
472 Interior::Background => ground,
473 Interior::Blank | Interior::Shade => None,
474 };
475 cell.glyph = if interior == Interior::Shade && !edge {
476 Glyph::Shade(level)
477 } else {
478 Glyph::Empty
479 };
480 cell.ink = tone.colour;
481 cell.id = tone.id;
482 }
483 if !edge {
484 continue;
485 }
486 let mut arms: Arms = [None; 4];
487 if (left || right) && !top {
488 arms[UP] = Some(weight);
489 }
490 if (left || right) && !bottom {
491 arms[DOWN] = Some(weight);
492 }
493 if (top || bottom) && !left {
494 arms[LEFT] = Some(weight);
495 }
496 if (top || bottom) && !right {
497 arms[RIGHT] = Some(weight);
498 }
499 let corner = (top || bottom) && (left || right);
500 self.arms(c, r, arms, rounded && corner, ink, !filled);
501 }
502 }
503 }
504
505 pub fn map(
506 &mut self,
507 draws: &[Draw],
508 groups: usize,
509 labels: &[(&str, Srgba)],
510 ) -> Result<(), String> {
511 let mut order = Order::default();
512 order.build(draws, groups)?;
513 for step in &order.steps {
514 let Step::Draw(index) = *step else {
515 continue;
516 };
517 let draw = &draws[index];
518 match draw.shape {
519 Shape::Text(label) => {
520 if let Some(&(text, colour)) = labels.get(label)
521 && draw.opacity > 0.0
522 {
523 self.text(
524 [draw.transform[3][0], draw.transform[3][1]],
525 text,
526 colour,
527 draw.id,
528 );
529 }
530 }
531 Shape::Icon(_) => {}
532 _ => self.shape(draw),
533 }
534 }
535 Ok(())
536 }
537
538 pub fn char_at(&self, c: u32, r: u32) -> char {
539 self.cell(c, r).map_or(' ', |cell| cell.glyph.char())
540 }
541
542 pub fn lines(&self) -> Vec<String> {
543 (0..self.grid.rows)
544 .map(|r| {
545 (0..self.grid.columns)
546 .map(|c| self.char_at(c, r))
547 .collect::<String>()
548 .trim_end()
549 .to_string()
550 })
551 .collect()
552 }
553
554 pub fn spans(&self, face: &Face) -> Vec<Span> {
555 let mut spans: Vec<Span> = Vec::new();
556 for r in 0..self.grid.rows {
557 let row = &self.cells[(r * self.grid.columns) as usize..][..self.grid.columns as usize];
558 let end = row
559 .iter()
560 .rposition(|cell| matches!(cell.glyph, Glyph::Char(_)))
561 .map_or(0, |last| last + 1);
562 let mut current: Option<Span> = None;
563 for cell in &row[..end] {
564 let (ch, colour) = match cell.glyph {
565 Glyph::Char(ch) => (ch, cell.ink.0),
566 _ => (' ', current.as_ref().map_or(cell.ink.0, |span| span.color)),
567 };
568 match current.as_mut() {
569 Some(span) if span.color == colour => span.text.push(ch),
570 _ => {
571 spans.extend(current.take());
572 current = Some(Span {
573 text: ch.to_string(),
574 face: face.clone(),
575 color: colour,
576 });
577 }
578 }
579 }
580 let mut last = current.unwrap_or_else(|| Span {
581 text: String::new(),
582 face: face.clone(),
583 color: [1.0; 4],
584 });
585 last.text.push('\n');
586 spans.push(last);
587 }
588 spans
589 }
590
591 pub fn snap(&self, rich: &RichParagraph, face: &Face) -> Vec<RichQuad> {
592 let [cw, ch] = self.grid.cell;
593 rich.quads
594 .iter()
595 .map(|quad| {
596 let column = (quad.pen[0] / cw).round();
597 let row = quad.line_index as f32;
598 let dx = self.grid.origin[0] + column * cw - quad.pen[0];
599 let dy = self.grid.origin[1] + (row + 0.5) * (ch - face.line);
600 let mut placed = RichQuad::from(quad);
601 placed.rect[0] += dx;
602 placed.rect[1] += dy;
603 placed
604 })
605 .collect()
606 }
607
608 pub fn render(
609 &self,
610 engine: &mut TextEngine,
611 face: &Face,
612 representation: Representation,
613 ) -> Result<(RichParagraph, Vec<RichQuad>), String> {
614 let rich = engine
615 .render_spans(
616 &self.spans(face),
617 None,
618 1.0,
619 Anchor::Start,
620 representation,
621 [0.0, 0.0],
622 1.0,
623 [-f32::MAX, -f32::MAX, f32::MAX, f32::MAX],
624 [0.0; 3],
625 )
626 .map_err(|error| format!("the grid text could not be laid out: {error:?}"))?;
627 let quads = self.snap(&rich, face);
628 Ok((rich, quads))
629 }
630
631 pub fn has_text(&self) -> bool {
632 self.cells
633 .iter()
634 .any(|cell| matches!(cell.glyph, Glyph::Char(_)))
635 }
636
637 pub fn draws(&self, text: Option<usize>, out: &mut Vec<Draw>) {
638 self.ground(out);
639 self.shades(out);
640 self.strokes(out);
641 if let Some(text) = text
642 && self.has_text()
643 {
644 out.push(
645 Draw::new(Shape::Text(text))
646 .transform(IDENTITY)
647 .shadow(Shadow::None),
648 );
649 }
650 }
651
652 fn rect(&self, columns: [i64; 2], rows: [i64; 2]) -> ([f32; 2], [f32; 2]) {
653 let [cw, ch] = self.grid.cell;
654 let x0 = self.grid.origin[0] + columns[0] as f32 * cw;
655 let x1 = self.grid.origin[0] + (columns[1] + 1) as f32 * cw;
656 let y0 = self.grid.origin[1] + rows[0] as f32 * ch;
657 let y1 = self.grid.origin[1] + (rows[1] + 1) as f32 * ch;
658 ([(x0 + x1) * 0.5, (y0 + y1) * 0.5], [x1 - x0, y1 - y0])
659 }
660
661 fn close(
662 &self,
663 open: &mut BTreeMap<Run, (i64, Srgba)>,
664 keep: &[Run],
665 last_row: i64,
666 out: &mut Vec<Draw>,
667 ) {
668 let done = open
669 .keys()
670 .filter(|run| !keep.contains(run))
671 .copied()
672 .collect::<Vec<_>>();
673 for run in done {
674 if let Some((first_row, colour)) = open.remove(&run) {
675 let (centre, size) = self.rect([run.0, run.1], [first_row, last_row]);
676 out.push(
677 Draw::rect(centre, size, 0.0)
678 .fill(colour)
679 .shadow(Shadow::None)
680 .id(run.3),
681 );
682 }
683 }
684 }
685
686 fn blocks(&self, of: impl Fn(&Cell) -> Option<(Srgba, u32)>, out: &mut Vec<Draw>) {
687 let mut open: BTreeMap<Run, (i64, Srgba)> = BTreeMap::new();
688 let mut runs: Vec<(Run, Srgba)> = Vec::new();
689 for r in 0..i64::from(self.grid.rows) {
690 runs.clear();
691 for c in 0..i64::from(self.grid.columns) {
692 let Some((colour, id)) = self.index(c, r).and_then(|index| of(&self.cells[index]))
693 else {
694 continue;
695 };
696 match runs.last_mut() {
697 Some((run, _)) if run.1 == c - 1 && run.2 == key(colour) && run.3 == id => {
698 run.1 = c;
699 }
700 _ => runs.push(((c, c, key(colour), id), colour)),
701 }
702 }
703 let current = runs.iter().map(|(run, _)| *run).collect::<Vec<_>>();
704 self.close(&mut open, ¤t, r - 1, out);
705 for &(run, colour) in &runs {
706 open.entry(run).or_insert((r, colour));
707 }
708 }
709 self.close(&mut open, &[], i64::from(self.grid.rows) - 1, out);
710 }
711
712 fn ground(&self, out: &mut Vec<Draw>) {
713 self.blocks(|cell| cell.ground.map(|ground| (ground, cell.id)), out);
714 }
715
716 fn shades(&self, out: &mut Vec<Draw>) {
717 self.blocks(
718 |cell| match cell.glyph {
719 Glyph::Shade(level) => Some((
720 cell.ink
721 .alpha(cell.ink.0[3] * f32::from(level.clamp(1, 4)) / 4.0),
722 cell.id,
723 )),
724 _ => None,
725 },
726 out,
727 );
728 }
729
730 fn thickness(&self) -> f32 {
731 (self.rules.thickness * self.grid.cell[0]).max(0.0)
732 }
733
734 fn arms_at(&self, c: i64, r: i64) -> Option<(Arms, bool)> {
735 let index = self.index(c, r)?;
736 match self.cells[index].glyph {
737 Glyph::Lines { arms, rounded } => Some((arms, rounded)),
738 _ => None,
739 }
740 }
741
742 fn radius(&self) -> f32 {
743 self.grid.cell[0].min(self.grid.cell[1]) * 0.5
744 }
745
746 fn end(
747 &self,
748 c: i64,
749 r: i64,
750 horizontal: bool,
751 start: bool,
752 offset: f32,
753 weight: Weight,
754 ) -> f32 {
755 let Some((arms, rounded)) = self.arms_at(c, r) else {
756 return 0.0;
757 };
758 let t = self.thickness();
759 let g = t;
760 let sign = if start { -1.0 } else { 1.0 };
761 let (low, high) = if horizontal {
762 (UP, DOWN)
763 } else {
764 (LEFT, RIGHT)
765 };
766 let across = [arms[low], arms[high]];
767 if rounded && weight == Weight::Light && corner_mask(mask(&arms)) {
768 return -sign * self.radius();
769 }
770 if weight == Weight::Double {
771 return match (across[0].is_some(), across[1].is_some()) {
772 (false, true) => {
773 if start {
774 offset - t * 0.5
775 } else {
776 -offset + t * 0.5
777 }
778 }
779 (true, false) => {
780 if start {
781 -offset - t * 0.5
782 } else {
783 offset + t * 0.5
784 }
785 }
786 _ => sign * (g + t * 0.5),
787 };
788 }
789 let half = across
790 .iter()
791 .flatten()
792 .map(|weight| match weight {
793 Weight::Light => t * 0.5,
794 Weight::Heavy => t,
795 Weight::Double => g + t * 0.5,
796 })
797 .fold(0.0f32, f32::max);
798 sign * half
799 }
800
801 fn strokes(&self, out: &mut Vec<Draw>) {
802 let t = self.thickness();
803 if t.is_nan() || t <= 0.0 {
804 return;
805 }
806 let [cw, ch] = self.grid.cell;
807 let [ox, oy] = self.grid.origin;
808 let columns = i64::from(self.grid.columns);
809 let rows = i64::from(self.grid.rows);
810 for horizontal in [true, false] {
811 let (outer, inner) = if horizontal {
812 (rows, columns)
813 } else {
814 (columns, rows)
815 };
816 let (back, forward) = if horizontal {
817 (LEFT, RIGHT)
818 } else {
819 (UP, DOWN)
820 };
821 for fixed in 0..outer {
822 let mut runs: Vec<(i64, i64, Weight, [u32; 4], u32, Srgba)> = Vec::new();
823 for at in 0..inner {
824 let (c, r) = if horizontal { (at, fixed) } else { (fixed, at) };
825 let Some((arms, _)) = self.arms_at(c, r) else {
826 continue;
827 };
828 let cell = &self.cells[self.index(c, r).unwrap()];
829 for (side, half) in [(back, 0), (forward, 1)] {
830 let Some(weight) = arms[side] else {
831 continue;
832 };
833 let from = 2 * at + half;
834 let run_key = (weight, key(cell.ink));
835 match runs.last_mut() {
836 Some(run) if run.1 == from && (run.2, run.3) == run_key => {
837 run.1 = from + 1;
838 }
839 _ => runs.push((from, from + 1, weight, run_key.1, cell.id, cell.ink)),
840 }
841 }
842 }
843 for (from, to, weight, _, id, ink) in runs {
844 let lines: Vec<(f32, f32)> = match weight {
845 Weight::Light => vec![(0.0, t)],
846 Weight::Heavy => vec![(0.0, 2.0 * t)],
847 Weight::Double => vec![(-t, t), (t, t)],
848 };
849 for (offset, width) in lines {
850 let along = |lattice: i64, start: bool| {
851 let cell = lattice.div_euclid(2);
852 let base = if horizontal {
853 ox + lattice as f32 * cw * 0.5
854 } else {
855 oy + lattice as f32 * ch * 0.5
856 };
857 if lattice % 2 == 1 {
858 let (c, r) = if horizontal {
859 (cell, fixed)
860 } else {
861 (fixed, cell)
862 };
863 base + self.end(c, r, horizontal, start, offset, weight)
864 } else {
865 base
866 }
867 };
868 let a = along(from, true);
869 let b = along(to, false);
870 if b <= a || b.is_nan() || a.is_nan() {
871 continue;
872 }
873 let across = if horizontal {
874 oy + (fixed as f32 + 0.5) * ch + offset
875 } else {
876 ox + (fixed as f32 + 0.5) * cw + offset
877 };
878 let (centre, size) = if horizontal {
879 ([(a + b) * 0.5, across], [b - a, width])
880 } else {
881 ([across, (a + b) * 0.5], [width, b - a])
882 };
883 out.push(
884 Draw::rect(centre, size, 0.0)
885 .fill(ink)
886 .shadow(Shadow::None)
887 .id(id),
888 );
889 }
890 }
891 }
892 }
893 let radius = self.radius();
894 for r in 0..rows {
895 for c in 0..columns {
896 let Some((arms, true)) = self.arms_at(c, r) else {
897 continue;
898 };
899 let mask = mask(&arms);
900 if !corner_mask(mask) || arms.iter().flatten().any(|w| *w != Weight::Light) {
901 continue;
902 }
903 let cx = ox + (c as f32 + 0.5) * cw;
904 let cy = oy + (r as f32 + 0.5) * ch;
905 let (centre, start) = match mask {
906 6 => ([cx + radius, cy + radius], PI),
907 12 => ([cx - radius, cy + radius], -FRAC_PI_2),
908 9 => ([cx - radius, cy - radius], 0.0),
909 _ => ([cx + radius, cy - radius], FRAC_PI_2),
910 };
911 let cell = &self.cells[self.index(c, r).unwrap()];
912 out.push(
913 Draw::new(Shape::Arc {
914 radius,
915 width: t,
916 start,
917 sweep: FRAC_PI_2,
918 })
919 .at(centre)
920 .fill(cell.ink)
921 .shadow(Shadow::None)
922 .id(cell.id),
923 );
924 }
925 }
926 }
927}