use glam::Vec3;
use indicatrix::geometry::meet_solver::{Block, SolvedTier, classify_blocks};
use indicatrix_cut_core::{
Design,
optics_hints::{self, Risk},
};
#[derive(Debug, Clone)]
pub struct FacetInfo {
pub tier_index: Option<usize>,
pub index_on_gear: u32,
pub angle_deg: f64,
pub block: Option<Block>,
pub name: String,
}
impl FacetInfo {
const fn preform() -> Self {
Self {
tier_index: None,
index_on_gear: 0,
angle_deg: 0.0,
block: None,
name: String::new(),
}
}
}
#[derive(Debug, Clone)]
pub struct OverlayFlags {
pub flagged: Vec<bool>,
pub pending: Vec<bool>,
pub selected: Vec<bool>,
}
#[derive(Debug, Clone)]
pub struct FacetMap {
preform_plane_count: usize,
facets: Vec<FacetInfo>,
orbits: Vec<Vec<u32>>,
}
struct Candidate {
normal: Vec3,
offset: f32,
tier_index: usize,
index_on_gear: u32,
}
const DEDUP_QUANTUM: f32 = 1.0 / 2048.0;
impl FacetMap {
#[must_use]
pub fn from_design(design: &Design, solved: &[SolvedTier]) -> Self {
let preform_plane_count = design.preform.planes().len();
let inputs = design.meet_tier_inputs();
let blocks = classify_blocks(&inputs);
let gear_teeth = (design.meta.gear_teeth_abs().max(1)) as f32;
let mut candidates: Vec<Candidate> = Vec::new();
let mut last_side_is_crown = true;
for (tier_index, tier) in design.tiers.iter().enumerate() {
let is_crown = if tier.angle_deg == 0.0 {
if tier.angle_deg.is_sign_negative() {
false
} else {
last_side_is_crown
}
} else {
tier.angle_deg > 0.0
};
last_side_is_crown = is_crown;
let theta = (tier.angle_deg.abs() as f32).to_radians();
let (sin_theta, cos_theta) = (theta.sin(), theta.cos());
let mast = solved.get(tier_index).map_or(0.0, |s| s.mast);
let offset = -(mast.abs() as f32);
if tier.indices.is_empty() {
let normal = if is_crown {
Vec3::new(0.0, cos_theta, sin_theta)
} else {
Vec3::new(0.0, -cos_theta, sin_theta)
};
candidates.push(Candidate {
normal: normal.normalize(),
offset,
tier_index,
index_on_gear: 0,
});
continue;
}
for &idx in &tier.indices {
let phi = 2.0 * std::f32::consts::PI * (idx as f32) / gear_teeth;
let (sin_phi, cos_phi) = (phi.sin(), phi.cos());
let normal = if is_crown {
Vec3::new(sin_theta * cos_phi, cos_theta, sin_theta * sin_phi)
} else {
Vec3::new(sin_theta * cos_phi, -cos_theta, sin_theta * sin_phi)
};
candidates.push(Candidate {
normal: normal.normalize(),
offset,
tier_index,
index_on_gear: idx.round() as u32,
});
}
}
let mut kept: Vec<Candidate> = Vec::with_capacity(candidates.len());
for candidate in candidates {
let is_duplicate = kept.iter().any(|k| {
let dot = k.normal.dot(candidate.normal);
dot >= 1.0 - DEDUP_QUANTUM && (k.offset - candidate.offset).abs() <= DEDUP_QUANTUM
});
if !is_duplicate {
kept.push(candidate);
}
}
let mut facets = vec![FacetInfo::preform(); preform_plane_count];
let mut orbits: Vec<Vec<u32>> = vec![Vec::new(); design.tiers.len()];
for candidate in kept {
let facet_id = facets.len() as u32;
let block = blocks.get(candidate.tier_index).copied();
let (angle_deg, name) = design
.tiers
.get(candidate.tier_index)
.map_or((0.0, String::new()), |t| (t.angle_deg, t.name.clone()));
facets.push(FacetInfo {
tier_index: Some(candidate.tier_index),
index_on_gear: candidate.index_on_gear,
angle_deg,
block,
name,
});
if let Some(orbit) = orbits.get_mut(candidate.tier_index) {
orbit.push(facet_id);
}
}
Self {
preform_plane_count,
facets,
orbits,
}
}
#[must_use]
pub const fn preform_plane_count(&self) -> usize {
self.preform_plane_count
}
#[must_use]
pub fn tier_of(&self, facet_id: usize) -> Option<usize> {
self.facets.get(facet_id).and_then(|f| f.tier_index)
}
#[must_use]
pub fn facets_of_tier(&self, tier_index: usize) -> &[u32] {
self.orbits.get(tier_index).map_or(&[], Vec::as_slice)
}
#[must_use]
pub const fn facet_count(&self) -> usize {
self.facets.len()
}
#[must_use]
pub fn facet_label(&self, facet_id: usize) -> &str {
self.facets.get(facet_id).map_or("", |f| f.name.as_str())
}
#[must_use]
pub fn hover_text(&self, facet_id: usize, n_d: f64) -> String {
let Some(info) = self.facets.get(facet_id) else {
return String::new();
};
let Some(_tier_index) = info.tier_index else {
return "Preform".to_string();
};
let block_text = match info.block {
Some(Block::Crown) => "Crown",
Some(Block::Pavilion) => "Pavilion",
Some(Block::Girdle) => "Girdle",
None => "?",
};
let name = if info.name.is_empty() {
"(unnamed)"
} else {
info.name.as_str()
};
let angle = info.angle_deg;
let index = info.index_on_gear;
let margin = optics_hints::tier_margin_deg(info.angle_deg, n_d);
format!(
"{name} · {angle:.1}° · index {index} · {block_text} · margin {margin:.1}° over critical"
)
}
#[must_use]
pub fn overlay_flags(
&self,
design: &Design,
n_d: f64,
selected_tier: Option<usize>,
pending_tier: Option<usize>,
) -> OverlayFlags {
let plane_count = self.facets.len();
let mut flagged = vec![false; plane_count];
let mut pending = vec![false; plane_count];
let mut selected = vec![false; plane_count];
for (facet_id, info) in self.facets.iter().enumerate() {
let Some(tier_index) = info.tier_index else {
continue;
};
if pending_tier == Some(tier_index) {
pending[facet_id] = true;
}
if selected_tier == Some(tier_index) {
selected[facet_id] = true;
}
let Some(tier) = design.tiers.get(tier_index) else {
continue;
};
if tier.angle_deg < 0.0
&& optics_hints::windowing_risk(tier.angle_deg, n_d) == Risk::Windows
{
flagged[facet_id] = true;
}
}
OverlayFlags {
flagged,
pending,
selected,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use indicatrix::geometry::meet_solver::MeetConstraint;
use indicatrix_cut_core::{ConstraintTier, PreformSpec, ScheduleMeta};
fn standard_round_brilliant_design() -> Design {
const GIRDLE_INDICES: [f64; 16] = [
0.0, 6.0, 12.0, 18.0, 24.0, 30.0, 36.0, 42.0, 48.0, 54.0, 60.0, 66.0, 72.0, 78.0, 84.0,
90.0,
];
const BREAK_INDICES: [f64; 16] = [
95.0, 1.0, 11.0, 13.0, 23.0, 25.0, 35.0, 37.0, 47.0, 49.0, 59.0, 61.0, 71.0, 73.0,
83.0, 85.0,
];
const MAIN_INDICES: [f64; 8] = [0.0, 12.0, 24.0, 36.0, 48.0, 60.0, 72.0, 84.0];
const STAR_INDICES: [f64; 8] = [6.0, 18.0, 30.0, 42.0, 54.0, 66.0, 78.0, 90.0];
fn tier(name: &str, angle_deg: f64, indices: &[f64], mast: f64) -> ConstraintTier {
ConstraintTier {
angle_deg,
name: name.to_string(),
indices: indices.to_vec(),
constraint: MeetConstraint::ScaleReference(mast),
imported_meet: None,
detached: Vec::new(),
}
}
let tiers = vec![
tier("Table", 0.0, &[], 0.32),
tier("Star", 15.0, &STAR_INDICES, 0.45),
tier("Crown Main", 34.5, &MAIN_INDICES, 0.59),
tier("Upper Girdle", 41.0, &BREAK_INDICES, 0.67),
tier("Girdle", 90.0, &GIRDLE_INDICES, 1.0),
tier("Pavilion Main", -41.0, &MAIN_INDICES, 0.67),
tier("Lower Girdle", -42.5, &BREAK_INDICES, 0.68),
tier("Culet", -0.0, &[], 0.88),
];
Design::new(
PreformSpec::block(2.0, 1.0, 2.0),
ScheduleMeta {
gemcad_version: "GemCad 5.0".to_string(),
gear_teeth: 96,
gear_reference_angle: 0.0,
symmetry_order: 8,
mirror: true,
refractive_index: 1.54,
headers: Vec::new(),
footnotes: Vec::new(),
},
tiers,
)
}
#[test]
fn plane_count_matches_design_planes_on_the_standard_round_brilliant() {
let design = standard_round_brilliant_design();
let solved = design.solve().expect("every tier is pinned");
let planes = design.planes_from_solved(&solved);
let map = FacetMap::from_design(&design, &solved);
assert_eq!(map.facets.len(), planes.len());
assert_eq!(map.preform_plane_count(), design.preform.planes().len());
}
#[test]
fn preform_planes_map_to_no_tier() {
let design = standard_round_brilliant_design();
let solved = design.solve().expect("every tier is pinned");
let map = FacetMap::from_design(&design, &solved);
for facet_id in 0..map.preform_plane_count() {
assert_eq!(map.tier_of(facet_id), None);
}
}
#[test]
fn orbit_sizes_match_each_tiers_index_count() {
let design = standard_round_brilliant_design();
let solved = design.solve().expect("every tier is pinned");
let map = FacetMap::from_design(&design, &solved);
for (tier_index, tier) in design.tiers.iter().enumerate() {
let expected = tier.indices.len().max(1);
assert_eq!(
map.facets_of_tier(tier_index).len(),
expected,
"tier {tier_index} ({})",
tier.name
);
}
}
#[test]
fn every_mapped_facets_normal_carries_its_tiers_own_elevation_angle() {
let design = standard_round_brilliant_design();
let solved = design.solve().expect("every tier is pinned");
let planes = design.planes_from_solved(&solved);
let map = FacetMap::from_design(&design, &solved);
for (facet_id, &(normal, _offset)) in
planes.iter().enumerate().skip(map.preform_plane_count())
{
let tier_index = map
.tier_of(facet_id)
.expect("every non-preform facet must map to a tier");
let tier = &design.tiers[tier_index];
let theta = tier.angle_deg.abs().to_radians();
assert!(
(normal.y.abs() - theta.cos()).abs() < 1e-5,
"facet {facet_id} (tier {tier_index} {}): normal.y={}, expected +-{}",
tier.name,
normal.y,
theta.cos()
);
let horizontal = normal.x.hypot(normal.z);
assert!(
(horizontal - theta.sin()).abs() < 1e-5,
"facet {facet_id} (tier {tier_index} {}): horizontal={horizontal}, expected {}",
tier.name,
theta.sin()
);
}
}
#[test]
fn overlay_flags_mark_the_pending_and_selected_tiers_and_nothing_else_by_default() {
let design = standard_round_brilliant_design();
let solved = design.solve().expect("every tier is pinned");
let map = FacetMap::from_design(&design, &solved);
let n_d = design.effective_refractive_index();
let flags = map.overlay_flags(&design, n_d, Some(2), Some(5));
for &facet_id in map.facets_of_tier(5) {
assert!(flags.pending[facet_id as usize]);
}
for &facet_id in map.facets_of_tier(2) {
assert!(flags.selected[facet_id as usize]);
}
for &facet_id in map.facets_of_tier(3) {
assert!(!flags.pending[facet_id as usize]);
assert!(!flags.selected[facet_id as usize]);
}
assert!(
flags.flagged.iter().all(|&f| !f),
"diamond RBC must not window"
);
}
#[test]
fn hover_text_reports_a_preform_plane_distinctly_from_a_facet() {
let design = standard_round_brilliant_design();
let solved = design.solve().expect("every tier is pinned");
let map = FacetMap::from_design(&design, &solved);
let n_d = design.effective_refractive_index();
assert_eq!(map.hover_text(0, n_d), "Preform");
let facet_text = map.hover_text(map.preform_plane_count(), n_d);
assert!(facet_text.contains("Table"), "got: {facet_text}");
assert!(facet_text.contains("index 0"), "got: {facet_text}");
}
}