use super::state::{
EditorState, ScratchDelta, builtin_preset_names, design_material_index_from_name,
gear_index_from_teeth, index_chip_items, push_rows, ri_source_text,
};
use crate::{
EditorModel, IndexChipItem, MainWindow, ViewportModel,
bridge::render_thread::RenderContext,
gui::{
editor::{
body_color_combo,
material_lookup::{
EditorMaterialLookup, material_guess, traced_gem_material, traced_material_for,
},
},
render::render_body_color::display_rgb_for_stone,
},
};
use indicatrix::optics::materials::GemMaterial;
use indicatrix_cut_core::{Design, critical_angle_deg};
use indicatrix_editor::material::{body_color_index_for_material, body_color_options};
use slint::{ComponentHandle, Model, ModelRc, SharedString, VecModel};
use std::sync::{Arc, Mutex};
fn push_material_guess(ui: &MainWindow, design: &Design, n_d: f64) {
let (text, name, others) = material_guess(design, n_d).unwrap_or_default();
ui.global::<EditorModel>()
.set_material_guess_text(text.into());
ui.global::<EditorModel>()
.set_material_guess_name(name.into());
ui.global::<EditorModel>()
.set_material_guess_other_candidates_text(others.into());
}
fn body_color_readout(design: &Design, custom: &[GemMaterial], stone_width_mm: f32) -> String {
let rgb = design.material.body_color_override;
design
.material
.body_color_label()
.map_or_else(String::new, |color| {
let (name, unresolved) = traced_material_for(design, custom);
if unresolved.is_some() {
String::new()
} else if let Some(rgb) = rgb.filter(|rgb| body_color_combo::is_custom(Some(*rgb))) {
let [r, g, b] = display_rgb_for_stone(rgb, stone_width_mm);
format!("{name} (Custom #{r:02x}{g:02x}{b:02x})")
} else {
format!("{name} ({color})")
}
})
}
fn is_physics_material(design: &Design, ctx: &RenderContext) -> bool {
design.material.name.as_deref().is_some_and(|name| {
ctx.custom_material_physics
.iter()
.any(|n| n.eq_ignore_ascii_case(name))
})
}
fn push_body_color_fields(
ui: &MainWindow,
design: &Design,
readout: String,
physics_material: bool,
material_changed: bool,
) {
let model = ui.global::<EditorModel>();
let options = body_color_options();
if model.get_body_color_options().row_count() != options.len() {
model.set_body_color_options(ModelRc::new(VecModel::from(
options
.into_iter()
.map(SharedString::from)
.collect::<Vec<_>>(),
)));
}
if material_changed {
model.set_body_color_index(body_color_index_for_material(&design.material));
}
model.set_body_color_readout(readout.into());
model.set_design_material_is_physics(physics_material);
model.set_physics_override_conflict(
physics_material && design.material.body_color_override.is_some(),
);
}
pub(super) fn refresh_design_settings(
ui: &MainWindow,
render_ctx: &Arc<Mutex<RenderContext>>,
state: &EditorState,
delta: &ScratchDelta,
) -> f64 {
let design = &state.design;
let (n_d, options, color_readout, physics_material) = {
let ctx = render_ctx
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let color_readout = body_color_readout(design, &ctx.custom_materials, ctx.stone_width_mm);
let n_d = design.effective_refractive_index_with(&ctx.custom_materials);
ui.global::<EditorModel>()
.set_ri_source_text(ri_source_text(&design.material, &ctx.custom_materials).into());
let options = state.material_combo_options(&ctx.custom_materials);
let physics_material = is_physics_material(design, &ctx);
drop(ctx);
(n_d, options, color_readout, physics_material)
};
push_body_color_fields(ui, design, color_readout, physics_material, delta.material);
let selected_material_index = {
let mut ctx = render_ctx
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
sync_viewport_material_link(ui, &mut ctx, design)
};
if let Some(idx) = selected_material_index {
ui.global::<ViewportModel>()
.set_selected_material_index(idx);
}
let current_options = ui.global::<EditorModel>().get_material_combo_options();
let unchanged = current_options.row_count() == options.len()
&& current_options
.iter()
.zip(options.iter())
.all(|(current, new)| current == new.as_str());
if !unchanged {
ui.global::<EditorModel>()
.set_material_combo_options(ModelRc::new(VecModel::from(
options
.iter()
.cloned()
.map(SharedString::from)
.collect::<Vec<_>>(),
)));
}
ui.global::<EditorModel>()
.set_new_material_options(ModelRc::new(VecModel::from(
builtin_preset_names()
.into_iter()
.map(SharedString::from)
.collect::<Vec<_>>(),
)));
if delta.material {
ui.global::<EditorModel>()
.set_material_combo_index(design_material_index_from_name(
design.material.name.as_deref(),
&options,
));
ui.global::<EditorModel>().set_ri_override_text(
design
.material
.refractive_index_override
.map_or_else(String::new, |v| format!("{v:.4}"))
.into(),
);
}
ui.global::<EditorModel>()
.set_effective_ri_text(format!("{n_d:.4}").into());
ui.global::<EditorModel>()
.set_critical_angle_text(format!("{:.2}\u{b0}", critical_angle_deg(n_d)).into());
push_material_guess(ui, design, n_d);
if delta.gear {
ui.global::<EditorModel>()
.set_gear_index(gear_index_from_teeth(design.meta.gear_teeth));
ui.global::<EditorModel>()
.set_gear_custom_text(design.meta.gear_teeth.to_string().into());
}
if delta.symmetry {
ui.global::<EditorModel>()
.set_symmetry_order_text(design.meta.symmetry_order.to_string().into());
ui.global::<EditorModel>().set_mirror(design.meta.mirror);
ui.global::<EditorModel>()
.set_symmetry_preview_text("".into());
}
if delta.meta {
let (title, extra_headers) = design
.meta
.headers
.split_first()
.map_or((String::new(), String::new()), |(title, rest)| {
(title.clone(), rest.join(";"))
});
ui.global::<EditorModel>().set_design_title(title.into());
ui.global::<EditorModel>()
.set_design_extra_headers(extra_headers.into());
ui.global::<EditorModel>()
.set_design_footnotes(design.meta.footnotes.join(";").into());
ui.global::<EditorModel>().set_design_gear_reference_angle(
format!("{}", design.meta.gear_reference_angle).into(),
);
}
n_d
}
#[must_use]
pub(in crate::gui::editor) fn sync_viewport_material_link(
ui: &MainWindow,
ctx: &mut RenderContext,
design: &Design,
) -> Option<i32> {
if !ui.global::<ViewportModel>().get_viewport_material_linked() {
return None;
}
let (name, unresolved) = traced_material_for(design, &ctx.custom_materials);
if ctx.material_unresolved != unresolved {
ctx.material_unresolved.clone_from(&unresolved);
ctx.dirty = true;
}
let resolved = unresolved
.is_none()
.then(|| {
traced_gem_material(
&name,
&design.material,
&EditorMaterialLookup::new(&ctx.custom_materials),
)
})
.flatten();
if ctx.material_override != resolved {
ctx.material_override = resolved;
ctx.dirty = true;
}
if ctx.material_name != name {
ctx.material_name.clone_from(&name);
ctx.dirty = true;
}
let options = ui.global::<ViewportModel>().get_material_options();
let selected_material_index = crate::gui::startup_settings::find_option_index(&options, &name);
let stone_width_mm = design.girdle_diameter_mm.unwrap_or(0.0) as f32;
if (ctx.stone_width_mm - stone_width_mm).abs() > f32::EPSILON {
ctx.stone_width_mm = stone_width_mm;
ctx.dirty = true;
}
selected_material_index
}
#[must_use]
fn selected_tier_chips(design: &Design, selected_tier_index: i32) -> Vec<IndexChipItem> {
usize::try_from(selected_tier_index)
.ok()
.and_then(|idx| design.tiers.get(idx))
.map_or_else(Vec::new, |tier| {
index_chip_items(&tier.indices, &tier.detached)
})
}
pub(in crate::gui::editor) fn push_selected_tier_chips(ui: &MainWindow, state: &EditorState) {
let selected = ui.global::<EditorModel>().get_selected_tier_index();
let cheater_offset_text = usize::try_from(selected)
.ok()
.and_then(|index| state.design.cheater_offset_deg(index))
.map_or_else(String::new, |deg| format!("{deg:.2}"));
ui.global::<EditorModel>()
.set_selected_tier_cheater_offset_text(cheater_offset_text.into());
let note_text = usize::try_from(selected)
.ok()
.and_then(|index| state.design.tier_note(index))
.map_or_else(String::new, str::to_string);
ui.global::<EditorModel>()
.set_selected_tier_note_text(note_text.into());
let chips = selected_tier_chips(&state.design, selected);
push_rows(
&ui.global::<EditorModel>().get_selected_tier_chips(),
chips,
|model| ui.global::<EditorModel>().set_selected_tier_chips(model),
);
}
#[cfg(test)]
mod tests {
use super::*;
use indicatrix::geometry::meet_solver::MeetConstraint;
use indicatrix_cut_core::ConstraintTier;
fn design_with_named_tiers(names: &[&str]) -> Design {
let mut state = EditorState::fresh();
for &name in names {
state.design.tiers.push(ConstraintTier {
angle_deg: -40.0,
name: name.to_string(),
indices: vec![0.0],
constraint: MeetConstraint::MeetExisting,
imported_meet: None,
original_notes: None,
detached: Vec::new(),
});
}
state.session.design
}
#[test]
fn selected_tier_chips_is_empty_when_nothing_is_selected() {
let design = design_with_named_tiers(&["G1"]);
assert_eq!(selected_tier_chips(&design, -1).len(), 0);
}
#[test]
fn selected_tier_chips_is_empty_for_an_out_of_range_index() {
let design = design_with_named_tiers(&["G1"]);
assert_eq!(selected_tier_chips(&design, 5).len(), 0);
}
#[test]
fn selected_tier_chips_reads_the_selected_tiers_own_indices_and_detached_set() {
let mut design = design_with_named_tiers(&["G1", "P1"]);
design.tiers[1].indices = vec![0.0, 24.0];
design.tiers[1].detached = vec![24.0];
let chips = selected_tier_chips(&design, 1);
assert_eq!(chips.len(), 2);
assert!(!chips[0].detached);
assert!(chips[1].detached);
}
}