use super::row_format::{
constraint_kind_and_text, format_angle_cell, format_index_value, imported_meet_text,
orbit_status_text, representative_crown_and_pavilion_angles_deg, tier_label,
tier_margin_and_risk,
};
use crate::EditorTierItem;
use indicatrix::geometry::meet_solver::{Block, SolveStrategy, SolvedTier, classify_blocks};
use indicatrix_cut_core::{ConstraintTier, Design, DesignSolveError, MissingAnchor};
use std::collections::BTreeMap;
struct TierSolveInfo {
mast: String,
mast_mm: String,
mast_full: String,
strategy: String,
strategy_is_uncertain: bool,
strategy_detail: String,
needs_anchor: bool,
}
struct RowContext<'a> {
tier_blocks: &'a [Block],
warnings: &'a BTreeMap<usize, String>,
pavilion_partner_deg: Option<f64>,
}
fn build_tier_row(
design: &Design,
index: usize,
tier: &ConstraintTier,
n_d: f64,
info: TierSolveInfo,
row_context: &RowContext<'_>,
) -> EditorTierItem {
let tier_blocks = row_context.tier_blocks;
let warnings = row_context.warnings;
let pavilion_partner_deg = row_context.pavilion_partner_deg;
let (constraint_kind, constraint_text) =
constraint_kind_and_text(&tier.constraint, design.tier_target(index));
let units = indicatrix_cut_core::orbit_units(&tier.indices, &design.meta);
let (orbit_status, orbit_incomplete) = orbit_status_text(&units);
let (margin_text, risk_level) = tier_margin_and_risk(tier.angle_deg, n_d, pavilion_partner_deg);
EditorTierItem {
index: index as i32,
angle_deg: format_angle_cell(tier.angle_deg).into(),
name: tier.name.clone().into(),
indices: tier
.indices
.iter()
.copied()
.map(format_index_value)
.collect::<Vec<_>>()
.join(", ")
.into(),
constraint_kind,
constraint_text: constraint_text.into(),
mast: info.mast.into(),
mast_mm: info.mast_mm.into(),
mast_full: info.mast_full.into(),
strategy: info.strategy.into(),
strategy_is_uncertain: info.strategy_is_uncertain,
strategy_detail: info.strategy_detail.into(),
needs_anchor: info.needs_anchor,
imported_meet_text: imported_meet_text(tier.imported_meet.as_ref()).into(),
orbit_status: orbit_status.into(),
orbit_incomplete,
is_detached: !tier.detached.is_empty(),
block: block_label(tier_blocks[index]).into(),
margin_text: margin_text.into(),
risk_level,
meet_partners_text: meet_partners_text(design, index).into(),
warning_text: warnings.get(&index).cloned().unwrap_or_default().into(),
multi_selected: false,
proposed_angle: String::new().into(),
}
}
pub(super) fn tier_items_stale(design: &Design, n_d: f64) -> Vec<EditorTierItem> {
let tier_blocks = classify_blocks(&design.meet_tier_inputs());
let warnings = warning_text_by_tier(&manufacturability_warnings_tagged(design, None));
let (_, pavilion_partner_deg) = representative_crown_and_pavilion_angles_deg(design);
let row_context = RowContext {
tier_blocks: &tier_blocks,
warnings: &warnings,
pavilion_partner_deg,
};
design
.tiers
.iter()
.enumerate()
.map(|(index, tier)| {
let info = TierSolveInfo {
mast: "-".to_string(),
mast_mm: String::new(),
mast_full: String::new(),
strategy: "not solved".to_string(),
strategy_is_uncertain: true,
strategy_detail: String::new(),
needs_anchor: false,
};
build_tier_row(design, index, tier, n_d, info, &row_context)
})
.collect()
}
#[must_use]
pub(in crate::gui::editor) fn tier_items_stale_with_last_solved(
design: &Design,
n_d: f64,
last_solved: Option<&[SolvedTier]>,
dirty: &std::collections::BTreeSet<usize>,
) -> Vec<EditorTierItem> {
let last_solved = last_solved.filter(|solved| solved.len() == design.tiers.len());
let Some(last_solved) = last_solved else {
return tier_items_stale(design, n_d);
};
let tier_blocks = classify_blocks(&design.meet_tier_inputs());
let warnings = warning_text_by_tier(&manufacturability_warnings_tagged(design, None));
let mm_per_unit = design.yield_report(last_solved).mm_per_unit;
let (_, pavilion_partner_deg) = representative_crown_and_pavilion_angles_deg(design);
let row_context = RowContext {
tier_blocks: &tier_blocks,
warnings: &warnings,
pavilion_partner_deg,
};
design
.tiers
.iter()
.enumerate()
.map(|(index, tier)| {
let info = if dirty.contains(&index) {
TierSolveInfo {
mast: "-".to_string(),
mast_mm: String::new(),
mast_full: String::new(),
strategy: "not solved".to_string(),
strategy_is_uncertain: true,
strategy_detail: String::new(),
needs_anchor: false,
}
} else {
let (label, _) = strategy_label(last_solved[index].strategy);
TierSolveInfo {
mast: format!("{:.4}", last_solved[index].mast),
mast_mm: mm_per_unit.map_or_else(String::new, |mm| {
format!("{:.3} mm", last_solved[index].mast * mm)
}),
mast_full: format!("{}", last_solved[index].mast),
strategy: format!("stale ({label})"),
strategy_is_uncertain: true,
strategy_detail: last_solved[index].detail.clone(),
needs_anchor: false,
}
};
build_tier_row(design, index, tier, n_d, info, &row_context)
})
.collect()
}
pub(in crate::gui::editor) fn apply_proposed_angles(
tiers: &mut [EditorTierItem],
changes: &[indicatrix_cut_core::AngleChange],
) {
for change in changes {
if let Some(row) = tiers.get_mut(change.index) {
row.proposed_angle = format_angle_cell(change.to_deg).into();
}
}
}
fn missing_anchor_tier_blocks(missing: &MissingAnchor, design: &Design) -> BTreeMap<usize, Block> {
missing
.block_details(design)
.into_iter()
.flat_map(|(block, indices)| indices.into_iter().map(move |index| (index, block)))
.collect()
}
fn meet_partners_text(design: &Design, index: usize) -> String {
let Ok(targets) = design.facet_meets(index) else {
return String::new();
};
if targets.is_empty() {
return String::new();
}
format!(
"meets {}",
targets
.into_iter()
.map(|target| tier_label(design, target))
.collect::<Vec<_>>()
.join(", ")
)
}
const fn strategy_label(strategy: SolveStrategy) -> (&'static str, bool) {
match strategy {
SolveStrategy::ScaleReference => ("Scale reference", false),
SolveStrategy::DependencyOrder => ("Dependency order", false),
SolveStrategy::JointGroup => ("Joint group", false),
SolveStrategy::LeastSquaresFallback => ("Least-squares est.", true),
SolveStrategy::Failed => ("FAILED (untrusted)", true),
}
}
pub(in crate::gui::editor) fn tier_items(design: &Design, n_d: f64) -> Vec<EditorTierItem> {
let solved = design.solve();
match &solved {
Ok(rows) => tier_items_from_solved(design, rows, n_d),
Err(err) => {
let missing_tier_blocks = match err {
DesignSolveError::MissingAnchor(missing) => {
missing_anchor_tier_blocks(missing, design)
}
DesignSolveError::Mismatch(_)
| DesignSolveError::Solve(_)
| DesignSolveError::Target(_) => BTreeMap::new(),
};
let blocked_detail = if matches!(err, DesignSolveError::MissingAnchor(_)) {
"Another block in this design has no anchor -- see the validation banner above."
.to_string()
} else {
err.to_string()
};
let tier_blocks = classify_blocks(&design.meet_tier_inputs());
let warnings = warning_text_by_tier(&manufacturability_warnings_tagged(design, None));
let (_, pavilion_partner_deg) = representative_crown_and_pavilion_angles_deg(design);
let row_context = RowContext {
tier_blocks: &tier_blocks,
warnings: &warnings,
pavilion_partner_deg,
};
design
.tiers
.iter()
.enumerate()
.map(|(index, tier)| {
let info = missing_tier_blocks.get(&index).map_or_else(
|| TierSolveInfo {
mast: "-".to_string(),
mast_mm: String::new(),
mast_full: String::new(),
strategy: "blocked".to_string(),
strategy_is_uncertain: true,
strategy_detail: blocked_detail.clone(),
needs_anchor: false,
},
|&block| TierSolveInfo {
mast: "?".to_string(),
mast_mm: String::new(),
mast_full: String::new(),
strategy: "no anchor yet".to_string(),
strategy_is_uncertain: true,
strategy_detail: MissingAnchor::block_sentence(block),
needs_anchor: true,
},
);
build_tier_row(design, index, tier, n_d, info, &row_context)
})
.collect()
}
}
}
pub(in crate::gui::editor) fn tier_items_from_solved(
design: &Design,
solved: &[SolvedTier],
n_d: f64,
) -> Vec<EditorTierItem> {
let tier_blocks = classify_blocks(&design.meet_tier_inputs());
let warnings = warning_text_by_tier(&manufacturability_warnings_tagged(design, Some(solved)));
let mm_per_unit = design.yield_report(solved).mm_per_unit;
let (_, pavilion_partner_deg) = representative_crown_and_pavilion_angles_deg(design);
let row_context = RowContext {
tier_blocks: &tier_blocks,
warnings: &warnings,
pavilion_partner_deg,
};
design
.tiers
.iter()
.enumerate()
.map(|(index, tier)| {
let (label, uncertain) = strategy_label(solved[index].strategy);
let info = TierSolveInfo {
mast: format!("{:.4}", solved[index].mast),
mast_mm: mm_per_unit.map_or_else(String::new, |mm| {
format!("{:.3} mm", solved[index].mast * mm)
}),
mast_full: format!("{}", solved[index].mast),
strategy: label.to_string(),
strategy_is_uncertain: uncertain,
strategy_detail: solved[index].detail.clone(),
needs_anchor: false,
};
build_tier_row(design, index, tier, n_d, info, &row_context)
})
.collect()
}
const fn block_label(block: Block) -> &'static str {
match block {
Block::Crown => "Crown",
Block::Pavilion => "Pavilion",
Block::Girdle => "Girdle",
}
}
fn manufacturability_warnings_by_tier(
design: &Design,
solved: Option<&[SolvedTier]>,
) -> Vec<(usize, String)> {
indicatrix_cut_core::manufacturability::check_manufacturability_available(
design,
solved,
indicatrix_cut_core::manufacturability::DEFAULT_MIN_FACET_AREA_FRACTION_OF_W2,
)
.iter()
.map(|warning| (warning.tier_index(), warning.to_string()))
.collect()
}
pub(in crate::gui::editor) fn manufacturability_warnings_tagged(
design: &Design,
solved: Option<&[SolvedTier]>,
) -> Vec<(usize, String)> {
let pairs = manufacturability_warnings_by_tier(design, solved);
if solved.is_some() {
return pairs;
}
pairs
.into_iter()
.map(|(index, text)| (index, format!("(pre-solve) {text}")))
.collect()
}
fn warning_text_by_tier(pairs: &[(usize, String)]) -> BTreeMap<usize, String> {
let mut grouped: BTreeMap<usize, Vec<String>> = BTreeMap::new();
for (tier_index, text) in pairs {
grouped.entry(*tier_index).or_default().push(text.clone());
}
grouped
.into_iter()
.map(|(index, texts)| (index, texts.join("; ")))
.collect()
}