mod save;
use super::{
dialog_color::{self, DialogColor},
dispersion_editor::{prefill_for, push_prefill, setup_dispersion_callbacks},
};
use crate::{
DispersionEditorModel, MainWindow, SettingsModel, ViewportModel,
bridge::render_thread::RenderContext,
gui::{
optics::crystal_optics::{
crystal_system_to_index, gem_material_from_row, optical_character_to_index,
},
refresh_material_options, show_toast,
tutorial_events::raise,
},
};
use indicatrix::optics::chromophore::ChromophoreCatalogue;
use indicatrix_cut_core::{
material::{ColorMode, absorption_bands_from_json},
native::dispersion_model_from_json,
};
use indicatrix_editor::guide::viewing_events as events;
use indicatrix_vault::db::sqlite::Database;
use slint::{ComponentHandle, Model, ModelRc, SharedString, VecModel};
use std::sync::{Arc, Mutex};
pub(super) use save::{
CUSTOM_KEEP_COLOR_INDEX, absorption_rgb_for_color_index, color_index_for_absorption_rgb,
};
use save::{
CustomMaterialForm, SavedCustomMaterial, apply_custom_material_delete,
apply_custom_material_save, built_in_name_collision,
};
fn setup_material_name_probe(ui: &MainWindow, render_ctx: &Arc<Mutex<RenderContext>>) {
let render_ctx_probe = render_ctx.clone();
let ui_weak = ui.as_weak();
ui.global::<ViewportModel>()
.on_material_name_edited(move |name: SharedString| {
let Some(ui) = ui_weak.upgrade() else {
return;
};
let trimmed = name.trim();
let collides_builtin = built_in_name_collision(trimmed).is_some();
let is_existing_custom = {
let ctx = render_ctx_probe
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
ctx.custom_materials
.iter()
.any(|m| m.name.eq_ignore_ascii_case(trimmed))
};
let model = ui.global::<ViewportModel>();
model.set_material_name_collides_with_builtin(collides_builtin);
model.set_material_name_is_existing_custom(is_existing_custom);
});
}
fn push_selected_custom_material_fields(
ui: &MainWindow,
db: &Arc<Mutex<Database>>,
selected_name: &str,
) {
let row = db
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.get_custom_materials()
.unwrap_or_default()
.into_iter()
.find(|r| r.name.eq_ignore_ascii_case(selected_name));
let model = ui.global::<ViewportModel>();
let Some(row) = row else {
dialog_color::set(DialogColor::default());
model.set_selected_custom_material_valid(false);
model.set_selected_material_color_outdated(false);
push_prefill(ui, &prefill_for(None));
return;
};
let material = gem_material_from_row(&row);
model.set_selected_custom_material_valid(true);
model.set_selected_custom_material_ri(row.refractive_index);
model.set_selected_custom_material_dispersion(row.dispersion);
model.set_selected_custom_material_birefringence(row.birefringence);
model.set_selected_custom_material_specific_gravity(row.specific_gravity.unwrap_or(0.0));
model.set_selected_custom_material_crystal_system_idx(crystal_system_to_index(
material.crystal_system,
));
model.set_selected_custom_material_optical_character_idx(optical_character_to_index(
material.optical_character,
));
model.set_selected_custom_material_biaxial_delta_beta_alpha(
material.biaxial_delta_beta_alpha.unwrap_or(0.0),
);
let mode = row
.color_recipe_json
.as_deref()
.and_then(ColorMode::from_json);
let fantasy_rgb = mode.as_ref().map_or(row.absorption_rgb, |m| m.fantasy_rgb);
model.set_selected_custom_material_color_idx(
color_index_for_absorption_rgb(fantasy_rgb).unwrap_or(CUSTOM_KEEP_COLOR_INDEX),
);
model.set_selected_custom_material_rgb(ModelRc::new(VecModel::from(fantasy_rgb.to_vec())));
dialog_color::set(DialogColor {
bands: row
.absorption_bands_json
.as_deref()
.and_then(absorption_bands_from_json),
triple: Some(fantasy_rgb),
});
model.set_selected_custom_material_color_recipe_json(
row.color_recipe_json
.as_deref()
.map(str::trim)
.filter(|_| mode.is_some())
.unwrap_or_default()
.into(),
);
model.set_selected_material_color_outdated(
mode.as_ref()
.is_some_and(|m| m.data_outdated(ChromophoreCatalogue::global())),
);
let stored_model = row
.dispersion_model_json
.as_deref()
.and_then(dispersion_model_from_json);
push_prefill(ui, &prefill_for(stored_model.as_ref()));
}
fn setup_selected_material_prefill_callback(ui: &MainWindow, db: &Arc<Mutex<Database>>) {
let db = Arc::clone(db);
let ui_weak = ui.as_weak();
ui.global::<ViewportModel>()
.on_selected_material_changed_for_editor_prefill(move || {
let Some(ui) = ui_weak.upgrade() else {
return;
};
let idx = usize::try_from(ui.global::<ViewportModel>().get_selected_material_index())
.unwrap_or(0);
let name = ui
.global::<ViewportModel>()
.get_material_options()
.row_data(idx)
.unwrap_or_default();
push_selected_custom_material_fields(&ui, &db, &name);
});
}
fn handle_saved_custom_material(ui: &MainWindow, outcome: &SavedCustomMaterial) {
refresh_material_options(ui, &outcome.custom_list);
if let Some(c_axis_available) = outcome.c_axis_available {
ui.global::<SettingsModel>()
.set_c_axis_override_available(c_axis_available);
}
if outcome.applied_to_live_render {
let viewport = ui.global::<ViewportModel>();
if viewport.get_viewport_material_linked() {
viewport.set_viewport_material_linked(false);
}
if let Some(index) = crate::gui::startup_settings::find_option_index(
&viewport.get_material_options(),
&outcome.trimmed_name,
) {
viewport.set_selected_material_index(index);
}
}
ui.global::<ViewportModel>()
.invoke_selected_material_changed_for_editor_prefill();
let verb = if outcome.overwrote_existing {
"Overwrote"
} else {
"Saved"
};
let suffix = if outcome.applied_to_live_render {
" and applied"
} else {
""
};
let sg_suffix = outcome
.specific_gravity
.map(|sg| format!(" (SG {sg:.2})"))
.unwrap_or_default();
show_toast(
ui,
&format!(
"{verb}{suffix} custom material '{}'{sg_suffix}",
outcome.trimmed_name
),
"success",
);
}
pub(in crate::gui) fn setup_custom_material_callbacks(
ui: &MainWindow,
render_ctx: &Arc<Mutex<RenderContext>>,
db: &Arc<Mutex<Database>>,
) {
setup_material_name_probe(ui, render_ctx);
setup_selected_material_prefill_callback(ui, db);
setup_dispersion_callbacks(ui);
let render_ctx_save = render_ctx.clone();
let db_save = db.clone();
let ui_weak_save = ui.as_weak();
ui.global::<ViewportModel>().on_save_custom_material(
move |name: SharedString,
ri: f32,
disp: f32,
biref: f32,
color_idx: i32,
crystal_system_idx: i32,
optical_character_idx: i32,
biaxial_delta_beta_alpha: f32,
specific_gravity: f32,
apply_to_live_render: bool,
color_recipe_json: SharedString| {
let dispersion_model_json = ui_weak_save
.upgrade()
.map(|ui| {
let section = ui.global::<DispersionEditorModel>();
let json = section.get_save_json();
section.set_save_json(SharedString::default());
json
})
.unwrap_or_default();
let typed_coefficients = !dispersion_model_json.trim().is_empty();
let result = apply_custom_material_save(
&db_save,
&render_ctx_save,
CustomMaterialForm {
name,
ri,
disp,
biref,
color_idx,
crystal_system_idx,
optical_character_idx,
biaxial_delta_beta_alpha,
specific_gravity,
apply_to_live_render,
color_recipe_json,
dispersion_model_json,
body_bands: dialog_color::current().bands,
},
);
let Some(ui) = ui_weak_save.upgrade() else {
return;
};
match result {
Ok(outcome) => {
handle_saved_custom_material(&ui, &outcome);
raise(&ui, events::CUSTOM_MATERIAL_SAVED);
if typed_coefficients {
raise(&ui, events::COEFFICIENT_MATERIAL_SAVED);
}
}
Err(e) => show_toast(&ui, &e, "error"),
}
},
);
let render_ctx_del = render_ctx.clone();
let db_del = db.clone();
let ui_weak_del = ui.as_weak();
ui.global::<ViewportModel>()
.on_delete_custom_material(move |name: SharedString| {
let trimmed = name.trim();
let result = apply_custom_material_delete(&db_del, &render_ctx_del, trimmed);
let Some(ui) = ui_weak_del.upgrade() else {
return;
};
match result {
Ok(outcome) => {
refresh_material_options(&ui, &outcome.custom_list);
ui.global::<SettingsModel>()
.set_c_axis_override_available(outcome.c_axis_available);
if outcome.existed {
show_toast(&ui, &format!("Deleted material '{trimmed}'"), "info");
} else {
show_toast(
&ui,
&format!(
"'{trimmed}' was not a saved custom material -- nothing to delete."
),
"error",
);
}
}
Err(e) => show_toast(&ui, &e, "error"),
}
});
super::physics_ui::setup_physics_callbacks(ui, render_ctx);
}
#[cfg(test)]
mod tests;