Skip to main content

urushi/view/canvas/
line_network.rs

1use std::collections::HashMap;
2use std::ops::RangeInclusive;
3
4use crate::TextStyle;
5
6use super::Position;
7use super::cell::validate_cell_glyph;
8use super::cell_primitives::{clip_line, line_points};
9use crate::view::geometry::Size;
10
11#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
12pub(super) struct LineConnections(u8);
13
14impl LineConnections {
15    const UP: Self = Self(1);
16    const RIGHT: Self = Self(2);
17    const DOWN: Self = Self(4);
18    const LEFT: Self = Self(8);
19
20    pub(super) const fn union(self, other: Self) -> Self {
21        Self(self.0 | other.0)
22    }
23}
24
25/// Incidence that continues beyond a line segment's inclusive range.
26///
27/// `START` and `END` refer to the ascending endpoints of the supplied range.
28/// For a horizontal segment they continue left and right respectively; for a
29/// vertical segment they continue up and down. Continuation affects the glyph
30/// selected at the endpoint without drawing or occupying the outside cell.
31#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
32pub struct LineContinuations(u8);
33
34impl LineContinuations {
35    /// No endpoint continues beyond the inclusive range.
36    pub const NONE: Self = Self(0);
37    /// Only the start endpoint continues beyond the inclusive range.
38    pub const START: Self = Self(1);
39    /// Only the end endpoint continues beyond the inclusive range.
40    pub const END: Self = Self(2);
41    /// Both endpoints continue beyond the inclusive range.
42    pub const BOTH: Self = Self(Self::START.0 | Self::END.0);
43
44    const fn includes_start(self) -> bool {
45        self.0 & Self::START.0 != 0
46    }
47
48    const fn includes_end(self) -> bool {
49        self.0 & Self::END.0 != 0
50    }
51}
52
53/// Glyphs selected by the incident directions of one line-network cell.
54///
55/// Every field must be exactly one printable, one-cell-wide character. Public
56/// fields allow application-specific repertoires as well as the built-in ones.
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub struct LineGlyphs {
59    /// A point with no incident segment.
60    pub isolated: char,
61    /// A one-ended segment continuing upward.
62    pub end_up: char,
63    /// A one-ended segment continuing rightward.
64    pub end_right: char,
65    /// A one-ended segment continuing downward.
66    pub end_down: char,
67    /// A one-ended segment continuing leftward.
68    pub end_left: char,
69    /// A segment continuing upward and downward.
70    pub vertical: char,
71    /// A segment continuing leftward and rightward.
72    pub horizontal: char,
73    /// A corner continuing downward and rightward.
74    pub corner_down_right: char,
75    /// A corner continuing downward and leftward.
76    pub corner_down_left: char,
77    /// A corner continuing upward and rightward.
78    pub corner_up_right: char,
79    /// A corner continuing upward and leftward.
80    pub corner_up_left: char,
81    /// A tee continuing upward, downward, and rightward.
82    pub tee_right: char,
83    /// A tee continuing leftward, rightward, and downward.
84    pub tee_down: char,
85    /// A tee continuing upward, downward, and leftward.
86    pub tee_left: char,
87    /// A tee continuing leftward, rightward, and upward.
88    pub tee_up: char,
89    /// A four-way crossing.
90    pub cross: char,
91}
92
93impl LineGlyphs {
94    /// Standard square box-drawing characters.
95    pub const NORMAL: Self = Self {
96        isolated: '•',
97        end_up: '│',
98        end_right: '─',
99        end_down: '│',
100        end_left: '─',
101        vertical: '│',
102        horizontal: '─',
103        corner_down_right: '┌',
104        corner_down_left: '┐',
105        corner_up_right: '└',
106        corner_up_left: '┘',
107        tee_right: '├',
108        tee_down: '┬',
109        tee_left: '┤',
110        tee_up: '┴',
111        cross: '┼',
112    };
113
114    /// Standard box drawing with rounded corners.
115    pub const ROUNDED: Self = Self {
116        corner_down_right: '╭',
117        corner_down_left: '╮',
118        corner_up_right: '╰',
119        corner_up_left: '╯',
120        ..Self::NORMAL
121    };
122
123    /// ASCII-only line drawing.
124    pub const ASCII: Self = Self {
125        isolated: '*',
126        end_up: '|',
127        end_right: '-',
128        end_down: '|',
129        end_left: '-',
130        vertical: '|',
131        horizontal: '-',
132        corner_down_right: '+',
133        corner_down_left: '+',
134        corner_up_right: '+',
135        corner_up_left: '+',
136        tee_right: '+',
137        tee_down: '+',
138        tee_left: '+',
139        tee_up: '+',
140        cross: '+',
141    };
142
143    /// Heavy box-drawing characters.
144    pub const THICK: Self = Self {
145        isolated: '•',
146        end_up: '┃',
147        end_right: '━',
148        end_down: '┃',
149        end_left: '━',
150        vertical: '┃',
151        horizontal: '━',
152        corner_down_right: '┏',
153        corner_down_left: '┓',
154        corner_up_right: '┗',
155        corner_up_left: '┛',
156        tee_right: '┣',
157        tee_down: '┳',
158        tee_left: '┫',
159        tee_up: '┻',
160        cross: '╋',
161    };
162
163    /// Double box-drawing characters.
164    pub const DOUBLE: Self = Self {
165        isolated: '•',
166        end_up: '║',
167        end_right: '═',
168        end_down: '║',
169        end_left: '═',
170        vertical: '║',
171        horizontal: '═',
172        corner_down_right: '╔',
173        corner_down_left: '╗',
174        corner_up_right: '╚',
175        corner_up_left: '╝',
176        tee_right: '╠',
177        tee_down: '╦',
178        tee_left: '╣',
179        tee_up: '╩',
180        cross: '╬',
181    };
182
183    /// Invisible line drawing that still occupies its cells.
184    pub const HIDDEN: Self = Self {
185        isolated: ' ',
186        end_up: ' ',
187        end_right: ' ',
188        end_down: ' ',
189        end_left: ' ',
190        vertical: ' ',
191        horizontal: ' ',
192        corner_down_right: ' ',
193        corner_down_left: ' ',
194        corner_up_right: ' ',
195        corner_up_left: ' ',
196        tee_right: ' ',
197        tee_down: ' ',
198        tee_left: ' ',
199        tee_up: ' ',
200        cross: ' ',
201    };
202
203    pub(super) fn glyph(self, connections: LineConnections) -> char {
204        match connections.0 {
205            0 => self.isolated,
206            1 => self.end_up,
207            2 => self.end_right,
208            4 => self.end_down,
209            8 => self.end_left,
210            5 => self.vertical,
211            10 => self.horizontal,
212            6 => self.corner_down_right,
213            12 => self.corner_down_left,
214            3 => self.corner_up_right,
215            9 => self.corner_up_left,
216            7 => self.tee_right,
217            14 => self.tee_down,
218            13 => self.tee_left,
219            11 => self.tee_up,
220            _ => self.cross,
221        }
222    }
223
224    fn validate(self) {
225        for glyph in [
226            self.isolated,
227            self.end_up,
228            self.end_right,
229            self.end_down,
230            self.end_left,
231            self.vertical,
232            self.horizontal,
233            self.corner_down_right,
234            self.corner_down_left,
235            self.corner_up_right,
236            self.corner_up_left,
237            self.tee_right,
238            self.tee_down,
239            self.tee_left,
240            self.tee_up,
241            self.cross,
242        ] {
243            let mut encoded = [0; 4];
244            let glyph = glyph.encode_utf8(&mut encoded);
245            validate_cell_glyph(glyph);
246        }
247    }
248}
249
250#[derive(Debug, Clone, Copy, PartialEq, Eq)]
251struct Segment {
252    from: Position,
253    to: Position,
254    axis: SegmentAxis,
255    continuations: LineContinuations,
256}
257
258#[derive(Debug, Clone, Copy, PartialEq, Eq)]
259enum SegmentAxis {
260    Horizontal,
261    Vertical,
262}
263
264/// One connected drawing whose horizontal and vertical segments form junctions.
265///
266/// Unlike [`crate::CanvasContext::line`], a line network does not accept
267/// diagonal segments or an arbitrary marker. Intersections are rendered as
268/// corners, tees, and crossings selected from its [`LineGlyphs`]. Only
269/// segments recorded in the same value contribute to those junctions;
270/// separate Canvas commands combine through their recorded composition.
271/// [`LineContinuations`] can add outward incidence at either endpoint without
272/// adding a cell outside the segment's inclusive range.
273///
274/// ```
275/// use urushi::{
276///     CanvasContext, LineContinuations, LineGlyphs, LineNetwork, TextStyle,
277/// };
278///
279/// fn draw(context: &mut CanvasContext) {
280///     let mut network = LineNetwork::new(LineGlyphs::NORMAL, TextStyle::new());
281///     network
282///         .horizontal(1, 0..=4)
283///         .vertical_with(2, 0..=2, LineContinuations::START);
284///     context.line_network(network);
285/// }
286/// ```
287#[derive(Debug, Clone, PartialEq, Eq)]
288pub struct LineNetwork {
289    segments: Vec<Segment>,
290    glyphs: LineGlyphs,
291    style: TextStyle,
292}
293
294impl LineNetwork {
295    /// Creates an empty line network with one glyph repertoire and style.
296    ///
297    /// # Panics
298    ///
299    /// Panics if a repertoire character is not printable or does not occupy
300    /// exactly one terminal cell.
301    pub fn new(glyphs: LineGlyphs, style: TextStyle) -> Self {
302        glyphs.validate();
303        Self {
304            segments: Vec::new(),
305            glyphs,
306            style,
307        }
308    }
309
310    /// Adds a horizontal segment at `y`, including both endpoints.
311    ///
312    /// An empty range adds nothing.
313    pub fn horizontal(&mut self, y: i64, columns: RangeInclusive<i64>) -> &mut Self {
314        self.horizontal_with(y, columns, LineContinuations::NONE)
315    }
316
317    /// Adds a horizontal segment with endpoint continuation at `y`.
318    ///
319    /// `START` continues left from the first column and `END` continues right
320    /// from the last column. Continuation changes endpoint incidence without
321    /// drawing outside the inclusive range. An empty range adds nothing.
322    pub fn horizontal_with(
323        &mut self,
324        y: i64,
325        columns: RangeInclusive<i64>,
326        continuations: LineContinuations,
327    ) -> &mut Self {
328        if columns.is_empty() {
329            return self;
330        }
331        self.segments.push(Segment {
332            from: Position::new(*columns.start(), y),
333            to: Position::new(*columns.end(), y),
334            axis: SegmentAxis::Horizontal,
335            continuations,
336        });
337        self
338    }
339
340    /// Adds a vertical segment at `x`, including both endpoints.
341    ///
342    /// An empty range adds nothing.
343    pub fn vertical(&mut self, x: i64, rows: RangeInclusive<i64>) -> &mut Self {
344        self.vertical_with(x, rows, LineContinuations::NONE)
345    }
346
347    /// Adds a vertical segment with endpoint continuation at `x`.
348    ///
349    /// `START` continues up from the first row and `END` continues down from
350    /// the last row. Continuation changes endpoint incidence without drawing
351    /// outside the inclusive range. An empty range adds nothing.
352    pub fn vertical_with(
353        &mut self,
354        x: i64,
355        rows: RangeInclusive<i64>,
356        continuations: LineContinuations,
357    ) -> &mut Self {
358        if rows.is_empty() {
359            return self;
360        }
361        self.segments.push(Segment {
362            from: Position::new(x, *rows.start()),
363            to: Position::new(x, *rows.end()),
364            axis: SegmentAxis::Vertical,
365            continuations,
366        });
367        self
368    }
369
370    pub(super) fn rasterize(&self, size: Size) -> Vec<NetworkCell> {
371        let mut cells = Vec::<NetworkCell>::new();
372        let mut indexes = HashMap::<Position, usize>::new();
373        for segment in &self.segments {
374            if let Some((from, to)) = clip_line(segment.from, segment.to, size) {
375                for position in line_points(from, to) {
376                    let connections = segment_connections(position, *segment);
377                    if let Some(index) = indexes.get(&position).copied() {
378                        cells[index].connections = cells[index].connections.union(connections);
379                    } else {
380                        indexes.insert(position, cells.len());
381                        cells.push(NetworkCell {
382                            position,
383                            connections,
384                        });
385                    }
386                }
387            }
388        }
389        cells
390    }
391
392    pub(super) const fn glyphs(&self) -> LineGlyphs {
393        self.glyphs
394    }
395
396    pub(super) const fn style(&self) -> &TextStyle {
397        &self.style
398    }
399}
400
401pub(super) struct NetworkCell {
402    pub(super) position: Position,
403    pub(super) connections: LineConnections,
404}
405
406fn segment_connections(position: Position, segment: Segment) -> LineConnections {
407    let (toward_start, toward_end) = match segment.axis {
408        SegmentAxis::Horizontal => (LineConnections::LEFT, LineConnections::RIGHT),
409        SegmentAxis::Vertical => (LineConnections::UP, LineConnections::DOWN),
410    };
411    let mut connections = LineConnections::default();
412    if position != segment.from || segment.continuations.includes_start() {
413        connections = connections.union(toward_start);
414    }
415    if position != segment.to || segment.continuations.includes_end() {
416        connections = connections.union(toward_end);
417    }
418    connections
419}