use monospace_core::{Direction, Orientation, Pos, Size};
use crate::{Delta, ShapeId};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Anchor {
Top,
Right,
Bottom,
Left,
}
impl Anchor {
#[must_use]
pub const fn leaving(self) -> Direction {
match self {
Self::Top => Direction::Up,
Self::Right => Direction::Right,
Self::Bottom => Direction::Down,
Self::Left => Direction::Left,
}
}
pub(crate) fn outward(self, out: i16) -> Delta {
match self {
Self::Top => Delta {
dx: 0,
dy: out.saturating_neg(),
},
Self::Right => Delta { dx: out, dy: 0 },
Self::Bottom => Delta { dx: 0, dy: out },
Self::Left => Delta {
dx: out.saturating_neg(),
dy: 0,
},
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Reference {
pub id: ShapeId,
pub anchor: Anchor,
pub offset: Delta,
pub out: i16,
}
impl Reference {
#[must_use]
pub fn new(id: ShapeId, anchor: Anchor, offset: Delta, out: i16) -> Self {
Self {
id,
anchor,
offset,
out,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Position {
Absolute(Pos),
Reference(Reference),
}
impl From<Pos> for Position {
fn from(at: Pos) -> Self {
Self::Absolute(at)
}
}
impl Position {
#[must_use]
pub(crate) fn displaced_by(self, by: Delta) -> Self {
match self {
Self::Absolute(at) => Self::Absolute(by.apply(at)),
Self::Reference(reference) => Self::Reference(Reference {
id: reference.id,
anchor: reference.anchor,
offset: reference.offset.grow(by),
out: reference.out,
}),
}
}
}
pub(crate) fn side_centre(at: Pos, size: Size, anchor: Anchor) -> Pos {
let width = size.width.saturating_sub(1);
let height = size.height.saturating_sub(1);
match anchor {
Anchor::Top => Pos {
x: at.x.saturating_add_unsigned(width / 2),
y: at.y,
},
Anchor::Right => Pos {
x: at.x.saturating_add_unsigned(width),
y: at.y.saturating_add_unsigned(height / 2),
},
Anchor::Bottom => Pos {
x: at.x.saturating_add_unsigned(width / 2),
y: at.y.saturating_add_unsigned(height),
},
Anchor::Left => Pos {
x: at.x,
y: at.y.saturating_add_unsigned(height / 2),
},
}
}
pub(crate) fn flat_size(len: u16, orientation: Orientation) -> Size {
match orientation {
Orientation::Horizontal => Size {
width: len,
height: 1,
},
Orientation::Vertical => Size {
width: 1,
height: len,
},
}
}
#[cfg(test)]
mod tests {
use std::num::NonZeroU16;
use monospace_core::{
Buffer, Direction, GlyphCatalog, Orientation, Pos, Size, Stroke, Terminal, Window, render,
};
use super::Anchor;
use crate::{Delta, Diagram, Endpoint, EndpointAt, Position, Reference, Shape, ShapeId};
fn identity(ordinal: u16) -> ShapeId {
ShapeId::new(NonZeroU16::new(ordinal).expect("no test names zero"))
}
fn light() -> Stroke {
Stroke::from("light")
}
fn the_box_at_the_origin() -> Shape {
Shape::Box {
at: Pos { x: 0, y: 0 },
size: Size {
width: 4,
height: 3,
},
stroke: light(),
fill: None,
text: None,
}
}
fn resolve(reference: Reference, diagram: &Diagram) -> Option<Pos> {
Endpoint::reference(reference, Terminal::Arm).resolve(diagram)
}
#[test]
fn each_anchor_leaves_toward_its_outward_normal() {
assert_eq!(Anchor::Top.leaving(), Direction::Up);
assert_eq!(Anchor::Right.leaving(), Direction::Right);
assert_eq!(Anchor::Bottom.leaving(), Direction::Down);
assert_eq!(Anchor::Left.leaving(), Direction::Left);
}
#[test]
fn a_reference_resolves_to_its_anchors_point_moved_by_the_offset() {
let diagram = Diagram::new();
let (diagram, id) = diagram.add(the_box_at_the_origin());
let right = Reference::new(id, Anchor::Right, Delta { dx: 2, dy: 0 }, 0);
let bottom = Reference::new(id, Anchor::Bottom, Delta { dx: 0, dy: 1 }, 0);
assert_eq!(resolve(right, &diagram), Some(Pos { x: 5, y: 1 }));
assert_eq!(resolve(bottom, &diagram), Some(Pos { x: 1, y: 3 }));
}
#[test]
fn a_reference_with_no_offset_stands_on_its_side_and_is_not_the_bare_point() {
let diagram = Diagram::new();
let the_box = the_box_at_the_origin();
let (diagram, id) = diagram.add(the_box.clone());
let on_the_side = Reference::new(id, Anchor::Right, Delta { dx: 0, dy: 0 }, 0);
let the_side_middle = the_box
.anchor(Anchor::Right)
.expect("a box answers all four of its sides");
assert_eq!(resolve(on_the_side, &diagram), Some(the_side_middle));
assert_ne!(
Position::Reference(on_the_side),
Position::Absolute(the_side_middle)
);
}
#[test]
fn a_box_answers_its_four_side_centres() {
let the_box = Shape::Box {
at: Pos { x: 0, y: 0 },
size: Size {
width: 4,
height: 3,
},
stroke: light(),
fill: None,
text: None,
};
assert_eq!(the_box.anchor(Anchor::Top), Some(Pos { x: 1, y: 0 }));
assert_eq!(the_box.anchor(Anchor::Right), Some(Pos { x: 3, y: 1 }));
assert_eq!(the_box.anchor(Anchor::Bottom), Some(Pos { x: 1, y: 2 }));
assert_eq!(the_box.anchor(Anchor::Left), Some(Pos { x: 0, y: 1 }));
}
#[test]
fn a_box_one_cell_wide_or_one_cell_tall_answers_the_same_rule() {
let one_wide = Shape::Box {
at: Pos { x: 0, y: 0 },
size: Size {
width: 1,
height: 3,
},
stroke: light(),
fill: None,
text: None,
};
let one_tall = Shape::Box {
at: Pos { x: 0, y: 0 },
size: Size {
width: 4,
height: 1,
},
stroke: light(),
fill: None,
text: None,
};
assert_eq!(one_wide.anchor(Anchor::Top), Some(Pos { x: 0, y: 0 }));
assert_eq!(one_wide.anchor(Anchor::Right), Some(Pos { x: 0, y: 1 }));
assert_eq!(one_wide.anchor(Anchor::Bottom), Some(Pos { x: 0, y: 2 }));
assert_eq!(one_wide.anchor(Anchor::Left), Some(Pos { x: 0, y: 1 }));
assert_eq!(one_tall.anchor(Anchor::Top), Some(Pos { x: 1, y: 0 }));
assert_eq!(one_tall.anchor(Anchor::Right), Some(Pos { x: 3, y: 0 }));
assert_eq!(one_tall.anchor(Anchor::Bottom), Some(Pos { x: 1, y: 0 }));
assert_eq!(one_tall.anchor(Anchor::Left), Some(Pos { x: 0, y: 0 }));
}
#[test]
fn a_line_answers_the_four_side_centres_of_a_flat_box() {
let line = |at: Pos, len: u16, orientation| Shape::Line {
at,
len,
orientation,
stroke: light(),
};
let flat = |at: Pos, len: u16, orientation| Shape::Box {
at,
size: super::flat_size(len, orientation),
stroke: light(),
fill: None,
text: None,
};
for (at, len, orientation) in [
(Pos { x: 0, y: 0 }, 5, Orientation::Horizontal),
(Pos { x: 0, y: 0 }, 5, Orientation::Vertical),
] {
for anchor in [Anchor::Top, Anchor::Right, Anchor::Bottom, Anchor::Left] {
assert_eq!(
line(at, len, orientation).anchor(anchor),
flat(at, len, orientation).anchor(anchor),
"{orientation:?} at {anchor:?}"
);
}
}
let across = line(Pos { x: 0, y: 0 }, 5, Orientation::Horizontal);
assert_eq!(across.anchor(Anchor::Top), Some(Pos { x: 2, y: 0 }));
assert_eq!(across.anchor(Anchor::Bottom), Some(Pos { x: 2, y: 0 }));
assert_eq!(across.anchor(Anchor::Left), Some(Pos { x: 0, y: 0 }));
assert_eq!(across.anchor(Anchor::Right), Some(Pos { x: 4, y: 0 }));
}
#[test]
fn a_connector_answers_none_of_its_four_side_centres() {
let connector = Shape::Connector {
from: Endpoint::absolute(Pos { x: 0, y: 0 }, Direction::Right, Terminal::Arm),
to: Endpoint::absolute(Pos { x: 6, y: 0 }, Direction::Left, Terminal::Arm),
stroke: light(),
};
for anchor in [Anchor::Top, Anchor::Right, Anchor::Bottom, Anchor::Left] {
assert_eq!(connector.anchor(anchor), None, "{anchor:?}");
}
}
#[test]
fn a_displacement_moves_a_point_and_grows_a_references_offset() {
let by = Delta { dx: 4, dy: 0 };
let reference = super::Position::Reference(crate::Reference::new(
identity(1),
Anchor::Right,
Delta { dx: 0, dy: 0 },
0,
));
let point = super::Position::Absolute(Pos { x: 1, y: 1 });
assert_eq!(
point.displaced_by(by),
super::Position::Absolute(Pos { x: 5, y: 1 })
);
assert_ne!(point.displaced_by(by), point);
let super::Position::Reference(moved) = reference.displaced_by(by) else {
unreachable!("the position above is a reference")
};
assert_eq!(moved.offset, by);
assert_ne!(reference.displaced_by(by), reference);
let super::Position::Reference(went_in) = &reference else {
unreachable!("the position above is a reference")
};
assert_eq!(moved.id, went_in.id);
assert_eq!(moved.anchor, went_in.anchor);
assert_ne!(moved.offset, went_in.offset);
let nothing = Delta { dx: 0, dy: 0 };
assert_eq!(reference.displaced_by(nothing), reference);
assert_eq!(point.displaced_by(nothing), point);
}
fn picture_of(diagram: &Diagram, size: Size) -> String {
let origin = Pos { x: 0, y: 0 };
let mut buffer = Buffer::new(Window { origin, size });
diagram.draw(&mut buffer);
render(&buffer, &GlyphCatalog::light(), buffer.window())
}
fn diagram_hanging_from(hanging_from: impl FnOnce(ShapeId) -> Reference) -> Diagram {
let diagram = Diagram::new();
let (mut diagram, id) = diagram.add(the_box_at_the_origin());
diagram = diagram
.add(Shape::Connector {
from: Endpoint::reference(hanging_from(id), Terminal::Arm),
to: Endpoint::absolute(Pos { x: 1, y: 5 }, Direction::Up, Terminal::Arm),
stroke: light(),
})
.0;
diagram
}
#[test]
fn a_reference_with_an_out_resolves_to_its_side_moved_by_the_offset_and_by_the_out() {
let diagram = Diagram::new();
let (diagram, id) = diagram.add(the_box_at_the_origin());
let reference = Reference::new(id, Anchor::Bottom, Delta { dx: 1, dy: 3 }, 1);
assert_eq!(
reference.offset,
Delta { dx: 1, dy: 3 },
"offset remains separate without folding out into it"
);
assert_eq!(reference.out, 1, "out is stored in its own field");
assert_eq!(
resolve(reference, &diagram),
Some(Pos { x: 2, y: 6 }),
"{{1, 2}} plus (1, 3) plus (0, 1) is {{2, 6}}"
);
}
#[test]
fn a_negative_out_is_a_point_into_the_shape_rather_than_an_error() {
let diagram = Diagram::new();
let (diagram, id) = diagram.add(the_box_at_the_origin());
let nothing = Delta { dx: 0, dy: 0 };
let into_the_box = Reference::new(id, Anchor::Bottom, nothing, -1);
assert_eq!(
resolve(into_the_box, &diagram),
Some(Pos { x: 1, y: 1 }),
"one cell into the box from its bottom centre {{1, 2}} is {{1, 1}}"
);
}
#[test]
fn the_four_signs_are_the_outward_normal_of_the_side_they_are_named_from() {
let diagram = Diagram::new();
let (diagram, id) = diagram.add(Shape::Box {
at: Pos { x: 0, y: 0 },
size: Size {
width: 5,
height: 3,
},
stroke: light(),
fill: None,
text: None,
});
let nothing = Delta { dx: 0, dy: 0 };
let stands_at = |anchor| {
let reference = Reference::new(id, anchor, nothing, 1);
(
reference.offset,
reference.out,
resolve(reference, &diagram),
)
};
assert_eq!(
stands_at(Anchor::Top),
(Delta { dx: 0, dy: 0 }, 1, Some(Pos { x: 2, y: -1 }))
);
assert_eq!(
stands_at(Anchor::Right),
(Delta { dx: 0, dy: 0 }, 1, Some(Pos { x: 5, y: 1 }))
);
assert_eq!(
stands_at(Anchor::Bottom),
(Delta { dx: 0, dy: 0 }, 1, Some(Pos { x: 2, y: 3 }))
);
assert_eq!(
stands_at(Anchor::Left),
(Delta { dx: 0, dy: 0 }, 1, Some(Pos { x: -1, y: 1 }))
);
for (anchor, dx, dy) in [
(Anchor::Top, 0, -3),
(Anchor::Right, 3, 0),
(Anchor::Bottom, 0, 3),
(Anchor::Left, -3, 0),
] {
let outward = Anchor::outward(anchor, 3);
assert_eq!(
(outward.dx, outward.dy),
(dx, dy),
"{anchor:?} moved along the axis its own side runs in"
);
}
}
#[test]
fn an_out_of_zero_is_the_reference_written_without_it() {
let size = Size {
width: 6,
height: 6,
};
let nothing = Delta { dx: 0, dy: 0 };
assert_eq!(
Reference::new(identity(1), Anchor::Bottom, nothing, 0),
Reference {
id: identity(1),
anchor: Anchor::Bottom,
offset: nothing,
out: 0,
},
"an `out` of zero is the four fields written with zero out"
);
let as_an_out = picture_of(
&diagram_hanging_from(|id| Reference::new(id, Anchor::Bottom, nothing, 0)),
size,
);
let as_an_offset = picture_of(
&diagram_hanging_from(|id| Reference {
id,
anchor: Anchor::Bottom,
offset: nothing,
out: 0,
}),
size,
);
let a_cell_clear = picture_of(
&diagram_hanging_from(|id| Reference::new(id, Anchor::Bottom, nothing, 1)),
size,
);
assert_eq!(as_an_out, as_an_offset, "the same picture");
assert_ne!(
as_an_out, a_cell_clear,
"an `out` of zero drew the same as an `out` of one, so the field is being dropped"
);
}
#[test]
fn a_reference_keeps_its_offset_and_its_out_distinct_while_resolving_to_the_same_point() {
let diagram = Diagram::new();
let (diagram, id) = diagram.add(the_box_at_the_origin());
let nothing = Delta { dx: 0, dy: 0 };
let one_down = Delta { dx: 0, dy: 1 };
let as_offset = Reference::new(id, Anchor::Bottom, one_down, 0);
let as_out = Reference::new(id, Anchor::Bottom, nothing, 1);
assert_ne!(
as_offset, as_out,
"offset and out are held in separate fields on Reference"
);
assert_eq!(as_offset.offset, one_down);
assert_eq!(as_offset.out, 0);
assert_eq!(as_out.offset, nothing);
assert_eq!(as_out.out, 1);
assert_eq!(
resolve(as_offset, &diagram),
resolve(as_out, &diagram),
"both spellings resolve to the exact same position in the diagram"
);
assert_eq!(resolve(as_out, &diagram), Some(Pos { x: 1, y: 3 }));
}
#[test]
fn a_displacement_grows_the_offset_of_a_reference_written_with_an_out_as_it_grows_any_other() {
let by = Delta { dx: 0, dy: 3 };
let diagram = Diagram::new();
let (diagram, id) = diagram.add(the_box_at_the_origin());
let hanging = Reference::new(id, Anchor::Bottom, Delta { dx: 0, dy: 0 }, 1);
let connector = Shape::Connector {
from: Endpoint::reference(hanging, Terminal::Arm),
to: Endpoint::absolute(Pos { x: 1, y: 5 }, Direction::Up, Terminal::Arm),
stroke: light(),
};
let Shape::Connector { from, .. } = connector.displaced_by(by) else {
unreachable!("the figure above is a connector")
};
let EndpointAt::Reference(moved) = from.at else {
unreachable!("its near end was a reference")
};
assert_eq!(
moved.offset,
Delta { dx: 0, dy: 3 },
"three rows of displacement added to the offset"
);
assert_eq!(moved.out, 1, "out is preserved across displacement");
assert_eq!(
resolve(moved, &diagram),
Some(Pos { x: 1, y: 6 }),
"{{1, 3}} slid three rows down and nothing else moved"
);
assert_eq!(
resolve(hanging, &diagram),
Some(Pos { x: 1, y: 3 }),
"the gap the caller wrote is the same reference's: {{1, 2}} and one cell out"
);
}
#[test]
fn an_out_and_an_offset_against_each_other_add_and_nothing_reports_the_cancellation() {
let diagram = Diagram::new();
let (diagram, id) = diagram.add(the_box_at_the_origin());
let cancelling = Reference::new(id, Anchor::Bottom, Delta { dx: 0, dy: -1 }, 1);
let past_the_side = Reference::new(id, Anchor::Bottom, Delta { dx: 0, dy: -3 }, 1);
assert_eq!(cancelling.offset, Delta { dx: 0, dy: -1 });
assert_eq!(cancelling.out, 1);
assert_eq!(
resolve(cancelling, &diagram),
Some(Pos { x: 1, y: 2 }),
"one cell up against one cell down is the side itself, which is a point and not a fault"
);
assert_eq!(past_the_side.offset, Delta { dx: 0, dy: -3 });
assert_eq!(past_the_side.out, 1);
assert_eq!(
resolve(past_the_side, &diagram),
Some(Pos { x: 1, y: 0 }),
"three cells up against one cell down leaves two, neither clamped nor reported"
);
}
#[test]
fn an_out_on_a_reference_that_resolves_to_nothing_still_draws_nothing() {
let size = Size {
width: 6,
height: 6,
};
let nothing = Delta { dx: 0, dy: 0 };
let arm = |reference: Reference, to: Pos| Shape::Connector {
from: Endpoint::reference(reference, Terminal::Arm),
to: Endpoint::absolute(to, Direction::Up, Terminal::Arm),
stroke: light(),
};
let mut chain = Diagram::new();
chain = chain.add(the_box_at_the_origin()).0;
let (mut chain, the_first) = chain.add(arm(
Reference::new(identity(99), Anchor::Bottom, nothing, 1),
Pos { x: 1, y: 5 },
));
chain = chain
.add(arm(
Reference::new(the_first, Anchor::Right, nothing, 1),
Pos { x: 3, y: 5 },
))
.0;
let mut just_the_box = Diagram::new();
just_the_box = just_the_box.add(the_box_at_the_origin()).0;
assert_eq!(
picture_of(&chain, size),
picture_of(&just_the_box, size),
"a reference that resolves to nothing drew something, whatever its `out`"
);
}
#[test]
fn an_out_from_the_top_or_the_bottom_of_a_horizontal_line_moves_the_same_one_cell() {
let diagram = Diagram::new();
let the_line = Shape::Line {
at: Pos { x: 0, y: 0 },
len: 5,
orientation: Orientation::Horizontal,
stroke: light(),
};
let (diagram, id) = diagram.add(the_line.clone());
let on_the_line = the_line
.anchor(Anchor::Top)
.expect("a line answers all four of its sides");
let nothing = Delta { dx: 0, dy: 0 };
let stands_at = |anchor| resolve(Reference::new(id, anchor, nothing, 1), &diagram);
for (anchor, at) in [
(Anchor::Top, Pos { x: 2, y: -1 }),
(Anchor::Bottom, Pos { x: 2, y: 1 }),
] {
assert_eq!(stands_at(anchor), Some(at), "{anchor:?}");
assert_eq!(
at.x, on_the_line.x,
"{anchor:?} moved the point along the line rather than away from it"
);
assert_eq!(
(at.y - on_the_line.y).abs(),
1,
"{anchor:?} did not stand one cell clear of the line"
);
}
}
}