indicatrix 0.9.0

A physically-based spectral gemstone renderer that turns GemCAD-style cutting instructions into rendered output.
Documentation
//! Tier/block classification: which of crown, pavilion or girdle each tier
//! belongs to, plus the crown/pavilion side convention ([`tier_sides`]) shared
//! with the normal-vector construction in `candidates`.

use super::MeetTierInput;
use crate::geometry::plane::tier_is_crown_side;

/// Which block a tier belongs to.
///
/// Classified like [`solve_meet_points`](super::solve_meet_points)'s arrangement:
/// by the sign/magnitude of the tier's normal `y`-component, a pure function of
/// `angle_deg` and crown/pavilion side (same for every index instance of a tier).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Block {
    Crown,
    Pavilion,
    Girdle,
}

/// Classifies every tier's [`Block`] in one pass, per [`tier_sides`]'s sign rule.
#[must_use]
pub fn classify_blocks(tiers: &[MeetTierInput]) -> Vec<Block> {
    let sides = tier_sides(tiers);
    tiers
        .iter()
        .zip(&sides)
        .map(|(t, &crown)| {
            let theta = t.angle_deg.abs().to_radians();
            let y = if crown { theta.cos() } else { -theta.cos() };
            // Purpose: classify an exact `f64` schedule angle (a 90 degree tier
            // gives `|cos| ~ 6e-17`), so the threshold only has to absorb libm noise.
            // Deliberately tighter than `girdle::GIRDLE_NORMAL_Y_EPSILON` (1e-3, for
            // `f32` plane normals carrying normalize noise) and looser in kind than
            // the 85 degree girdle-tier pick in `names` (a naming heuristic, not a
            // geometric test).
            if y.abs() <= 1e-6 {
                Block::Girdle
            } else if y > 0.0 {
                Block::Crown
            } else {
                Block::Pavilion
            }
        })
        .collect()
}

/// Crown/pavilion side per tier (`true` = crown), from each tier's own angle sign
/// via [`tier_is_crown_side`] -- a sign-negative zero is the culet, a positive
/// zero the table; file order plays no part.
pub(super) fn tier_sides(tiers: &[MeetTierInput]) -> Vec<bool> {
    tiers
        .iter()
        .map(|tier| tier_is_crown_side(tier.angle_deg))
        .collect()
}

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

    #[test]
    fn classify_blocks_matches_angle_sign_and_girdle_threshold() {
        let tiers = vec![
            MeetTierInput {
                angle_deg: 45.0,
                indices: vec![],
                constraint: MeetConstraint::MeetExisting,
                names: vec![],
            },
            MeetTierInput {
                angle_deg: -45.0,
                indices: vec![],
                constraint: MeetConstraint::MeetExisting,
                names: vec![],
            },
            MeetTierInput {
                angle_deg: 90.0,
                indices: vec![],
                constraint: MeetConstraint::MeetExisting,
                names: vec![],
            },
            MeetTierInput {
                angle_deg: -90.0,
                indices: vec![],
                constraint: MeetConstraint::MeetExisting,
                names: vec![],
            },
        ];
        let blocks = classify_blocks(&tiers);
        assert_eq!(
            blocks,
            vec![Block::Crown, Block::Pavilion, Block::Girdle, Block::Girdle]
        );
    }
}