use crate::cell::Side;
use crate::shape::fragment::end::End;
use crate::shape::fragment::segment::Segment;
use crate::{Orientation, Pos, Shape, Stroke, Surface};
#[derive(Debug)]
pub struct Line {
pub at: Pos,
pub len: u16,
pub orientation: Orientation,
pub stroke: Stroke,
}
impl Line {
fn pos_at(&self, i: u16) -> Option<Pos> {
match self.orientation {
Orientation::Horizontal => self
.at
.x
.checked_add_unsigned(i)
.map(|x| Pos { x, y: self.at.y }),
Orientation::Vertical => self
.at
.y
.checked_add_unsigned(i)
.map(|y| Pos { x: self.at.x, y }),
}
}
}
impl Shape for Line {
fn draw(&self, surface: &mut dyn Surface) {
if self.len == 0 {
return;
}
let (forward, backward) = match self.orientation {
Orientation::Horizontal => (Side::Right, Side::Left),
Orientation::Vertical => (Side::Bottom, Side::Top),
};
if let Some(first) = self.pos_at(0) {
End {
at: first,
side: forward,
stroke: self.stroke.clone(),
}
.draw(surface);
}
if self.len >= 3
&& let Some(from) = self.pos_at(1)
{
Segment {
from,
len: self.len - 2,
orientation: self.orientation,
stroke: self.stroke.clone(),
}
.draw(surface);
}
if self.len >= 2
&& let Some(last) = self.pos_at(self.len - 1)
{
End {
at: last,
side: backward,
stroke: self.stroke.clone(),
}
.draw(surface);
}
}
}
#[cfg(test)]
mod tests {
use super::Line;
use crate::shape::counting::CountingSurface;
use crate::{
Arm, Buffer, CellReader, FigureLayer, GlyphCatalog, Orientation, Pos, Shape, Size,
StampMode, Stroke, Window, render,
};
fn light() -> Stroke {
Stroke::from("light")
}
#[test]
fn a_horizontal_line_of_length_5_renders_and_stops_cleanly() {
let origin = Pos { x: 0, y: 0 };
let size = Size {
width: 6,
height: 1,
};
let mut buffer = Buffer::new(Window { origin, size });
Line {
at: origin,
len: 5,
orientation: Orientation::Horizontal,
stroke: light(),
}
.draw(&mut FigureLayer::new(&mut buffer, StampMode::Above));
assert_eq!(
render(&buffer, &GlyphCatalog::light(), buffer.window()),
"───── \n"
);
assert!(buffer.cell(Pos { x: 5, y: 0 }).is_none());
}
#[test]
fn a_vertical_line_of_length_4_renders_and_stops_cleanly() {
let origin = Pos { x: 0, y: 0 };
let size = Size {
width: 1,
height: 5,
};
let mut buffer = Buffer::new(Window { origin, size });
Line {
at: origin,
len: 4,
orientation: Orientation::Vertical,
stroke: light(),
}
.draw(&mut FigureLayer::new(&mut buffer, StampMode::Above));
assert_eq!(
render(&buffer, &GlyphCatalog::light(), buffer.window()),
"│\n│\n│\n│\n \n"
);
assert!(buffer.cell(Pos { x: 0, y: 4 }).is_none());
}
#[test]
fn a_horizontal_lines_first_cell_carries_only_its_forward_arm() {
let origin = Pos { x: 0, y: 0 };
let mut buffer = Buffer::new(Window {
origin,
size: Size {
width: 5,
height: 1,
},
});
Line {
at: origin,
len: 5,
orientation: Orientation::Horizontal,
stroke: light(),
}
.draw(&mut FigureLayer::new(&mut buffer, StampMode::Above));
let Some(crate::Cell::Strokes(cell)) = buffer.cell(origin) else {
panic!("expected a stroke cell at the line's first position");
};
assert_eq!(cell.right, Arm::Set(Stroke::from("light")));
assert_eq!(cell.top, Arm::Unset);
assert_eq!(cell.bottom, Arm::Unset);
assert_eq!(cell.left, Arm::Unset);
}
#[test]
fn a_line_of_length_2_is_two_ends_with_no_run_between_them() {
let origin = Pos { x: 0, y: 0 };
let size = Size {
width: 2,
height: 1,
};
let mut buffer = Buffer::new(Window { origin, size });
Line {
at: origin,
len: 2,
orientation: Orientation::Horizontal,
stroke: light(),
}
.draw(&mut FigureLayer::new(&mut buffer, StampMode::Above));
assert_eq!(
render(&buffer, &GlyphCatalog::light(), buffer.window()),
"──\n"
);
}
#[test]
fn a_line_of_length_1_writes_exactly_one_position() {
let origin = Pos { x: 0, y: 0 };
let mut buffer = Buffer::new(Window {
origin,
size: Size {
width: 1,
height: 1,
},
});
Line {
at: origin,
len: 1,
orientation: Orientation::Horizontal,
stroke: light(),
}
.draw(&mut FigureLayer::new(&mut buffer, StampMode::Above));
assert!(buffer.cell(origin).is_some());
}
#[test]
fn a_line_of_length_0_draws_nothing() {
let origin = Pos { x: 0, y: 0 };
let mut buffer = Buffer::new(Window {
origin,
size: Size {
width: 1,
height: 1,
},
});
Line {
at: origin,
len: 0,
orientation: Orientation::Horizontal,
stroke: light(),
}
.draw(&mut FigureLayer::new(&mut buffer, StampMode::Above));
assert!(buffer.cell(origin).is_none());
}
#[test]
fn no_line_above_writes_any_position_more_than_once() {
for len in [0, 1, 2, 3, 5] {
let mut surface = CountingSurface::default();
Line {
at: Pos { x: 0, y: 0 },
len,
orientation: Orientation::Horizontal,
stroke: light(),
}
.draw(&mut surface);
assert!(
surface.max_writes() <= 1,
"length {len} wrote a position more than once"
);
}
}
#[test]
fn two_lines_sharing_an_end_compose_into_the_corner_they_make() {
let origin = Pos { x: 0, y: 0 };
let size = Size {
width: 1,
height: 1,
};
let horizontal = || Line {
at: origin,
len: 3,
orientation: Orientation::Horizontal,
stroke: light(),
};
let vertical = || Line {
at: origin,
len: 3,
orientation: Orientation::Vertical,
stroke: light(),
};
for mode in [StampMode::Above, StampMode::Below] {
let mut horizontal_first = Buffer::new(Window { origin, size });
horizontal().draw(&mut FigureLayer::new(
&mut horizontal_first,
StampMode::Above,
));
vertical().draw(&mut FigureLayer::new(&mut horizontal_first, mode));
let mut vertical_first = Buffer::new(Window { origin, size });
vertical().draw(&mut FigureLayer::new(&mut vertical_first, StampMode::Above));
horizontal().draw(&mut FigureLayer::new(&mut vertical_first, mode));
for (label, buffer) in [
("horizontal first", &horizontal_first),
("vertical first", &vertical_first),
] {
assert_eq!(
render(buffer, &GlyphCatalog::light(), buffer.window()),
"┌\n",
"{label}, second stamp under {mode:?}"
);
}
}
}
}