use super::{Config, Coord};
use std::{
cmp::Ordering,
fmt::{self, Display, Formatter},
matches,
ops::Mul,
str::FromStr,
vec,
};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum Transform {
#[default]
Id,
#[cfg_attr(feature = "serde", serde(rename(serialize = "R90")))]
#[cfg_attr(feature = "serde", serde(alias = "R90"))]
Rotate90,
#[cfg_attr(feature = "serde", serde(rename(serialize = "R180")))]
#[cfg_attr(feature = "serde", serde(alias = "R180"))]
Rotate180,
#[cfg_attr(feature = "serde", serde(rename(serialize = "R270")))]
#[cfg_attr(feature = "serde", serde(alias = "R270"))]
Rotate270,
#[cfg_attr(feature = "serde", serde(rename(serialize = "F-")))]
#[cfg_attr(feature = "serde", serde(alias = "F-"))]
FlipRow,
#[cfg_attr(feature = "serde", serde(rename(serialize = "F|")))]
#[cfg_attr(feature = "serde", serde(alias = "F|"))]
FlipCol,
#[cfg_attr(feature = "serde", serde(rename(serialize = "F\\")))]
#[cfg_attr(feature = "serde", serde(alias = "F\\"))]
FlipDiag,
#[cfg_attr(feature = "serde", serde(rename(serialize = "F/")))]
#[cfg_attr(feature = "serde", serde(alias = "F/"))]
FlipAntidiag,
}
impl FromStr for Transform {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"Id" => Ok(Self::Id),
"R90" => Ok(Self::Rotate90),
"R180" => Ok(Self::Rotate180),
"R270" => Ok(Self::Rotate270),
"F-" => Ok(Self::FlipRow),
"F|" => Ok(Self::FlipCol),
"F\\" => Ok(Self::FlipDiag),
"F/" => Ok(Self::FlipAntidiag),
_ => Err(String::from("Invalid Transform")),
}
}
}
impl Display for Transform {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
let s = match self {
Self::Id => "Id",
Self::Rotate90 => "R90",
Self::Rotate180 => "R180",
Self::Rotate270 => "R270",
Self::FlipRow => "F-",
Self::FlipCol => "F|",
Self::FlipDiag => "F\\",
Self::FlipAntidiag => "F/",
};
write!(f, "{s}")?;
Ok(())
}
}
impl Mul for Transform {
type Output = Self;
fn mul(self, rhs: Self) -> Self {
match (self, rhs) {
(Self::Id, Self::Id)
| (Self::Rotate90, Self::Rotate270)
| (Self::Rotate180, Self::Rotate180)
| (Self::Rotate270, Self::Rotate90)
| (Self::FlipRow, Self::FlipRow)
| (Self::FlipCol, Self::FlipCol)
| (Self::FlipDiag, Self::FlipDiag)
| (Self::FlipAntidiag, Self::FlipAntidiag) => Self::Id,
(Self::Id, Self::Rotate90)
| (Self::Rotate90, Self::Id)
| (Self::Rotate180, Self::Rotate270)
| (Self::Rotate270, Self::Rotate180)
| (Self::FlipRow, Self::FlipAntidiag)
| (Self::FlipCol, Self::FlipDiag)
| (Self::FlipDiag, Self::FlipRow)
| (Self::FlipAntidiag, Self::FlipCol) => Self::Rotate90,
(Self::Id, Self::Rotate180)
| (Self::Rotate90, Self::Rotate90)
| (Self::Rotate180, Self::Id)
| (Self::Rotate270, Self::Rotate270)
| (Self::FlipRow, Self::FlipCol)
| (Self::FlipCol, Self::FlipRow)
| (Self::FlipDiag, Self::FlipAntidiag)
| (Self::FlipAntidiag, Self::FlipDiag) => Self::Rotate180,
(Self::Id, Self::Rotate270)
| (Self::Rotate90, Self::Rotate180)
| (Self::Rotate180, Self::Rotate90)
| (Self::Rotate270, Self::Id)
| (Self::FlipRow, Self::FlipDiag)
| (Self::FlipCol, Self::FlipAntidiag)
| (Self::FlipDiag, Self::FlipCol)
| (Self::FlipAntidiag, Self::FlipRow) => Self::Rotate270,
(Self::Id, Self::FlipRow)
| (Self::Rotate90, Self::FlipAntidiag)
| (Self::Rotate180, Self::FlipCol)
| (Self::Rotate270, Self::FlipDiag)
| (Self::FlipRow, Self::Id)
| (Self::FlipCol, Self::Rotate180)
| (Self::FlipDiag, Self::Rotate90)
| (Self::FlipAntidiag, Self::Rotate270) => Self::FlipRow,
(Self::Id, Self::FlipCol)
| (Self::Rotate90, Self::FlipDiag)
| (Self::Rotate180, Self::FlipRow)
| (Self::Rotate270, Self::FlipAntidiag)
| (Self::FlipRow, Self::Rotate180)
| (Self::FlipCol, Self::Id)
| (Self::FlipDiag, Self::Rotate270)
| (Self::FlipAntidiag, Self::Rotate90) => Self::FlipCol,
(Self::Id, Self::FlipDiag)
| (Self::Rotate90, Self::FlipRow)
| (Self::Rotate180, Self::FlipAntidiag)
| (Self::Rotate270, Self::FlipCol)
| (Self::FlipRow, Self::Rotate270)
| (Self::FlipCol, Self::Rotate90)
| (Self::FlipDiag, Self::Id)
| (Self::FlipAntidiag, Self::Rotate180) => Self::FlipDiag,
(Self::Id, Self::FlipAntidiag)
| (Self::Rotate90, Self::FlipCol)
| (Self::Rotate180, Self::FlipDiag)
| (Self::Rotate270, Self::FlipRow)
| (Self::FlipRow, Self::Rotate90)
| (Self::FlipCol, Self::Rotate270)
| (Self::FlipDiag, Self::Rotate180)
| (Self::FlipAntidiag, Self::Id) => Self::FlipAntidiag,
}
}
}
impl Transform {
pub const ALL: [Self; 8] = [
Self::Id,
Self::Rotate90,
Self::Rotate180,
Self::Rotate270,
Self::FlipRow,
Self::FlipCol,
Self::FlipDiag,
Self::FlipAntidiag,
];
pub const fn require_square_world(self) -> bool {
!self.is_in(Symmetry::D4Ortho)
}
pub const fn require_no_diagonal_width(self) -> bool {
!self.is_in(Symmetry::D4Diag)
}
pub const fn order(self) -> u8 {
match self {
Self::Id => 1,
Self::Rotate90 | Self::Rotate270 => 4,
_ => 2,
}
}
pub const fn inverse(self) -> Self {
match self {
Self::Rotate90 => Self::Rotate270,
Self::Rotate270 => Self::Rotate90,
x => x,
}
}
pub const fn is_in(self, sym: Symmetry) -> bool {
matches!(
(self, sym),
(Self::Id, _)
| (_, Symmetry::D8)
| (Self::Rotate90 | Self::Rotate270, Symmetry::C4)
| (
Self::Rotate180,
Symmetry::C2 | Symmetry::C4 | Symmetry::D4Ortho | Symmetry::D4Diag
)
| (Self::FlipRow, Symmetry::D2Row | Symmetry::D4Ortho)
| (Self::FlipCol, Symmetry::D2Col | Symmetry::D4Ortho)
| (Self::FlipDiag, Symmetry::D2Diag | Symmetry::D4Diag)
| (Self::FlipAntidiag, Symmetry::D2Antidiag | Symmetry::D4Diag),
)
}
pub const fn act_on(self, coord: Coord, width: i32, height: i32) -> Coord {
let (x, y, t) = coord;
match self {
Self::Id => (x, y, t),
Self::Rotate90 => (y, width - 1 - x, t),
Self::Rotate180 => (width - 1 - x, height - 1 - y, t),
Self::Rotate270 => (height - 1 - y, x, t),
Self::FlipRow => (x, height - 1 - y, t),
Self::FlipCol => (width - 1 - x, y, t),
Self::FlipDiag => (y, x, t),
Self::FlipAntidiag => (height - 1 - y, width - 1 - x, t),
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum Symmetry {
#[default]
C1,
C2,
C4,
#[cfg_attr(feature = "serde", serde(rename(serialize = "D2-")))]
#[cfg_attr(feature = "serde", serde(alias = "D2-"))]
D2Row,
#[cfg_attr(feature = "serde", serde(rename(serialize = "D2|")))]
#[cfg_attr(feature = "serde", serde(alias = "D2|"))]
D2Col,
#[cfg_attr(feature = "serde", serde(rename(serialize = "D2\\")))]
#[cfg_attr(feature = "serde", serde(alias = "D2\\"))]
D2Diag,
#[cfg_attr(feature = "serde", serde(rename(serialize = "D2/")))]
#[cfg_attr(feature = "serde", serde(alias = "D2/"))]
D2Antidiag,
#[cfg_attr(feature = "serde", serde(rename(serialize = "D4+")))]
#[cfg_attr(feature = "serde", serde(alias = "D4+"))]
D4Ortho,
#[cfg_attr(feature = "serde", serde(rename(serialize = "D4X")))]
#[cfg_attr(feature = "serde", serde(alias = "D4X"))]
D4Diag,
D8,
}
impl FromStr for Symmetry {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"C1" => Ok(Self::C1),
"C2" => Ok(Self::C2),
"C4" => Ok(Self::C4),
"D2-" => Ok(Self::D2Row),
"D2|" => Ok(Self::D2Col),
"D2\\" => Ok(Self::D2Diag),
"D2/" => Ok(Self::D2Antidiag),
"D4+" => Ok(Self::D4Ortho),
"D4X" => Ok(Self::D4Diag),
"D8" => Ok(Self::D8),
_ => Err(String::from("invalid Self")),
}
}
}
impl Display for Symmetry {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
let s = match self {
Self::C1 => "C1",
Self::C2 => "C2",
Self::C4 => "C4",
Self::D2Row => "D2-",
Self::D2Col => "D2|",
Self::D2Diag => "D2\\",
Self::D2Antidiag => "D2/",
Self::D4Ortho => "D4+",
Self::D4Diag => "D4X",
Self::D8 => "D8",
};
write!(f, "{s}")?;
Ok(())
}
}
impl PartialOrd for Symmetry {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
if self == other {
Some(Ordering::Equal)
} else if self.is_subgroup_of(*other) {
Some(Ordering::Less)
} else if other.is_subgroup_of(*self) {
Some(Ordering::Greater)
} else {
None
}
}
}
impl Symmetry {
pub const ALL: [Self; 10] = [
Self::C1,
Self::C2,
Self::C4,
Self::D2Col,
Self::D2Row,
Self::D2Diag,
Self::D2Antidiag,
Self::D4Diag,
Self::D4Ortho,
Self::D8,
];
pub const fn is_subgroup_of(self, other: Self) -> bool {
matches!(
(self, other),
(Self::C1, _)
| (_, Self::D8)
| (Self::C2, Self::C2 | Self::C4 | Self::D4Ortho | Self::D4Diag)
| (Self::C4, Self::C4)
| (Self::D2Row, Self::D2Row | Self::D4Ortho)
| (Self::D2Col, Self::D2Col | Self::D4Ortho)
| (Self::D2Diag, Self::D2Diag | Self::D4Diag)
| (Self::D2Antidiag, Self::D2Antidiag | Self::D4Diag)
| (Self::D4Ortho, Self::D4Ortho)
| (Self::D4Diag, Self::D4Diag)
)
}
pub const fn require_square_world(self) -> bool {
!self.is_subgroup_of(Self::D4Ortho)
}
pub const fn require_no_diagonal_width(self) -> bool {
!self.is_subgroup_of(Self::D4Diag)
}
pub fn members(self) -> Vec<Transform> {
match self {
Self::C1 => vec![Transform::Id],
Self::C2 => vec![Transform::Id, Transform::Rotate180],
Self::C4 => vec![
Transform::Id,
Transform::Rotate90,
Transform::Rotate180,
Transform::Rotate270,
],
Self::D2Row => vec![Transform::Id, Transform::FlipRow],
Self::D2Col => vec![Transform::Id, Transform::FlipCol],
Self::D2Diag => vec![Transform::Id, Transform::FlipDiag],
Self::D2Antidiag => vec![Transform::Id, Transform::FlipAntidiag],
Self::D4Ortho => vec![
Transform::Id,
Transform::FlipRow,
Transform::FlipCol,
Transform::Rotate180,
],
Self::D4Diag => vec![
Transform::Id,
Transform::FlipDiag,
Transform::FlipAntidiag,
Transform::Rotate180,
],
Self::D8 => vec![
Transform::Id,
Transform::Rotate90,
Transform::Rotate180,
Transform::Rotate270,
Transform::FlipRow,
Transform::FlipCol,
Transform::FlipDiag,
Transform::FlipAntidiag,
],
}
}
pub fn cosets(self) -> Vec<Transform> {
match self {
Self::C1 => vec![
Transform::Id,
Transform::Rotate90,
Transform::Rotate180,
Transform::Rotate270,
Transform::FlipRow,
Transform::FlipCol,
Transform::FlipDiag,
Transform::FlipAntidiag,
],
Self::C2 => vec![
Transform::Id,
Transform::Rotate90,
Transform::FlipRow,
Transform::FlipDiag,
],
Self::C4 => vec![Transform::Id, Transform::FlipRow],
Self::D2Row => vec![
Transform::Id,
Transform::FlipCol,
Transform::FlipDiag,
Transform::FlipAntidiag,
],
Self::D2Col => vec![
Transform::Id,
Transform::FlipRow,
Transform::FlipDiag,
Transform::FlipAntidiag,
],
Self::D2Diag => vec![
Transform::Id,
Transform::FlipRow,
Transform::FlipCol,
Transform::FlipAntidiag,
],
Self::D2Antidiag => vec![
Transform::Id,
Transform::FlipRow,
Transform::FlipCol,
Transform::FlipDiag,
],
Self::D4Ortho => vec![Transform::Id, Transform::FlipDiag],
Self::D4Diag => vec![Transform::Id, Transform::FlipRow],
Self::D8 => vec![Transform::Id],
}
}
pub const fn generated_with(self, transform: Transform) -> Self {
match (self, transform) {
(sym, Transform::Id) => sym,
(Self::C1, Transform::Rotate90 | Transform::Rotate270) => Self::C4,
(Self::C1, Transform::Rotate180) => Self::C2,
(Self::C1, Transform::FlipRow) => Self::D2Row,
(Self::C1, Transform::FlipCol) => Self::D2Col,
(Self::C1, Transform::FlipDiag) => Self::D2Diag,
(Self::C1, Transform::FlipAntidiag) => Self::D2Antidiag,
(Self::C2, Transform::Rotate90 | Transform::Rotate270) => Self::C4,
(Self::C2, Transform::Rotate180) => Self::C2,
(Self::C2, Transform::FlipRow | Transform::FlipCol) => Self::D4Ortho,
(Self::C2, Transform::FlipDiag | Transform::FlipAntidiag) => Self::D4Diag,
(Self::C4, Transform::Rotate90 | Transform::Rotate180 | Transform::Rotate270) => {
Self::C4
}
(Self::D2Row, Transform::Rotate180 | Transform::FlipCol) => Self::D4Ortho,
(Self::D2Row, Transform::FlipRow) => Self::D2Row,
(Self::D2Col, Transform::Rotate180 | Transform::FlipRow) => Self::D4Ortho,
(Self::D2Col, Transform::FlipCol) => Self::D2Col,
(Self::D2Diag, Transform::Rotate180 | Transform::FlipAntidiag) => Self::D4Diag,
(Self::D2Diag, Transform::FlipDiag) => Self::D2Diag,
(Self::D2Antidiag, Transform::Rotate180 | Transform::FlipDiag) => Self::D4Diag,
(Self::D2Antidiag, Transform::FlipAntidiag) => Self::D2Antidiag,
(Self::D4Ortho, Transform::Rotate180 | Transform::FlipRow | Transform::FlipCol) => {
Self::D4Ortho
}
(
Self::D4Diag,
Transform::Rotate180 | Transform::FlipDiag | Transform::FlipAntidiag,
) => Self::D4Diag,
_ => Self::D8,
}
}
pub fn generated_by(transforms: impl IntoIterator<Item = Transform>) -> Self {
transforms
.into_iter()
.fold(Self::C1, |sym, transform| sym.generated_with(transform))
}
}
impl Config {
pub(crate) const fn translate(&self, coord: Coord) -> Coord {
let mut coord = coord;
while coord.2 < 0 {
coord = self
.transform
.inverse()
.act_on(coord, self.width, self.height);
coord.0 -= self.dx;
coord.1 -= self.dy;
coord.2 += self.period;
}
while coord.2 >= self.period {
coord.0 += self.dx;
coord.1 += self.dy;
coord.2 -= self.period;
coord = self.transform.act_on(coord, self.width, self.height);
}
coord
}
}
#[cfg(test)]
mod tests {
use super::*;
use rand::{thread_rng, Rng};
use std::collections::HashSet;
#[test]
fn test_sym_tran_names() {
for sym in Symmetry::ALL {
assert!(Symmetry::from_str(&sym.to_string()) == Ok(sym))
}
for trans in Transform::ALL {
assert!(Transform::from_str(&trans.to_string()) == Ok(trans))
}
}
#[test]
fn test_symmetry_group_member() {
for sym in Symmetry::ALL {
let members = sym.members();
for tran in Transform::ALL {
assert_eq!(tran.is_in(sym), members.contains(&tran));
}
}
}
#[test]
fn test_symmetry_subgroup() {
for sym in Symmetry::ALL {
for sub_sym in Symmetry::ALL {
let is_subgroup = sub_sym.members().into_iter().all(|tran| tran.is_in(sym));
assert_eq!(sub_sym <= sym, is_subgroup);
}
}
}
#[test]
fn test_symmetry_coset() {
let group = Transform::ALL.iter().copied().collect::<HashSet<_>>();
for sym in Symmetry::ALL {
let all_cosets = sym
.cosets()
.into_iter()
.flat_map(|coset| sym.members().into_iter().map(move |elem| elem * coset))
.collect::<HashSet<_>>();
assert_eq!(all_cosets, group);
}
}
#[test]
fn test_generating() {
for sym in Symmetry::ALL {
let members = sym.members();
assert_eq!(sym, Symmetry::generated_by(members));
let generated_with_cosets = sym
.cosets()
.iter()
.cloned()
.fold(sym, Symmetry::generated_with);
assert_eq!(generated_with_cosets, Symmetry::D8);
for transform in Transform::ALL {
let generated_with = sym.generated_with(transform);
if transform.is_in(sym) {
assert_eq!(generated_with, sym);
} else {
assert!(generated_with > sym);
}
}
}
}
#[test]
fn test_transform_inverse() {
let width = 16;
let height = 16;
let mut rng = thread_rng();
for _ in 0..10 {
let x = rng.gen_range(0..width);
let y = rng.gen_range(0..height);
let coord = (x, y, 0);
for tran in Transform::ALL {
assert_eq!(
coord,
tran.inverse()
.act_on(tran.act_on(coord, width, height), width, height),
"{} ^ -1 != {}",
tran,
tran.inverse()
)
}
}
}
#[test]
fn test_transform_mul() {
let width = 16;
let height = 16;
let mut rng = thread_rng();
for _ in 0..10 {
let x = rng.gen_range(0..width);
let y = rng.gen_range(0..height);
let coord = (x, y, 0);
for tran0 in Transform::ALL {
for tran1 in Transform::ALL {
assert_eq!(
(tran0 * tran1).act_on(coord, width, height),
tran1.act_on(tran0.act_on(coord, width, height), width, height),
"{} * {} != {}",
tran0,
tran1,
tran0 * tran1
)
}
}
}
}
#[test]
fn test_world_condition() {
for tran in Transform::ALL {
assert_eq!(
tran.require_square_world(),
matches!(
tran,
Transform::Rotate90
| Transform::Rotate270
| Transform::FlipDiag
| Transform::FlipAntidiag
)
);
assert_eq!(
tran.require_no_diagonal_width(),
matches!(
tran,
Transform::Rotate90
| Transform::Rotate270
| Transform::FlipRow
| Transform::FlipCol
)
);
}
for sym in Symmetry::ALL {
assert_eq!(
sym.require_square_world(),
matches!(
sym,
Symmetry::C4
| Symmetry::D2Diag
| Symmetry::D2Antidiag
| Symmetry::D4Diag
| Symmetry::D8
)
);
assert_eq!(
sym.require_no_diagonal_width(),
matches!(
sym,
Symmetry::C4
| Symmetry::D2Row
| Symmetry::D2Col
| Symmetry::D4Ortho
| Symmetry::D8
)
);
}
}
}