use crate::{Coordinate, Offset};
use super::Contour;
#[repr(u8)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum ChainDirection {
East = 0,
NorthEast = 1,
North = 2,
NorthWest = 3,
West = 4,
SouthWest = 5,
South = 6,
SouthEast = 7,
}
impl ChainDirection {
pub const ALL: [Self; 8] = [
Self::East,
Self::NorthEast,
Self::North,
Self::NorthWest,
Self::West,
Self::SouthWest,
Self::South,
Self::SouthEast,
];
#[must_use]
pub const fn offset(self) -> Offset {
match self {
Self::East => Offset::new(1, 0),
Self::NorthEast => Offset::new(1, -1),
Self::North => Offset::new(0, -1),
Self::NorthWest => Offset::new(-1, -1),
Self::West => Offset::new(-1, 0),
Self::SouthWest => Offset::new(-1, 1),
Self::South => Offset::new(0, 1),
Self::SouthEast => Offset::new(1, 1),
}
}
#[must_use]
pub const fn from_offset(offset: Offset) -> Option<Self> {
match (offset.dx, offset.dy) {
(1, 0) => Some(Self::East),
(1, -1) => Some(Self::NorthEast),
(0, -1) => Some(Self::North),
(-1, -1) => Some(Self::NorthWest),
(-1, 0) => Some(Self::West),
(-1, 1) => Some(Self::SouthWest),
(0, 1) => Some(Self::South),
(1, 1) => Some(Self::SouthEast),
_ => None,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ChainCode {
start: Coordinate,
moves: Vec<ChainDirection>,
}
impl ChainCode {
#[must_use]
pub fn from_contour(contour: &Contour) -> Self {
let points = contour.points();
let start = points[0];
let moves = if points.len() < 2 {
Vec::new()
} else {
(0..points.len())
.map(|i| {
let a = points[i];
let b = points[(i + 1) % points.len()];
ChainDirection::from_offset(a.offset_to(b))
.expect("traced contour points are 8-adjacent")
})
.collect()
};
Self { start, moves }
}
#[must_use]
pub const fn start(&self) -> Coordinate {
self.start
}
#[must_use]
pub fn moves(&self) -> &[ChainDirection] {
&self.moves
}
#[must_use]
pub fn to_points(&self) -> Vec<Coordinate> {
let mut points = Vec::with_capacity(self.moves.len().max(1));
let mut at = self.start;
points.push(at);
for step in self.moves.iter().take(self.moves.len().saturating_sub(1)) {
at = at
.checked_add(step.offset())
.expect("chain code left the image quadrant");
points.push(at);
}
points
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn offsets_round_trip() {
for dir in ChainDirection::ALL {
assert_eq!(ChainDirection::from_offset(dir.offset()), Some(dir));
}
assert_eq!(ChainDirection::from_offset(Offset::ZERO), None);
assert_eq!(ChainDirection::from_offset(Offset::new(2, 0)), None);
assert_eq!(ChainDirection::from_offset(Offset::new(-1, 2)), None);
}
#[test]
fn freeman_codes_ascend_counterclockwise_from_east() {
assert_eq!(ChainDirection::East as u8, 0);
assert_eq!(ChainDirection::NorthEast as u8, 1);
assert_eq!(ChainDirection::North as u8, 2);
assert_eq!(ChainDirection::NorthWest as u8, 3);
assert_eq!(ChainDirection::West as u8, 4);
assert_eq!(ChainDirection::SouthWest as u8, 5);
assert_eq!(ChainDirection::South as u8, 6);
assert_eq!(ChainDirection::SouthEast as u8, 7);
}
}