use super::{
local_compute::LocalComputeTarget,
worker::{LiveComputeTarget, LocalPreviewScale, WorkerSettings},
};
use crate::bridge::export_thread::DEFAULT_TEMPLATE as DEFAULT_EXPORT_FILENAME_TEMPLATE;
use serde::{Deserialize, Serialize};
pub const DEFAULT_TARGET_SAMPLES: u32 = 256;
pub const DEFAULT_RENDER_WIDTH: u32 = 800;
pub const DEFAULT_RENDER_HEIGHT: u32 = 600;
pub const DEFAULT_MAX_BOUNCES: u32 = 12;
pub const DEFAULT_EXPOSURE: f32 = 1.0;
pub const DEFAULT_INCLUSION_SIGMA_S: f32 = 0.0;
pub const DEFAULT_C_AXIS_OVERRIDE_ENABLED: bool = false;
pub const DEFAULT_C_AXIS_TILT_DEG: f32 = 0.0;
pub const DEFAULT_C_AXIS_AZIMUTH_DEG: f32 = 0.0;
pub const DEFAULT_GIRDLE_FROSTED: bool = false;
pub const DEFAULT_EDGE_ROUNDING_RADIUS: f32 = 0.0;
pub const DEFAULT_STONE_WIDTH_MM: f32 = 0.0;
pub const DEFAULT_LOCAL_PREVIEW_SCALE: LocalPreviewScale = LocalPreviewScale::Off;
pub const DEFAULT_REMOTE_RENDER_SAMPLES: u32 = 512;
pub const DEFAULT_LIVE_COMPUTE_TARGET: LiveComputeTarget = LiveComputeTarget::Both;
pub const DEFAULT_LOCAL_COMPUTE_TARGET: LocalComputeTarget = LocalComputeTarget::CpuGpu;
pub const DEFAULT_LIGHT_YAW_DEG: f32 = 48.0;
pub const DEFAULT_LIGHT_PITCH_DEG: f32 = 54.0;
pub const DEFAULT_LIGHTING_RIG: &str = "Light tent + black cards";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum Backdrop {
AsLit,
#[default]
GemRayGrey,
White,
}
impl Backdrop {
#[must_use]
pub const fn level(self) -> f32 {
match self {
Self::AsLit => 0.0,
Self::GemRayGrey => indicatrix::optics::raytracer::BACKDROP_GEMRAY_GREY,
Self::White => indicatrix::optics::raytracer::BACKDROP_WHITE,
}
}
#[must_use]
pub const fn index(self) -> i32 {
match self {
Self::AsLit => 0,
Self::GemRayGrey => 1,
Self::White => 2,
}
}
#[must_use]
pub const fn from_index(index: i32) -> Self {
match index {
0 => Self::AsLit,
2 => Self::White,
_ => Self::GemRayGrey,
}
}
}
pub const DEFAULT_CAMERA_YAW: f32 = 0.60;
pub const DEFAULT_CAMERA_PITCH: f32 = 0.45;
pub const DEFAULT_CAMERA_DISTANCE: f32 = 2.4;
pub const DEFAULT_MATERIAL: &str = "Diamond";
pub const DEFAULT_PREVIEW_SIZE: u32 = 160;
pub const DEFAULT_PREVIEW_SPP: u32 = 256;
pub const DEFAULT_SOLID_VIEW_MODE: u8 = 0;
pub const DEFAULT_LIVE_VIEW_MODE: u8 = 1;
pub const DEFAULT_EDITOR_AUTO_SOLVE_BUDGET_MS: u32 = 300;
pub const DEFAULT_EDITOR_DOCK_WIDTH: f32 = 640.0;
pub const DEFAULT_EDITOR_INSPECTOR_HEIGHT: f32 = 260.0;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct AppSettings {
pub target_samples: u32,
pub render_width: u32,
pub render_height: u32,
pub max_bounces: u32,
pub exposure: f32,
pub light_yaw_deg: f32,
pub light_pitch_deg: f32,
pub lighting_rig: String,
pub camera_yaw: f32,
pub camera_pitch: f32,
pub camera_distance: f32,
pub selected_material: String,
#[serde(default)]
pub remote_workers: Vec<WorkerSettings>,
#[serde(default = "default_denoise_enabled")]
pub denoise_enabled: bool,
#[serde(default)]
pub backdrop: Backdrop,
pub inclusion_sigma_s: f32,
pub c_axis_override_enabled: bool,
pub c_axis_tilt_deg: f32,
pub c_axis_azimuth_deg: f32,
pub girdle_frosted: bool,
pub edge_rounding_radius: f32,
pub stone_width_mm: f32,
pub local_preview_scale: LocalPreviewScale,
pub remote_render_samples: u32,
#[serde(default)]
pub live_compute_target: LiveComputeTarget,
#[serde(default)]
pub local_compute_target: LocalComputeTarget,
#[serde(default)]
pub env_map_path: String,
#[serde(default = "default_preview_size")]
pub preview_size: u32,
#[serde(default = "default_preview_spp")]
pub preview_spp: u32,
#[serde(default)]
pub export_directory: String,
#[serde(default)]
pub last_import_directory: String,
#[serde(default = "default_export_filename_template")]
pub export_filename_template: String,
#[serde(default)]
pub library_panel_collapsed: bool,
#[serde(default)]
pub solid_view_mode: u8,
#[serde(default = "default_live_view_mode")]
pub live_view_mode: u8,
#[serde(default = "default_editor_auto_solve_budget_ms")]
pub editor_auto_solve_budget_ms: u32,
#[serde(default = "default_editor_dock_width")]
pub editor_dock_width: f32,
#[serde(default = "default_editor_inspector_height")]
pub editor_inspector_height: f32,
#[serde(default)]
pub editor_settings_collapsed: bool,
#[serde(default)]
pub editor_inspector_collapsed: bool,
#[serde(default)]
pub editor_remap_collapsed: bool,
#[serde(default)]
pub editor_layout_touched: bool,
#[serde(default)]
pub recent_native_files: Vec<String>,
}
pub const MAX_RECENT_NATIVE_FILES: usize = 10;
const fn default_preview_size() -> u32 {
DEFAULT_PREVIEW_SIZE
}
const fn default_preview_spp() -> u32 {
DEFAULT_PREVIEW_SPP
}
const fn default_denoise_enabled() -> bool {
true
}
const fn default_editor_auto_solve_budget_ms() -> u32 {
DEFAULT_EDITOR_AUTO_SOLVE_BUDGET_MS
}
const fn default_live_view_mode() -> u8 {
DEFAULT_LIVE_VIEW_MODE
}
const fn default_editor_dock_width() -> f32 {
DEFAULT_EDITOR_DOCK_WIDTH
}
const fn default_editor_inspector_height() -> f32 {
DEFAULT_EDITOR_INSPECTOR_HEIGHT
}
fn default_export_filename_template() -> String {
DEFAULT_EXPORT_FILENAME_TEMPLATE.to_string()
}
impl Default for AppSettings {
fn default() -> Self {
Self {
target_samples: DEFAULT_TARGET_SAMPLES,
render_width: DEFAULT_RENDER_WIDTH,
render_height: DEFAULT_RENDER_HEIGHT,
max_bounces: DEFAULT_MAX_BOUNCES,
exposure: DEFAULT_EXPOSURE,
light_yaw_deg: DEFAULT_LIGHT_YAW_DEG,
light_pitch_deg: DEFAULT_LIGHT_PITCH_DEG,
lighting_rig: DEFAULT_LIGHTING_RIG.to_string(),
camera_yaw: DEFAULT_CAMERA_YAW,
camera_pitch: DEFAULT_CAMERA_PITCH,
camera_distance: DEFAULT_CAMERA_DISTANCE,
selected_material: DEFAULT_MATERIAL.to_string(),
remote_workers: Vec::new(),
denoise_enabled: true,
backdrop: Backdrop::default(),
inclusion_sigma_s: DEFAULT_INCLUSION_SIGMA_S,
c_axis_override_enabled: DEFAULT_C_AXIS_OVERRIDE_ENABLED,
c_axis_tilt_deg: DEFAULT_C_AXIS_TILT_DEG,
c_axis_azimuth_deg: DEFAULT_C_AXIS_AZIMUTH_DEG,
girdle_frosted: DEFAULT_GIRDLE_FROSTED,
edge_rounding_radius: DEFAULT_EDGE_ROUNDING_RADIUS,
stone_width_mm: DEFAULT_STONE_WIDTH_MM,
local_preview_scale: DEFAULT_LOCAL_PREVIEW_SCALE,
remote_render_samples: DEFAULT_REMOTE_RENDER_SAMPLES,
live_compute_target: DEFAULT_LIVE_COMPUTE_TARGET,
local_compute_target: DEFAULT_LOCAL_COMPUTE_TARGET,
env_map_path: String::new(),
preview_size: DEFAULT_PREVIEW_SIZE,
preview_spp: DEFAULT_PREVIEW_SPP,
export_directory: String::new(),
last_import_directory: String::new(),
export_filename_template: DEFAULT_EXPORT_FILENAME_TEMPLATE.to_string(),
library_panel_collapsed: false,
solid_view_mode: DEFAULT_SOLID_VIEW_MODE,
live_view_mode: DEFAULT_LIVE_VIEW_MODE,
editor_auto_solve_budget_ms: DEFAULT_EDITOR_AUTO_SOLVE_BUDGET_MS,
editor_dock_width: DEFAULT_EDITOR_DOCK_WIDTH,
editor_inspector_height: DEFAULT_EDITOR_INSPECTOR_HEIGHT,
editor_settings_collapsed: false,
editor_inspector_collapsed: false,
editor_remap_collapsed: false,
editor_layout_touched: false,
recent_native_files: Vec::new(),
}
}
}
impl AppSettings {
pub fn add_worker(&mut self, worker: WorkerSettings) {
self.remote_workers.push(worker);
}
pub fn update_worker(&mut self, index: usize, worker: WorkerSettings) -> Result<(), String> {
let slot = self
.remote_workers
.get_mut(index)
.ok_or_else(|| format!("No worker at index {index}."))?;
*slot = worker;
Ok(())
}
pub fn remove_worker(&mut self, index: usize) -> Result<(), String> {
if index >= self.remote_workers.len() {
return Err(format!("No worker at index {index}."));
}
self.remote_workers.remove(index);
Ok(())
}
pub fn record_recent_native_file(&mut self, path: String) {
self.recent_native_files
.retain(|existing| existing != &path);
self.recent_native_files.insert(0, path);
self.recent_native_files.truncate(MAX_RECENT_NATIVE_FILES);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn record_recent_native_file_inserts_at_the_front() {
let mut settings = AppSettings::default();
settings.record_recent_native_file("a.indicatrix.toml".to_string());
settings.record_recent_native_file("b.indicatrix.toml".to_string());
assert_eq!(
settings.recent_native_files,
vec![
"b.indicatrix.toml".to_string(),
"a.indicatrix.toml".to_string()
]
);
}
#[test]
fn record_recent_native_file_moves_an_existing_entry_to_the_front_without_duplicating() {
let mut settings = AppSettings::default();
settings.record_recent_native_file("a.indicatrix.toml".to_string());
settings.record_recent_native_file("b.indicatrix.toml".to_string());
settings.record_recent_native_file("a.indicatrix.toml".to_string());
assert_eq!(
settings.recent_native_files,
vec![
"a.indicatrix.toml".to_string(),
"b.indicatrix.toml".to_string()
]
);
}
#[test]
fn record_recent_native_file_caps_at_max_recent_native_files() {
let mut settings = AppSettings::default();
for i in 0..(MAX_RECENT_NATIVE_FILES + 3) {
settings.record_recent_native_file(format!("design-{i}.indicatrix.toml"));
}
assert_eq!(settings.recent_native_files.len(), MAX_RECENT_NATIVE_FILES);
assert_eq!(
settings.recent_native_files[0],
format!("design-{}.indicatrix.toml", MAX_RECENT_NATIVE_FILES + 2)
);
}
}