nucleation 0.3.15

A high-performance Minecraft schematic parser and utility library
Documentation
//! Symmetry groups generated by composing `transforms.rs` rotations/flips.
//!
//! A [`RigidOp`] is a sequence of rotation/flip steps. It applies to a position
//! by composing integer matrices and to a blockstate via `transforms`, using
//! the *same* step sequence so geometry and directional properties stay
//! consistent.

use crate::block_state::BlockState;
use crate::transforms::{transform_block_state_flip, transform_block_state_rotate, Axis};

pub type Pos = (i32, i32, i32);

#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum Step {
    Rotate(Axis, i32), // degrees in {90,180,270}
    Flip(Axis),
}

#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum Symmetry {
    None,
    Yaw,
    YawMirror,
    Octahedral,
    OctahedralFull,
}

impl Symmetry {
    /// Parse a symmetry-group name (lowercase). Inverse of the canonical names.
    pub fn from_name(name: &str) -> Option<Symmetry> {
        Some(match name {
            "none" => Symmetry::None,
            "yaw" => Symmetry::Yaw,
            "yaw_mirror" => Symmetry::YawMirror,
            "octahedral" => Symmetry::Octahedral,
            "octahedral_full" => Symmetry::OctahedralFull,
            _ => return None,
        })
    }
}

/// A composed rigid operation: a sequence of steps applied left-to-right.
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct RigidOp {
    steps: Vec<Step>,
}

fn rot_pos(p: Pos, axis: Axis, deg: i32) -> Pos {
    let (x, y, z) = p;
    let d = deg.rem_euclid(360);
    // Right-handed. MUST match transforms::Direction rotation handedness — the
    // rotation-invariance test in the fingerprint module is the oracle.
    match (axis, d) {
        (Axis::Y, 90) => (z, y, -x),
        (Axis::Y, 180) => (-x, y, -z),
        (Axis::Y, 270) => (-z, y, x),
        (Axis::X, 90) => (x, -z, y),
        (Axis::X, 180) => (x, -y, -z),
        (Axis::X, 270) => (x, z, -y),
        (Axis::Z, 90) => (-y, x, z),
        (Axis::Z, 180) => (-x, -y, z),
        (Axis::Z, 270) => (y, -x, z),
        _ => (x, y, z),
    }
}

fn flip_pos(p: Pos, axis: Axis) -> Pos {
    let (x, y, z) = p;
    match axis {
        Axis::X => (-x, y, z),
        Axis::Y => (x, -y, z),
        Axis::Z => (x, y, -z),
    }
}

impl RigidOp {
    /// The identity operation (no rotation/flip).
    pub fn identity() -> RigidOp {
        RigidOp { steps: vec![] }
    }

    pub fn apply_pos(&self, p: Pos) -> Pos {
        self.steps.iter().fold(p, |acc, s| match *s {
            Step::Rotate(a, d) => rot_pos(acc, a, d),
            Step::Flip(a) => flip_pos(acc, a),
        })
    }

    pub fn apply_block(&self, b: &BlockState) -> BlockState {
        self.steps.iter().fold(b.clone(), |acc, s| match *s {
            Step::Rotate(a, d) => transform_block_state_rotate(&acc, a, d),
            Step::Flip(a) => transform_block_state_flip(&acc, a),
        })
    }
}

impl Symmetry {
    pub fn elements(&self) -> Vec<RigidOp> {
        match self {
            Symmetry::None => vec![RigidOp { steps: vec![] }],
            Symmetry::Yaw => yaw_ops(),
            Symmetry::YawMirror => {
                let mut v = yaw_ops();
                let mirrored: Vec<RigidOp> = yaw_ops()
                    .into_iter()
                    .map(|mut op| {
                        op.steps.push(Step::Flip(Axis::X));
                        op
                    })
                    .collect();
                v.extend(mirrored);
                v
            }
            Symmetry::Octahedral => octahedral_ops(false),
            Symmetry::OctahedralFull => octahedral_ops(true),
        }
    }
}

fn yaw_ops() -> Vec<RigidOp> {
    [0, 90, 180, 270]
        .into_iter()
        .map(|d| RigidOp {
            steps: if d == 0 {
                vec![]
            } else {
                vec![Step::Rotate(Axis::Y, d)]
            },
        })
        .collect()
}

/// Generate the 24 (or 48) octahedral ops by BFS over the X/Y 90° generators
/// (plus an X-flip generator for the full group), deduplicating by the images
/// of the three basis vectors.
fn octahedral_ops(with_reflections: bool) -> Vec<RigidOp> {
    let basis = [(1, 0, 0), (0, 1, 0), (0, 0, 1)];
    let key = |op: &RigidOp| -> [Pos; 3] {
        [
            op.apply_pos(basis[0]),
            op.apply_pos(basis[1]),
            op.apply_pos(basis[2]),
        ]
    };

    let mut generators = vec![
        RigidOp {
            steps: vec![Step::Rotate(Axis::X, 90)],
        },
        RigidOp {
            steps: vec![Step::Rotate(Axis::Y, 90)],
        },
    ];
    if with_reflections {
        generators.push(RigidOp {
            steps: vec![Step::Flip(Axis::X)],
        });
    }

    let identity = RigidOp { steps: vec![] };
    let mut seen = std::collections::BTreeMap::new();
    seen.insert(key(&identity), identity.clone());
    let mut frontier = vec![identity];
    while let Some(cur) = frontier.pop() {
        for g in &generators {
            let mut steps = cur.steps.clone();
            steps.extend(g.steps.iter().copied());
            let next = RigidOp { steps };
            let k = key(&next);
            if let std::collections::btree_map::Entry::Vacant(e) = seen.entry(k) {
                e.insert(next.clone());
                frontier.push(next);
            }
        }
    }
    seen.into_values().collect()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn yaw_rotations_of_a_point() {
        let ops = Symmetry::Yaw.elements();
        assert_eq!(ops.len(), 4);
        let p = (1, 0, 0);
        let mut seen: Vec<Pos> = ops.iter().map(|g| g.apply_pos(p)).collect();
        assert!(seen.iter().all(|q| q.1 == 0), "Y is fixed under yaw");
        seen.sort();
        seen.dedup();
        assert_eq!(seen.len(), 4, "four distinct images");
    }

    #[test]
    fn group_sizes() {
        assert_eq!(Symmetry::None.elements().len(), 1);
        assert_eq!(Symmetry::Yaw.elements().len(), 4);
        assert_eq!(Symmetry::YawMirror.elements().len(), 8);
        assert_eq!(Symmetry::Octahedral.elements().len(), 24);
        assert_eq!(Symmetry::OctahedralFull.elements().len(), 48);
    }

    #[test]
    fn rotating_a_directional_block_rotates_its_facing() {
        let north = BlockState::new("minecraft:repeater")
            .with_properties(vec![("facing".into(), "north".into())]);
        let yaw90 = &Symmetry::Yaw.elements()[1];
        let once = yaw90.apply_block(&north);
        assert_ne!(once.get_property("facing"), north.get_property("facing"));
        let four = (0..4).fold(north.clone(), |b, _| yaw90.apply_block(&b));
        assert_eq!(four.get_property("facing"), north.get_property("facing"));
    }

    #[test]
    fn rigidop_round_trips_via_serde() {
        let op = Symmetry::YawMirror.elements().into_iter().nth(2).unwrap();
        let j = serde_json::to_string(&op).unwrap();
        let back: RigidOp = serde_json::from_str(&j).unwrap();
        assert_eq!(format!("{:?}", op), format!("{:?}", back));
    }

    #[test]
    fn symmetry_from_name() {
        assert_eq!(Symmetry::from_name("none"), Some(Symmetry::None));
        assert_eq!(Symmetry::from_name("yaw_mirror"), Some(Symmetry::YawMirror));
        assert_eq!(
            Symmetry::from_name("octahedral"),
            Some(Symmetry::Octahedral)
        );
        assert_eq!(Symmetry::from_name("bogus"), None);
    }
}