use crate::{
bridge::render_thread::RenderContext,
settings::{
SettingsFile,
model::{AppSettings, Backdrop, clamp_surface_glare},
persist::DiskOverride,
},
};
use indicatrix::optics::LightingPreset;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
pub(super) const FORMAT_VERSION: u32 = 1;
pub(super) const PITCH_RANGE_RAD: (f32, f32) = (0.15, 1.55);
pub(super) const EXPOSURE_RANGE: (f32, f32) = (0.2, 5.0);
const fn format_version() -> u32 {
FORMAT_VERSION
}
const fn full_glare() -> f32 {
1.0
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub(super) struct LightingValues {
#[serde(default = "format_version")]
pub(super) version: u32,
pub(super) lighting_rig: String,
pub(super) light_yaw_deg: f32,
pub(super) light_pitch_deg: f32,
pub(super) exposure: f32,
#[serde(default = "full_glare")]
pub(super) surface_glare: f32,
#[serde(default)]
pub(super) backdrop: Backdrop,
#[serde(default)]
pub(super) env_map_path: Option<String>,
}
impl LightingValues {
pub(super) fn from_app_settings(app: &AppSettings) -> Self {
Self {
version: FORMAT_VERSION,
lighting_rig: app.lighting_rig.clone(),
light_yaw_deg: app.light_yaw_deg,
light_pitch_deg: app.light_pitch_deg,
exposure: app.exposure,
surface_glare: app.surface_glare,
backdrop: app.backdrop,
env_map_path: (!app.env_map_path.is_empty()).then(|| app.env_map_path.clone()),
}
.sanitised()
}
pub(super) fn from_live(ctx: &RenderContext, env_map_path: Option<String>) -> Self {
Self {
version: FORMAT_VERSION,
lighting_rig: ctx.lighting_preset.label().to_string(),
light_yaw_deg: round2(ctx.light_yaw.to_degrees()),
light_pitch_deg: round2(ctx.light_pitch.to_degrees()),
exposure: round2(ctx.exposure),
surface_glare: ctx.surface_glare,
backdrop: ctx.backdrop,
env_map_path,
}
.sanitised()
}
pub(super) fn from_json(text: &str) -> Result<Self, String> {
serde_json::from_str::<Self>(text)
.map(Self::sanitised)
.map_err(|err| err.to_string())
}
pub(super) fn to_json(&self) -> Result<String, String> {
serde_json::to_string(self).map_err(|err| err.to_string())
}
pub(super) fn sanitised(mut self) -> Self {
let defaults = AppSettings::default();
self.light_yaw_deg =
finite_or(self.light_yaw_deg, defaults.light_yaw_deg).rem_euclid(360.0);
self.light_pitch_deg =
clamp_pitch_deg(finite_or(self.light_pitch_deg, defaults.light_pitch_deg));
self.exposure =
finite_or(self.exposure, defaults.exposure).clamp(EXPOSURE_RANGE.0, EXPOSURE_RANGE.1);
self.surface_glare = clamp_surface_glare(self.surface_glare);
self.env_map_path = self.env_map_path.filter(|path| !path.trim().is_empty());
self
}
pub(super) fn write_into(&self, app: &mut AppSettings) {
app.lighting_rig.clone_from(&self.lighting_rig);
app.light_yaw_deg = self.light_yaw_deg;
app.light_pitch_deg = self.light_pitch_deg;
app.exposure = self.exposure;
app.surface_glare = self.surface_glare;
app.backdrop = self.backdrop;
app.env_map_path = self.env_map_path.clone().unwrap_or_default();
}
pub(super) fn disk_pin(&self) -> DiskOverride {
let values = self.clone();
Arc::new(move |file: &mut SettingsFile| values.write_into(&mut file.settings))
}
pub(super) fn rig(&self) -> Option<LightingPreset> {
known_rig(&self.lighting_rig)
}
pub(super) fn summary(&self) -> String {
let mut text = format!(
"{}, light {:.0}° around and {:.0}° up, exposure {:.2}x",
self.lighting_rig, self.light_yaw_deg, self.light_pitch_deg, self.exposure
);
if self.env_map_path.is_some() {
text.push_str(", with an HDR environment map");
}
text.push('.');
text
}
}
const fn finite_or(value: f32, fallback: f32) -> f32 {
if value.is_finite() { value } else { fallback }
}
fn round2(value: f32) -> f32 {
(value * 100.0).round() / 100.0
}
const fn clamp_pitch_deg(degrees: f32) -> f32 {
degrees.clamp(
PITCH_RANGE_RAD.0.to_degrees(),
PITCH_RANGE_RAD.1.to_degrees(),
)
}
pub(super) fn known_rig(label: &str) -> Option<LightingPreset> {
let rig = LightingPreset::from_label(label);
if rig.is_uv_lamp() && !crate::gui::optics::physics_state::PHYSICS_COLOR_UI {
return None;
}
let is_tent_label = matches!(
label,
"Light tent + black cards" | "Soft dome + ring lights"
);
(rig != LightingPreset::LightTent || is_tent_label).then_some(rig)
}
pub(super) fn env_path_for_capture(map_loaded: bool, path_text: &str) -> Option<String> {
let path = path_text.trim();
(map_loaded && !path.is_empty()).then(|| path.to_string())
}
pub(super) fn env_path_to_store(
pending: Option<&str>,
map_loaded: bool,
path_text: &str,
) -> Option<String> {
pending
.map(str::trim)
.filter(|path| !path.is_empty())
.map(str::to_string)
.or_else(|| env_path_for_capture(map_loaded, path_text))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) enum EnvSource {
Studio,
AlreadyLoaded,
Decode(String),
ChosenByHand,
}
pub(super) fn env_source(wanted: Option<&str>, loaded: Option<&str>) -> EnvSource {
match (wanted.map(str::trim), loaded.map(str::trim)) {
(None, _) => EnvSource::Studio,
(Some(wanted), Some(loaded)) if wanted == loaded => EnvSource::AlreadyLoaded,
(Some(wanted), _) => EnvSource::Decode(wanted.to_string()),
}
}
pub(super) fn env_source_for_opening(
chosen_since_open: bool,
wanted: Option<&str>,
loaded: Option<&str>,
) -> EnvSource {
if chosen_since_open {
EnvSource::ChosenByHand
} else {
env_source(wanted, loaded)
}
}
pub(super) const fn answer_is_current(asked: u64, latest: u64) -> bool {
asked == latest
}