use crate::{
EditorModel, LibraryModel, LightingPresetItem, MainWindow, RemoteWorkerModel, SettingsModel,
SolidPreviewModel, ViewportModel,
bridge::render_thread::{RenderContext, load_env_map, resolve_material},
gui::{
optics::c_axis::angles_to_c_axis,
remote::{live_compute_target_index, remote_samples_count_to_exponent},
render::sample_scale::count_to_exponent,
show_toast,
},
settings::{
LightingPreset as SavedLightingPreset, LocalComputeTarget, LocalPreviewScale, SettingsFile,
},
};
use indicatrix::{
color::ColorSpace,
optics::{
LightingPreset,
materials::{GemMaterial, OpticalCharacter},
},
};
use slint::{ComponentHandle, Model, ModelRc, SharedString, VecModel};
use std::sync::{Arc, Mutex};
pub(super) fn apply_loaded_settings(
ui: &MainWindow,
render_ctx: &Arc<Mutex<RenderContext>>,
loaded: &SettingsFile,
) {
let s = &loaded.settings;
let lighting_preset = LightingPreset::from_label(&s.lighting_rig);
apply_loaded_render_context(render_ctx, s, lighting_preset);
apply_loaded_ui_mirrors(ui, s, lighting_preset);
if let Some(idx) = find_option_index(
&ui.global::<ViewportModel>().get_material_options(),
&s.selected_material,
) {
ui.global::<ViewportModel>()
.set_selected_material_index(idx);
}
let starting_material = resolve_material(
&GemMaterial::all_materials(),
&render_ctx.lock().unwrap().custom_materials,
&s.selected_material,
);
ui.global::<SettingsModel>()
.set_c_axis_override_available(is_c_axis_override_available(&starting_material));
let ri_default = crate::bridge::preview_render::material_ri_at_sodium_d(&starting_material);
ui.global::<LibraryModel>()
.set_ri_material_default(ri_default as f32);
ui.global::<LibraryModel>()
.set_ri_tolerance_center(ri_default as f32);
ui.global::<SettingsModel>()
.set_env_map_path(s.env_map_path.clone().into());
if s.env_map_path.is_empty() {
ui.global::<SettingsModel>().set_env_map_loaded(false);
ui.global::<SettingsModel>()
.set_env_map_status(String::new().into());
} else {
match load_env_map(&s.env_map_path) {
Ok(map) => {
ui.global::<SettingsModel>()
.set_env_map_status(env_map_status_text(&map, &s.env_map_path).into());
ui.global::<SettingsModel>().set_env_map_loaded(true);
render_ctx.lock().unwrap().env_map = Some(map);
}
Err(err) => {
ui.global::<SettingsModel>().set_env_map_loaded(false);
ui.global::<SettingsModel>()
.set_env_map_status(String::new().into());
show_toast(
ui,
&format!("Could not reload saved HDR environment: {err}"),
"error",
);
}
}
}
}
fn apply_loaded_render_context(
render_ctx: &Arc<Mutex<RenderContext>>,
s: &crate::settings::model::AppSettings,
lighting_preset: LightingPreset,
) {
let mut ctx = render_ctx.lock().unwrap();
ctx.target_samples = s.target_samples;
ctx.width = s.render_width;
ctx.height = s.render_height;
ctx.max_bounces = s.max_bounces;
ctx.exposure = s.exposure;
ctx.inclusion_sigma_s = s.inclusion_sigma_s;
ctx.c_axis_override = s
.c_axis_override_enabled
.then(|| angles_to_c_axis(s.c_axis_tilt_deg, s.c_axis_azimuth_deg));
ctx.girdle_frosted = s.girdle_frosted;
ctx.edge_rounding_radius = s.edge_rounding_radius;
ctx.stone_width_mm = s.stone_width_mm;
ctx.light_yaw = s.light_yaw_deg.to_radians();
ctx.light_pitch = s.light_pitch_deg.to_radians().clamp(0.15, 1.55);
ctx.lighting_preset = lighting_preset;
ctx.yaw = s.camera_yaw;
ctx.pitch = crate::gui::render::camera_lighting::wrap_pitch(s.camera_pitch);
ctx.distance = s.camera_distance.clamp(1.2, 8.0);
ctx.material_name.clone_from(&s.selected_material);
ctx.denoise_enabled = s.denoise_enabled;
ctx.backdrop = s.backdrop;
ctx.local_preview_scale = s.local_preview_scale;
ctx.remote_render_samples = s.remote_render_samples;
ctx.live_compute_target = s.live_compute_target;
ctx.local_compute_target = s.local_compute_target;
ctx.dirty = true;
}
fn apply_loaded_ui_mirrors(
ui: &MainWindow,
s: &crate::settings::model::AppSettings,
lighting_preset: LightingPreset,
) {
ui.global::<SettingsModel>()
.set_target_samples_exponent(count_to_exponent(s.target_samples) as f32);
ui.global::<SettingsModel>()
.set_resolution_index(resolution_index(s.render_width, s.render_height));
ui.global::<SettingsModel>()
.set_preview_size_val(s.preview_size as f32);
ui.global::<SettingsModel>()
.set_preview_spp_val(s.preview_spp as f32);
ui.global::<SettingsModel>()
.set_bounce_index(bounces_index(s.max_bounces));
ui.global::<SettingsModel>().set_exposure_val(s.exposure);
ui.global::<SettingsModel>()
.set_backdrop_index(s.backdrop.index());
ui.global::<SettingsModel>()
.set_inclusion_sigma_s(s.inclusion_sigma_s);
ui.global::<SettingsModel>()
.set_c_axis_override_enabled(s.c_axis_override_enabled);
ui.global::<SettingsModel>()
.set_c_axis_tilt_deg(s.c_axis_tilt_deg);
ui.global::<SettingsModel>()
.set_c_axis_azimuth_deg(s.c_axis_azimuth_deg);
ui.global::<SettingsModel>()
.set_girdle_frosted(s.girdle_frosted);
ui.global::<SettingsModel>()
.set_edge_rounding_radius(s.edge_rounding_radius);
ui.global::<SettingsModel>()
.set_stone_width_mm(s.stone_width_mm);
ui.global::<SettingsModel>()
.set_local_preview_scale_index(local_preview_scale_index(s.local_preview_scale));
ui.global::<SettingsModel>()
.set_live_compute_target_index(live_compute_target_index(s.live_compute_target));
ui.global::<SettingsModel>()
.set_local_compute_target_index(local_compute_target_index(s.local_compute_target));
ui.global::<RemoteWorkerModel>()
.set_render_samples_exponent(
remote_samples_count_to_exponent(s.remote_render_samples) as f32
);
ui.global::<SettingsModel>()
.set_light_yaw_deg(s.light_yaw_deg);
ui.global::<SettingsModel>()
.set_light_pitch_deg(s.light_pitch_deg);
ui.global::<LibraryModel>()
.set_panel_collapsed(s.library_panel_collapsed);
ui.global::<SolidPreviewModel>()
.set_view_mode(i32::from(s.solid_view_mode));
ui.global::<ViewportModel>()
.set_live_view_mode(i32::from(s.live_view_mode));
ui.global::<EditorModel>()
.set_auto_solve_budget_ms(i32::try_from(s.editor_auto_solve_budget_ms).unwrap_or(i32::MAX));
ui.global::<ViewportModel>()
.set_selected_lighting_index(lighting_preset.index());
ui.set_recent_native_files(ModelRc::new(VecModel::from(
s.recent_native_files
.iter()
.cloned()
.map(SharedString::from)
.collect::<Vec<_>>(),
)));
}
pub(in crate::gui) fn env_map_status_text(
map: &indicatrix::renderer::env_map::EnvironmentMap,
path: &str,
) -> String {
let name = std::path::Path::new(path)
.file_name()
.map_or_else(|| path.to_string(), |n| n.to_string_lossy().into_owned());
format!("Loaded: {name} ({}\u{d7}{})", map.width(), map.height())
}
pub(in crate::gui) fn is_c_axis_override_available(material: &GemMaterial) -> bool {
material.optical_character != OpticalCharacter::Isotropic
}
const fn bounces_index(bounces: u32) -> i32 {
const RUNGS: [u32; 6] = [4, 8, 12, 24, 64, 128];
let mut best_idx = 0usize;
let mut best_dist = u32::MAX;
let mut i = 0usize;
while i < RUNGS.len() {
let dist = RUNGS[i].abs_diff(bounces);
if dist < best_dist {
best_dist = dist;
best_idx = i;
}
i += 1;
}
best_idx as i32
}
const fn resolution_index(width: u32, height: u32) -> i32 {
match (width, height) {
(640, 480) => 0,
(1280, 720) => 2,
(1920, 1080) => 3,
_ => 1, }
}
const fn local_preview_scale_index(scale: LocalPreviewScale) -> i32 {
match scale {
LocalPreviewScale::Off => 0,
LocalPreviewScale::Half => 1,
LocalPreviewScale::Quarter => 2,
}
}
pub(in crate::gui) const fn local_preview_scale_from_index(index: i32) -> LocalPreviewScale {
match index {
1 => LocalPreviewScale::Half,
2 => LocalPreviewScale::Quarter,
_ => LocalPreviewScale::Off,
}
}
const fn local_compute_target_index(target: LocalComputeTarget) -> i32 {
match target {
LocalComputeTarget::Cpu => 0,
LocalComputeTarget::CpuGpu => 1,
LocalComputeTarget::Gpu => 2,
}
}
pub(in crate::gui) const fn local_compute_target_from_index(index: i32) -> LocalComputeTarget {
match index {
0 => LocalComputeTarget::Cpu,
2 => LocalComputeTarget::Gpu,
_ => LocalComputeTarget::CpuGpu,
}
}
#[must_use]
pub(in crate::gui) fn find_option_index(
options: &ModelRc<SharedString>,
needle: &str,
) -> Option<i32> {
(0..options.row_count()).find_map(|i| {
let matches = options.row_data(i).is_some_and(|s| s.as_str() == needle);
matches.then_some(i as i32)
})
}
pub(in crate::gui) fn refresh_lighting_preset_options(
ui: &MainWindow,
presets: &[SavedLightingPreset],
) {
let items: Vec<LightingPresetItem> = presets
.iter()
.map(|p| LightingPresetItem {
name: p.name.clone().into(),
built_in: p.built_in,
export_usable: p.export_usable,
has_env_map: p.env_map_path.is_some(),
selected: false,
})
.collect();
ui.global::<ViewportModel>()
.set_lighting_presets(ModelRc::new(VecModel::from(items)));
}
pub(in crate::gui) const fn color_space_from_index(index: i32) -> ColorSpace {
match index {
1 => ColorSpace::DisplayP3,
2 => ColorSpace::Rec2020,
_ => ColorSpace::Srgb,
}
}
fn built_in_material_option_names() -> Vec<String> {
let mut names: Vec<String> = GemMaterial::all_materials()
.into_iter()
.map(|m| m.name)
.collect();
names.sort_by_key(|a| a.to_ascii_lowercase());
names
}
pub(in crate::gui) fn refresh_material_options(ui: &MainWindow, custom_mats: &[GemMaterial]) {
let mut names = built_in_material_option_names();
for m in custom_mats {
if !names.iter().any(|n| n.eq_ignore_ascii_case(&m.name)) {
names.push(m.name.clone());
}
}
let model: Vec<SharedString> = names.into_iter().map(std::convert::Into::into).collect();
ui.global::<ViewportModel>()
.set_material_options(std::rc::Rc::new(slint::VecModel::from(model)).into());
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bounces_index_round_trips_all_six_pill_values() {
assert_eq!(bounces_index(4), 0);
assert_eq!(bounces_index(8), 1);
assert_eq!(bounces_index(12), 2);
assert_eq!(bounces_index(24), 3);
assert_eq!(bounces_index(64), 4);
assert_eq!(bounces_index(128), 5);
}
#[test]
fn bounces_index_picks_the_nearest_rung_for_unknown_values() {
assert_eq!(bounces_index(16), 2);
assert_eq!(bounces_index(1), 0);
assert_eq!(bounces_index(999), 5);
assert_eq!(bounces_index(44), 3);
}
#[test]
fn resolution_index_round_trips_all_four_pill_values() {
assert_eq!(resolution_index(640, 480), 0);
assert_eq!(resolution_index(800, 600), 1);
assert_eq!(resolution_index(1280, 720), 2);
assert_eq!(resolution_index(1920, 1080), 3);
}
#[test]
fn resolution_index_falls_back_to_800x600_for_unknown_values() {
assert_eq!(resolution_index(1, 1), 1);
assert_eq!(resolution_index(3840, 2160), 1);
assert_eq!(resolution_index(640, 600), 1);
}
#[test]
fn local_preview_scale_index_round_trips_all_three_pill_values() {
for scale in [
LocalPreviewScale::Off,
LocalPreviewScale::Half,
LocalPreviewScale::Quarter,
] {
let idx = local_preview_scale_index(scale);
assert_eq!(
local_preview_scale_from_index(idx),
scale,
"scale={scale:?}"
);
}
assert_eq!(local_preview_scale_index(LocalPreviewScale::Off), 0);
assert_eq!(local_preview_scale_index(LocalPreviewScale::Half), 1);
assert_eq!(local_preview_scale_index(LocalPreviewScale::Quarter), 2);
}
#[test]
fn local_preview_scale_from_index_falls_back_to_off_for_unknown_values() {
assert_eq!(local_preview_scale_from_index(-1), LocalPreviewScale::Off);
assert_eq!(local_preview_scale_from_index(99), LocalPreviewScale::Off);
}
#[test]
fn local_compute_target_index_round_trips_all_three_pill_values() {
for target in [
LocalComputeTarget::Cpu,
LocalComputeTarget::CpuGpu,
LocalComputeTarget::Gpu,
] {
let idx = local_compute_target_index(target);
assert_eq!(
local_compute_target_from_index(idx),
target,
"target={target:?}"
);
}
assert_eq!(local_compute_target_index(LocalComputeTarget::Cpu), 0);
assert_eq!(local_compute_target_index(LocalComputeTarget::CpuGpu), 1);
assert_eq!(local_compute_target_index(LocalComputeTarget::Gpu), 2);
}
#[test]
fn local_compute_target_from_index_falls_back_to_cpu_gpu_for_unknown_values() {
assert_eq!(
local_compute_target_from_index(-1),
LocalComputeTarget::CpuGpu
);
assert_eq!(
local_compute_target_from_index(99),
LocalComputeTarget::CpuGpu
);
}
#[test]
fn color_space_from_index_maps_all_three_pill_values() {
assert_eq!(color_space_from_index(0), ColorSpace::Srgb);
assert_eq!(color_space_from_index(1), ColorSpace::DisplayP3);
assert_eq!(color_space_from_index(2), ColorSpace::Rec2020);
}
#[test]
fn color_space_from_index_falls_back_to_srgb_for_unknown_values() {
assert_eq!(color_space_from_index(-1), ColorSpace::Srgb);
assert_eq!(color_space_from_index(99), ColorSpace::Srgb);
}
#[test]
fn find_option_index_locates_an_exact_match() {
let options: ModelRc<SharedString> = ModelRc::new(VecModel::from(vec![
SharedString::from("Diamond"),
SharedString::from("Sapphire"),
SharedString::from("Ruby"),
]));
assert_eq!(find_option_index(&options, "Sapphire"), Some(1));
assert_eq!(find_option_index(&options, "Ruby"), Some(2));
}
#[test]
fn find_option_index_returns_none_when_absent_rather_than_guessing() {
let options: ModelRc<SharedString> =
ModelRc::new(VecModel::from(vec![SharedString::from("Diamond")]));
assert_eq!(find_option_index(&options, "Moissanite"), None);
}
#[test]
fn find_option_index_on_an_empty_model_returns_none() {
let options: ModelRc<SharedString> =
ModelRc::new(VecModel::from(Vec::<SharedString>::new()));
assert_eq!(find_option_index(&options, "anything"), None);
}
}