use indicatrix::{
geometry::meet_solver::{Block, MeetConstraint, classify_blocks},
optics::materials::GemMaterial,
};
use indicatrix_cut_core::{
AngleChange, Design, DesignSolveError, Edit, OptimizeConfig, ResolvedMaterial, Risk,
SearchHooks, critical_angle_deg, optimize_design, retarget_angle_deg, tier_margin_deg,
windowing_risk,
};
#[cfg(test)]
mod tests;
const OPTIMIZER_SAFETY_BOUND_DEG: f64 = 89.5;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct CrownShift {
pub fraction: f64,
pub scale_by_ratio: bool,
}
impl Default for CrownShift {
fn default() -> Self {
Self {
fraction: 0.0,
scale_by_ratio: false,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum RetargetMode {
Shift,
Optimize(OptimizeConfig),
}
#[derive(Debug, Clone, PartialEq)]
pub struct RetargetRow {
pub tier_index: usize,
pub block: Block,
pub name: String,
pub old_angle: f64,
pub new_angle: f64,
pub margin_deg: f64,
pub risk: Risk,
}
#[derive(Debug, Clone, PartialEq)]
pub struct RetargetProposal {
pub rows: Vec<RetargetRow>,
pub target: ResolvedMaterial,
pub notes: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RetargetError {
Solve(DesignSolveError),
AnchoredTiers(Vec<(usize, String)>),
}
impl std::fmt::Display for RetargetError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Solve(err) => write!(f, "design does not solve: {err}"),
Self::AnchoredTiers(tiers) => {
write!(f, "adopt these tiers' meet constraint before optimizing: ")?;
for (i, (index, name)) in tiers.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "#{index} \"{name}\"")?;
}
Ok(())
}
}
}
}
impl std::error::Error for RetargetError {}
const fn is_scale_reference(constraint: &MeetConstraint) -> bool {
matches!(constraint, MeetConstraint::ScaleReference(_))
}
fn clamp_to_safety_bound(angle_deg: f64) -> f64 {
angle_deg.clamp(-OPTIMIZER_SAFETY_BOUND_DEG, OPTIMIZER_SAFETY_BOUND_DEG)
}
fn shifted_pavilion_angle(old_angle: f64, n_from: f64, n_to: f64) -> f64 {
clamp_to_safety_bound(retarget_angle_deg(old_angle, n_from, n_to))
}
fn shifted_crown_angle(old_angle: f64, n_from: f64, n_to: f64, crown: CrownShift) -> f64 {
let new_angle = if crown.scale_by_ratio {
old_angle * critical_angle_deg(n_to) / critical_angle_deg(n_from)
} else {
crown.fraction.mul_add(
critical_angle_deg(n_to) - critical_angle_deg(n_from),
old_angle,
)
};
clamp_to_safety_bound(new_angle)
}
fn shifted_angle(
design: &Design,
index: usize,
block: Block,
n_from: f64,
n_to: f64,
crown: CrownShift,
) -> f64 {
let old_angle = design.tiers[index].angle_deg;
match block {
Block::Pavilion => shifted_pavilion_angle(old_angle, n_from, n_to),
Block::Crown => shifted_crown_angle(old_angle, n_from, n_to, crown),
Block::Girdle => old_angle,
}
}
#[must_use]
pub(super) fn retarget_scope(design: &Design) -> (Vec<usize>, Vec<Block>) {
let blocks = classify_blocks(&design.meet_tier_inputs());
let scope = (0..design.tiers.len())
.filter(|&i| blocks[i] != Block::Girdle)
.collect();
(scope, blocks)
}
#[must_use]
pub(super) fn anchored_tiers_in(design: &Design, scope: &[usize]) -> Vec<(usize, String)> {
scope
.iter()
.filter(|&&i| is_scale_reference(&design.tiers[i].constraint))
.map(|&i| (i, design.tiers[i].name.clone()))
.collect()
}
#[must_use]
pub(super) fn seed_shift_design(
design: &Design,
scope: &[usize],
blocks: &[Block],
n_from: f64,
n_to: f64,
crown: CrownShift,
) -> Design {
let mut seeded = design.clone();
for &i in scope {
seeded.tiers[i].angle_deg = shifted_angle(design, i, blocks[i], n_from, n_to, crown);
}
seeded
}
#[must_use]
pub(super) fn rows_from_outcome(
original: &Design,
seeded: &Design,
scope: &[usize],
blocks: &[Block],
n_to: f64,
changes: &[AngleChange],
) -> Vec<RetargetRow> {
let mut final_angles: Vec<f64> = scope.iter().map(|&i| seeded.tiers[i].angle_deg).collect();
for change in changes {
if let Some(slot) = scope.iter().position(|&i| i == change.index) {
final_angles[slot] = change.to_deg;
}
}
scope
.iter()
.zip(&final_angles)
.map(|(&index, &new_angle)| row_for(original, index, blocks[index], new_angle, n_to))
.collect()
}
pub(super) fn build_notes(mode: RetargetMode, n_from: f64, n_to: f64) -> Vec<String> {
let delta = critical_angle_deg(n_to) - critical_angle_deg(n_from);
let mut notes = vec![format!(
"Retargeting from n_D {n_from:.4} to n_D {n_to:.4}: critical angle moves by {delta:+.3} deg."
)];
if matches!(mode, RetargetMode::Optimize(_)) {
notes.push(
"Optimize mode seeds from the critical-angle shift, then searches free tiers only; anchored tiers keep their seeded angle."
.to_string(),
);
}
notes
}
fn row_for(design: &Design, index: usize, block: Block, new_angle: f64, n_to: f64) -> RetargetRow {
RetargetRow {
tier_index: index,
block,
name: design.tiers[index].name.clone(),
old_angle: design.tiers[index].angle_deg,
new_angle,
margin_deg: tier_margin_deg(new_angle, n_to),
risk: windowing_risk(new_angle, n_to),
}
}
#[must_use = "a proposal must be shown to the user before it is applied"]
pub fn build_proposal(
design: &Design,
target: &ResolvedMaterial,
crown: CrownShift,
mode: RetargetMode,
custom_materials: &[GemMaterial],
) -> Result<RetargetProposal, RetargetError> {
build_proposal_impl(
design,
design.effective_refractive_index_with(custom_materials),
target,
crown,
mode,
)
}
fn build_proposal_impl(
design: &Design,
n_from: f64,
target: &ResolvedMaterial,
crown: CrownShift,
mode: RetargetMode,
) -> Result<RetargetProposal, RetargetError> {
let n_to = target.n_d;
let (scope, blocks) = retarget_scope(design);
let final_angles = match mode {
RetargetMode::Shift => scope
.iter()
.map(|&i| shifted_angle(design, i, blocks[i], n_from, n_to, crown))
.collect(),
RetargetMode::Optimize(config) => {
build_optimized_angles(design, n_from, target, &scope, crown, &config)?
}
};
let rows = scope
.iter()
.zip(&final_angles)
.map(|(&index, &new_angle)| row_for(design, index, blocks[index], new_angle, n_to))
.collect();
Ok(RetargetProposal {
rows,
target: target.clone(),
notes: build_notes(mode, n_from, n_to),
})
}
fn build_optimized_angles(
design: &Design,
n_from: f64,
target: &ResolvedMaterial,
scope: &[usize],
crown: CrownShift,
config: &OptimizeConfig,
) -> Result<Vec<f64>, RetargetError> {
let anchored = anchored_tiers_in(design, scope);
if !anchored.is_empty() {
return Err(RetargetError::AnchoredTiers(anchored));
}
let n_to = target.n_d;
let blocks = classify_blocks(&design.meet_tier_inputs());
let seeded = seed_shift_design(design, scope, &blocks, n_from, n_to, crown);
let outcome = optimize_design(&seeded, &target.gem, config, &SearchHooks::default())
.map_err(RetargetError::Solve)?;
let mut final_angles: Vec<f64> = scope.iter().map(|&i| seeded.tiers[i].angle_deg).collect();
for change in &outcome.changes {
if let Some(slot) = scope.iter().position(|&i| i == change.index) {
final_angles[slot] = change.to_deg;
}
}
Ok(final_angles)
}
#[must_use]
pub fn apply(design: &Design, proposal: &RetargetProposal) -> Edit {
let changes = proposal
.rows
.iter()
.filter(|row| row.tier_index < design.tiers.len())
.map(|row| {
let current = design.tiers[row.tier_index].angle_deg;
(row.tier_index, current, row.new_angle)
})
.collect();
Edit::RetargetAngles { changes }
}