use super::{
head_shadow::{DEFAULT_HEAD_SHADOW_DEG, default_head_shadow_deg, deserialize_head_shadow_deg},
local_compute::LocalComputeTarget,
remote_endpoint::{
LegacyWorkerMigration, RemoteEndpoint, TiltVideoCompute, migrate_legacy_workers,
},
surface_glare::{DEFAULT_SURFACE_GLARE, default_surface_glare, deserialize_surface_glare},
ui_preferences::{UiMode, deserialize_ui_scale_percent, normalize_ui_scale_percent},
worker::{LiveComputeTarget, LocalPreviewScale, WorkerSettings},
};
use crate::bridge::export_thread::DEFAULT_TEMPLATE as DEFAULT_EXPORT_FILENAME_TEMPLATE;
use indicatrix::optics::raytracer::DEFAULT_POSE;
use serde::{Deserialize, Serialize};
use std::collections::BTreeSet;
pub const DEFAULT_TARGET_SAMPLES: u32 = 256;
pub const DEFAULT_RENDER_WIDTH: u32 = 800;
pub const DEFAULT_RENDER_HEIGHT: u32 = 600;
pub use indicatrix::optics::raytracer::DEFAULT_MAX_BOUNCES;
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_LIVE_COMPUTE_TARGET: LiveComputeTarget = LiveComputeTarget::Both;
pub const DEFAULT_LOCAL_COMPUTE_TARGET: LocalComputeTarget = LocalComputeTarget::CpuGpu;
pub const DEFAULT_CONTRIBUTE_TO_FINAL_PICTURE: bool = true;
pub const DEFAULT_REMOTE_BATCH_LANES: u32 = 4;
pub const MIN_REMOTE_BATCH_LANES: u32 = 1;
pub const MAX_REMOTE_BATCH_LANES: u32 = 32;
#[must_use]
pub fn clamp_remote_batch_lanes(lanes: u32) -> u32 {
lanes.clamp(MIN_REMOTE_BATCH_LANES, MAX_REMOTE_BATCH_LANES)
}
pub const DEFAULT_LIGHT_YAW_DEG: f32 = 48.0;
pub const DEFAULT_LIGHT_PITCH_DEG: f32 = 72.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]
Grey,
White,
}
impl Backdrop {
#[must_use]
pub const fn level(self) -> f32 {
self.to_indicatrix().level()
}
#[must_use]
pub const fn index(self) -> i32 {
self.to_indicatrix().index()
}
#[must_use]
pub const fn from_index(index: i32) -> Self {
Self::from_indicatrix(indicatrix::render_setup::Backdrop::from_index(index))
}
const fn to_indicatrix(self) -> indicatrix::render_setup::Backdrop {
match self {
Self::AsLit => indicatrix::render_setup::Backdrop::AsLit,
Self::Grey => indicatrix::render_setup::Backdrop::Grey,
Self::White => indicatrix::render_setup::Backdrop::White,
}
}
const fn from_indicatrix(backdrop: indicatrix::render_setup::Backdrop) -> Self {
match backdrop {
indicatrix::render_setup::Backdrop::AsLit => Self::AsLit,
indicatrix::render_setup::Backdrop::Grey => Self::Grey,
indicatrix::render_setup::Backdrop::White => Self::White,
}
}
}
pub const DEFAULT_CAMERA_YAW: f32 = 0.35;
pub const DEFAULT_LIVE_CAMERA_PITCH: f32 = 1.15;
pub const DEFAULT_CAMERA_PITCH: f32 = DEFAULT_LIVE_CAMERA_PITCH;
pub const DEFAULT_CAMERA_DISTANCE: f32 = DEFAULT_POSE.distance;
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 = 340.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: Option<RemoteEndpoint>,
#[serde(rename = "remote_workers", default, skip_serializing)]
pub legacy_remote_workers: Vec<WorkerSettings>,
#[serde(default = "default_denoise_enabled")]
pub denoise_enabled: bool,
#[serde(default)]
pub backdrop: Backdrop,
#[serde(
default = "default_surface_glare",
deserialize_with = "deserialize_surface_glare"
)]
pub surface_glare: f32,
#[serde(
default = "default_head_shadow_deg",
deserialize_with = "deserialize_head_shadow_deg"
)]
pub head_shadow_deg: f32,
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,
#[serde(default)]
pub live_compute_target: LiveComputeTarget,
#[serde(default)]
pub local_compute_target: LocalComputeTarget,
#[serde(default)]
pub tilt_video_compute_target: TiltVideoCompute,
#[serde(default = "default_contribute_to_final_picture")]
pub contribute_to_final_picture: bool,
#[serde(
default = "default_remote_batch_lanes",
deserialize_with = "deserialize_remote_batch_lanes"
)]
pub remote_batch_lanes: u32,
#[serde(default = "default_plan_time_limit_secs")]
pub plan_time_limit_secs: u32,
#[serde(default)]
pub import_preview_choice: super::ImportPreviewChoice,
#[serde(
default,
deserialize_with = "indicatrix_net::messages::adaptive::PayloadChoice::deserialize_lenient"
)]
pub payload_encoding: indicatrix_net::messages::adaptive::PayloadChoice,
#[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_tier_table_collapsed: bool,
#[serde(default)]
pub editor_remap_collapsed: bool,
#[serde(default)]
pub editor_layout_touched: bool,
#[serde(default)]
pub recent_native_files: Vec<String>,
#[serde(default)]
pub suppressed_confirmations: BTreeSet<String>,
#[serde(default, deserialize_with = "UiMode::deserialize_lenient")]
pub ui_mode: UiMode,
#[serde(default = "default_first_run_tour_done")]
pub first_run_tour_done: bool,
#[serde(default)]
pub tutorials_completed: BTreeSet<String>,
#[serde(default, deserialize_with = "deserialize_ui_scale_percent")]
pub ui_scale_percent: u16,
#[serde(default)]
pub high_contrast: bool,
#[serde(default)]
pub large_handles: bool,
#[serde(default)]
pub manipulate_snap_off: bool,
#[serde(default = "default_slice_symmetric")]
pub slice_symmetric: bool,
#[serde(default, deserialize_with = "deserialize_render_body_color")]
pub render_body_color_override: Option<[f32; 3]>,
}
pub const MAX_RENDER_BODY_COLOR_CHANNEL: f32 = 10.0;
fn deserialize_render_body_color<'de, D>(deserializer: D) -> Result<Option<[f32; 3]>, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum Lenient {
Triple([f32; 3]),
Other(serde::de::IgnoredAny),
}
Ok(match Option::<Lenient>::deserialize(deserializer)? {
Some(Lenient::Triple(rgb)) if rgb.iter().all(|c| c.is_finite()) => {
Some(rgb.map(|c| c.clamp(0.0, MAX_RENDER_BODY_COLOR_CHANNEL)))
}
_ => None,
})
}
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_first_run_tour_done() -> bool {
true
}
const fn default_slice_symmetric() -> bool {
true
}
const fn default_contribute_to_final_picture() -> bool {
DEFAULT_CONTRIBUTE_TO_FINAL_PICTURE
}
const fn default_plan_time_limit_secs() -> u32 {
crate::plan_limit::DEFAULT_LIMIT_SECS
}
const fn default_remote_batch_lanes() -> u32 {
DEFAULT_REMOTE_BATCH_LANES
}
fn deserialize_remote_batch_lanes<'de, D>(deserializer: D) -> Result<u32, D::Error>
where
D: serde::Deserializer<'de>,
{
let raw = i64::deserialize(deserializer)?;
let limited = raw.clamp(
i64::from(MIN_REMOTE_BATCH_LANES),
i64::from(MAX_REMOTE_BATCH_LANES),
);
Ok(u32::try_from(limited).unwrap_or(DEFAULT_REMOTE_BATCH_LANES))
}
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: None,
legacy_remote_workers: Vec::new(),
denoise_enabled: true,
backdrop: Backdrop::default(),
surface_glare: DEFAULT_SURFACE_GLARE,
head_shadow_deg: DEFAULT_HEAD_SHADOW_DEG,
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,
live_compute_target: DEFAULT_LIVE_COMPUTE_TARGET,
local_compute_target: DEFAULT_LOCAL_COMPUTE_TARGET,
tilt_video_compute_target: TiltVideoCompute::Both,
contribute_to_final_picture: DEFAULT_CONTRIBUTE_TO_FINAL_PICTURE,
remote_batch_lanes: DEFAULT_REMOTE_BATCH_LANES,
plan_time_limit_secs: crate::plan_limit::DEFAULT_LIMIT_SECS,
import_preview_choice: super::ImportPreviewChoice::Ask,
payload_encoding: indicatrix_net::messages::adaptive::PayloadChoice::Auto,
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_tier_table_collapsed: false,
editor_remap_collapsed: false,
editor_layout_touched: false,
recent_native_files: Vec::new(),
suppressed_confirmations: BTreeSet::new(),
ui_mode: UiMode::Simple,
first_run_tour_done: false,
tutorials_completed: BTreeSet::new(),
ui_scale_percent: 0,
high_contrast: false,
large_handles: false,
manipulate_snap_off: false,
slice_symmetric: true,
render_body_color_override: None,
}
}
}
impl AppSettings {
#[must_use]
pub fn remote_worker(&self) -> Option<WorkerSettings> {
self.remote.as_ref().map(|r| r.connection.clone())
}
pub fn migrate_legacy_remote_workers(&mut self) -> Option<LegacyWorkerMigration> {
migrate_legacy_workers(&mut self.legacy_remote_workers, &mut self.remote)
}
pub fn set_remote_batch_lanes(&mut self, lanes: u32) {
self.remote_batch_lanes = clamp_remote_batch_lanes(lanes);
}
pub fn record_recent_native_file(&mut self, path: String) {
self.recent_native_files.retain(|existing| {
existing != &path && !super::recent_files::is_superseded_sidecar(existing, &path)
});
self.recent_native_files.insert(0, path);
self.recent_native_files.truncate(MAX_RECENT_NATIVE_FILES);
}
#[must_use]
pub fn is_confirm_suppressed(&self, key: &str) -> bool {
self.suppressed_confirmations.contains(key)
}
pub fn suppress_confirm(&mut self, key: impl Into<String>) {
self.suppressed_confirmations.insert(key.into());
}
pub const fn treat_as_existing_install(&mut self) {
self.ui_mode = UiMode::Advanced;
self.first_run_tour_done = true;
}
pub fn set_ui_scale_percent(&mut self, percent: i32) {
self.ui_scale_percent = normalize_ui_scale_percent(i64::from(percent));
}
pub fn reset_tutorials(&mut self) {
self.tutorials_completed.clear();
}
pub const fn show_tour_again(&mut self) {
self.first_run_tour_done = false;
}
}
#[cfg(test)]
mod tests;