Skip to main content

pfx_live/flat/
grid.rs

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, &current, 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}