use crate::{
MainWindow, SettingsModel, ViewportModel,
bridge::render_thread::{RenderContext, resolve_material},
gui::{
is_c_axis_override_available,
optics::crystal_optics::{
crystal_system_from_index, is_biaxial, optical_character_from_index, save_gem_material,
},
refresh_material_options, show_toast,
},
};
use indicatrix::optics::materials::GemMaterial;
use indicatrix_vault::db::sqlite::Database;
use slint::{ComponentHandle, SharedString};
use std::sync::{Arc, Mutex};
fn built_in_name_collision(name: &str) -> Option<String> {
GemMaterial::all_materials()
.into_iter()
.map(|m| m.name)
.find(|builtin| builtin.eq_ignore_ascii_case(name))
}
struct CustomMaterialForm {
name: SharedString,
ri: f32,
disp: f32,
biref: f32,
color_idx: i32,
crystal_system_idx: i32,
optical_character_idx: i32,
biaxial_delta_beta_alpha: f32,
apply_to_live_render: bool,
}
struct SavedCustomMaterial {
trimmed_name: String,
overwrote_existing: bool,
c_axis_available: Option<bool>,
custom_list: Arc<Vec<GemMaterial>>,
applied_to_live_render: bool,
}
fn apply_custom_material_save(
db: &Arc<Mutex<Database>>,
render_ctx: &Arc<Mutex<RenderContext>>,
form: CustomMaterialForm,
) -> Result<SavedCustomMaterial, String> {
let CustomMaterialForm {
name,
ri,
disp,
biref,
color_idx,
crystal_system_idx,
optical_character_idx,
biaxial_delta_beta_alpha,
apply_to_live_render,
} = form;
let abs_rgb = match color_idx {
1 => [2.8f32, 1.2, 0.1], 2 => [0.1f32, 2.5, 2.2], 3 => [2.2f32, 0.2, 2.0], 4 => [1.8f32, 1.6, 0.2], 5 => [0.2f32, 0.4, 2.8], 6 => [0.4f32, 2.2, 1.6], 7 => [0.2f32, 0.6, 1.8], 8 => [1.2f32, 0.4, 0.1], _ => [0.0f32, 0.0, 0.0], };
let trimmed = name.trim().to_string();
if trimmed.is_empty() {
return Err("A material name is required.".to_string());
}
if let Some(builtin) = built_in_name_collision(&trimmed) {
return Err(format!(
"'{trimmed}' is a built-in material name -- saving a custom \
material under it would silently shadow '{builtin}' everywhere \
this design's material is used. Rename it first."
));
}
let mut new_mat = GemMaterial::new_custom(&trimmed, ri, disp, biref, abs_rgb);
if let Some(cs) = crystal_system_from_index(crystal_system_idx) {
new_mat.crystal_system = cs;
}
if let Some(oc) = optical_character_from_index(optical_character_idx) {
new_mat.optical_character = oc;
}
new_mat.biaxial_delta_beta_alpha =
is_biaxial(new_mat.optical_character).then_some(biaxial_delta_beta_alpha);
let save_result = save_gem_material(
&db.lock().unwrap(),
&new_mat,
ri,
disp,
biref,
abs_rgb,
None,
);
if let Err(e) = save_result {
return Err(format!("Could not save '{trimmed}': {e}"));
}
let c_axis_available = apply_to_live_render.then(|| is_c_axis_override_available(&new_mat));
let mut ctx = render_ctx.lock().unwrap();
let materials = Arc::make_mut(&mut ctx.custom_materials);
let overwrote_existing = materials
.iter()
.any(|m| m.name.eq_ignore_ascii_case(&trimmed));
if let Some(pos) = materials
.iter()
.position(|m| m.name.eq_ignore_ascii_case(&trimmed))
{
materials[pos] = new_mat;
} else {
materials.push(new_mat);
}
if apply_to_live_render {
ctx.material_name.clone_from(&trimmed);
ctx.dirty = true;
}
let custom_list = ctx.custom_materials.clone();
drop(ctx);
Ok(SavedCustomMaterial {
trimmed_name: trimmed,
overwrote_existing,
c_axis_available,
custom_list,
applied_to_live_render: apply_to_live_render,
})
}
struct DeletedCustomMaterial {
existed: bool,
c_axis_available: bool,
custom_list: Arc<Vec<GemMaterial>>,
}
fn apply_custom_material_delete(
db: &Arc<Mutex<Database>>,
render_ctx: &Arc<Mutex<RenderContext>>,
name: &str,
) -> Result<DeletedCustomMaterial, String> {
if name.is_empty() {
return Err("No material name given to delete.".to_string());
}
let delete_result = db.lock().unwrap().delete_custom_material(name);
if let Err(e) = delete_result {
return Err(format!("Could not delete '{name}': {e}"));
}
let mut ctx = render_ctx.lock().unwrap();
let materials = Arc::make_mut(&mut ctx.custom_materials);
let existed = materials.iter().any(|m| m.name.eq_ignore_ascii_case(name));
materials.retain(|m| !m.name.eq_ignore_ascii_case(name));
if ctx.material_name.eq_ignore_ascii_case(name) {
ctx.material_name = "Diamond".to_string();
}
ctx.dirty = true;
let c_axis_available = is_c_axis_override_available(&resolve_material(
&GemMaterial::all_materials(),
&ctx.custom_materials,
&ctx.material_name,
));
let custom_list = ctx.custom_materials.clone();
drop(ctx);
Ok(DeletedCustomMaterial {
existed,
c_axis_available,
custom_list,
})
}
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);
});
}
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);
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,
apply_to_live_render: bool| {
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,
apply_to_live_render,
},
);
let Some(ui) = ui_weak_save.upgrade() else {
return;
};
match result {
Ok(outcome) => {
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);
}
let verb = if outcome.overwrote_existing {
"Overwrote"
} else {
"Saved"
};
let suffix = if outcome.applied_to_live_render {
" and applied"
} else {
""
};
show_toast(
&ui,
&format!("{verb}{suffix} custom material '{}'", outcome.trimmed_name),
"success",
);
}
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"),
}
});
}