mod opening;
mod stored;
use crate::{
DesignLightingModel, MainWindow, SettingsModel, ViewportModel,
bridge::render_thread::{RenderContext, load_env_map},
gui::{env_map_status_text, show_toast, tutorial_events::raise},
settings::{SettingsPersister, model::percent_from_surface_glare},
};
use indicatrix::{optics::LightingPreset, renderer::env_map::EnvironmentMap};
use indicatrix_editor::guide::viewing_events as events;
use indicatrix_vault::{db::sqlite::Database, model::design_key::normalize_design_uuid};
use opening::{
Lookup, Note, Plan, decide, first_time_for, lookup_from_row, may_show_over, note_text,
};
use slint::ComponentHandle;
use std::{
cell::RefCell,
collections::HashSet,
panic::{AssertUnwindSafe, catch_unwind},
sync::{Arc, Mutex, PoisonError, TryLockError},
thread,
time::{Duration, SystemTime, UNIX_EPOCH},
};
use stored::{
EnvSource, LightingValues, PITCH_RANGE_RAD, answer_is_current, env_path_for_capture,
env_path_to_store, env_source_for_opening,
};
const NOTE_DELAY: Duration = Duration::from_millis(1200);
fn read_with(db: &Database, uuid: &str) -> Lookup {
match db.design_lighting(uuid) {
Ok(row) => lookup_from_row(row.as_ref().map(|row| row.settings_json.as_str())),
Err(err) => Lookup::Unreadable(format!("{err:#}")),
}
}
fn read_lookup(db: &Mutex<Database>, uuid: &str) -> Lookup {
read_with(&db.lock().unwrap_or_else(PoisonError::into_inner), uuid)
}
fn try_read_lookup(db: &Mutex<Database>, uuid: &str) -> Option<Lookup> {
let guard = match db.try_lock() {
Ok(guard) => guard,
Err(TryLockError::Poisoned(poisoned)) => poisoned.into_inner(),
Err(TryLockError::WouldBlock) => return None,
};
Some(read_with(&guard, uuid))
}
fn store_values(
db: &Mutex<Database>,
uuid: &str,
values: &LightingValues,
now: i64,
) -> Result<(), String> {
let json = values.to_json()?;
db.lock()
.unwrap_or_else(PoisonError::into_inner)
.set_design_lighting(uuid, &values.lighting_rig, &json, now)
.map_err(|err| format!("{err:#}"))
}
fn forget_values(db: &Mutex<Database>, uuid: &str) -> Result<(), String> {
db.lock()
.unwrap_or_else(PoisonError::into_inner)
.clear_design_lighting(uuid)
.map(|_| ())
.map_err(|err| format!("{err:#}"))
}
fn unix_now() -> i64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |elapsed| {
i64::try_from(elapsed.as_secs()).unwrap_or(i64::MAX)
})
}
#[derive(Clone)]
struct Handles {
db: Arc<Mutex<Database>>,
render_ctx: Arc<Mutex<RenderContext>>,
settings_store: Arc<SettingsPersister>,
}
#[derive(Clone)]
struct EnvUi {
loaded: bool,
status: String,
path_text: String,
}
struct Normal {
values: LightingValues,
env_map: Option<Arc<EnvironmentMap>>,
env_ui: EnvUi,
}
#[derive(Default)]
struct Session {
handles: Option<Handles>,
design: Option<String>,
normal: Option<Normal>,
told: HashSet<String>,
open_generation: u64,
pending_env_path: Option<String>,
env_decode: u64,
env_decode_at_open: u64,
}
thread_local! {
static SESSION: RefCell<Session> = RefCell::new(Session::default());
}
fn handles() -> Option<Handles> {
SESSION.with(|session| session.borrow().handles.clone())
}
fn open_design() -> Option<String> {
SESSION.with(|session| session.borrow().design.clone())
}
fn override_active() -> bool {
SESSION.with(|session| session.borrow().normal.is_some())
}
fn begin_generation() -> u64 {
SESSION.with(|session| {
let mut session = session.borrow_mut();
session.open_generation = session.open_generation.wrapping_add(1);
session.pending_env_path = None;
session.env_decode_at_open = session.env_decode;
session.open_generation
})
}
fn map_chosen_since_open() -> bool {
SESSION.with(|session| {
let session = session.borrow();
session.env_decode != session.env_decode_at_open
})
}
fn pending_env_path() -> Option<String> {
SESSION.with(|session| session.borrow().pending_env_path.clone())
}
fn set_pending_env_path(path: Option<String>) {
SESSION.with(|session| session.borrow_mut().pending_env_path = path);
}
fn begin_env_decode(path: &str) -> u64 {
SESSION.with(|session| {
let mut session = session.borrow_mut();
session.env_decode = session.env_decode.wrapping_add(1);
session.pending_env_path = Some(path.to_owned());
session.env_decode_at_open = session.env_decode;
session.env_decode
})
}
fn env_decode_is_current(asked: u64) -> bool {
SESSION.with(|session| answer_is_current(asked, session.borrow().env_decode))
}
pub(in crate::gui) fn env_map_chosen() {
SESSION.with(|session| {
let mut session = session.borrow_mut();
session.env_decode = session.env_decode.wrapping_add(1);
session.pending_env_path = None;
});
}
fn generation_is_current(asked: u64) -> bool {
SESSION.with(|session| answer_is_current(asked, session.borrow().open_generation))
}
pub(in crate::gui) fn setup_design_lighting_callbacks(
ui: &MainWindow,
db: &Arc<Mutex<Database>>,
render_ctx: &Arc<Mutex<RenderContext>>,
settings_store: &Arc<SettingsPersister>,
) {
SESSION.with(|session| {
session.borrow_mut().handles = Some(Handles {
db: Arc::clone(db),
render_ctx: Arc::clone(render_ctx),
settings_store: Arc::clone(settings_store),
});
});
let weak = ui.as_weak();
ui.global::<DesignLightingModel>()
.on_use_for_design(move || {
if let Some(ui) = weak.upgrade() {
use_lighting_for_design(&ui);
}
});
let weak = ui.as_weak();
ui.global::<DesignLightingModel>()
.on_forget_for_design(move || {
if let Some(ui) = weak.upgrade() {
forget_lighting_for_design(&ui);
}
});
}
pub(in crate::gui) fn design_opened(ui: &MainWindow, design_uuid: &str, has_design: bool) {
let Some(handles) = handles() else {
return;
};
let uuid = if has_design {
normalize_design_uuid(design_uuid)
} else {
None
};
let generation = begin_generation();
SESSION.with(|session| session.borrow_mut().design.clone_from(&uuid));
let Some(uuid) = uuid else {
finish_open(ui, &handles, None, &Lookup::NoRow, generation);
return;
};
match try_read_lookup(&handles.db, &uuid) {
Some(lookup) => finish_open(ui, &handles, Some(&uuid), &lookup, generation),
None => read_lookup_on_a_thread(ui, &handles, uuid, generation),
}
}
fn read_lookup_on_a_thread(ui: &MainWindow, shared: &Handles, uuid: String, generation: u64) {
let db = Arc::clone(&shared.db);
let weak = ui.as_weak();
let spawned = thread::Builder::new()
.name("design-lighting-read".to_owned())
.spawn(move || {
let lookup = read_lookup(&db, &uuid);
let _ = weak.upgrade_in_event_loop(move |ui| {
if !generation_is_current(generation) {
return;
}
if let Some(handles) = handles() {
finish_open(&ui, &handles, Some(&uuid), &lookup, generation);
}
});
});
if spawned.is_err() {
let unreadable = Lookup::Unreadable("the program could not start the work".to_owned());
let uuid = open_design();
finish_open(ui, shared, uuid.as_deref(), &unreadable, generation);
}
}
fn finish_open(
ui: &MainWindow,
handles: &Handles,
uuid: Option<&str>,
lookup: &Lookup,
generation: u64,
) {
let showing_own = override_active();
let decision = decide(lookup, showing_own);
match decision.plan {
Plan::Keep => {}
Plan::Apply(values) => {
if !showing_own {
begin_override(ui, handles);
}
apply_values(ui, handles, &values, generation);
}
Plan::Restore => restore_normal_lighting(ui, handles),
}
publish_model(ui, uuid.is_some(), lookup);
let announce = decision
.note
.as_ref()
.filter(|note| should_announce(note, uuid));
if let Some(note) = announce {
let (text, kind) = note_text(note);
toast_after_open(ui, text, kind);
}
}
fn should_announce(note: &Note, design_uuid: Option<&str>) -> bool {
match (note, design_uuid) {
(Note::UnknownRig(_) | Note::Unreadable(_), Some(uuid)) => {
SESSION.with(|session| first_time_for(&mut session.borrow_mut().told, uuid))
}
_ => true,
}
}
pub(in crate::gui) fn normal_lighting_chosen() {
begin_generation();
let showing_own = SESSION.with(|session| session.borrow_mut().normal.take().is_some());
if showing_own && let Some(handles) = handles() {
handles.settings_store.set_disk_override(None);
}
}
fn restore_normal_lighting(ui: &MainWindow, handles: &Handles) {
begin_generation();
let normal = SESSION.with(|session| session.borrow_mut().normal.take());
if let Some(normal) = normal {
restore_normal(ui, handles, &normal);
handles.settings_store.set_disk_override(None);
}
}
fn begin_override(ui: &MainWindow, handles: &Handles) {
let normal = capture_normal(ui, handles);
handles
.settings_store
.set_disk_override(Some(normal.values.disk_pin()));
SESSION.with(|session| session.borrow_mut().normal = Some(normal));
}
fn capture_normal(ui: &MainWindow, handles: &Handles) -> Normal {
let values = LightingValues::from_app_settings(&handles.settings_store.snapshot().settings);
let env_map = RenderContext::lock(&handles.render_ctx).env_map.clone();
let settings = ui.global::<SettingsModel>();
let env_ui = EnvUi {
loaded: settings.get_env_map_loaded(),
status: settings.get_env_map_status().to_string(),
path_text: settings.get_env_map_path().to_string(),
};
Normal {
values,
env_map,
env_ui,
}
}
fn apply_values(ui: &MainWindow, handles: &Handles, values: &LightingValues, generation: u64) {
let rig = values.rig().unwrap_or_default();
let loaded = loaded_env_path(ui, handles);
let source = env_source_for_opening(
map_chosen_since_open(),
values.env_map_path.as_deref(),
loaded.as_deref(),
);
{
let mut ctx = RenderContext::lock(&handles.render_ctx);
push_values_to_context(&mut ctx, values, rig);
if source == EnvSource::Studio {
ctx.env_map = None;
}
ctx.dirty = true;
}
push_values_to_controls(ui, values, rig);
match source {
EnvSource::Studio => {
let settings = ui.global::<SettingsModel>();
settings.set_env_map_status(String::new().into());
settings.set_env_map_loaded(false);
}
EnvSource::AlreadyLoaded | EnvSource::ChosenByHand => {}
EnvSource::Decode(path) => start_env_decode(ui, path, generation),
}
}
fn loaded_env_path(ui: &MainWindow, handles: &Handles) -> Option<String> {
let has_map = RenderContext::lock(&handles.render_ctx).env_map.is_some();
let settings = ui.global::<SettingsModel>();
env_path_for_capture(
has_map && settings.get_env_map_loaded(),
settings.get_env_map_path().as_str(),
)
}
fn start_env_decode(ui: &MainWindow, path: String, generation: u64) {
let decode = begin_env_decode(&path);
let weak = ui.as_weak();
let spawned = thread::Builder::new()
.name("design-lighting-hdr".to_owned())
.spawn(move || {
let decoded = catch_unwind(AssertUnwindSafe(|| load_env_map(&path)))
.unwrap_or_else(|_| Err("the file could not be decoded".to_owned()));
let _ = weak.upgrade_in_event_loop(move |ui| {
env_decoded(&ui, &path, decoded, generation, decode);
});
});
if spawned.is_err() {
set_pending_env_path(None);
toast_after_open(
ui,
"This design's saved lighting uses an HDR environment map that could not be \
loaded (the program could not start the work). The current environment is kept."
.to_owned(),
"warning",
);
}
}
fn env_decoded(
ui: &MainWindow,
path: &str,
decoded: Result<Arc<EnvironmentMap>, String>,
generation: u64,
decode: u64,
) {
if !generation_is_current(generation) || !env_decode_is_current(decode) {
return;
}
set_pending_env_path(None);
if !override_active() {
return;
}
let Some(handles) = handles() else {
return;
};
match decoded {
Ok(map) => {
{
let mut ctx = RenderContext::lock(&handles.render_ctx);
ctx.env_map = Some(Arc::clone(&map));
ctx.dirty = true;
}
let settings = ui.global::<SettingsModel>();
settings.set_env_map_status(env_map_status_text(&map, path).into());
settings.set_env_map_loaded(true);
settings.set_env_map_path(path.into());
}
Err(err) => toast_after_open(
ui,
format!(
"This design's saved lighting uses an HDR environment map that could not be \
loaded ({err}). The current environment is kept."
),
"warning",
),
}
}
fn restore_normal(ui: &MainWindow, handles: &Handles, normal: &Normal) {
let values = &normal.values;
let rig = crate::gui::optics::offered_lighting::offered_from_label(&values.lighting_rig);
{
let mut ctx = RenderContext::lock(&handles.render_ctx);
push_values_to_context(&mut ctx, values, rig);
ctx.env_map.clone_from(&normal.env_map);
ctx.dirty = true;
}
handles
.settings_store
.update(|file| values.write_into(&mut file.settings));
push_values_to_controls(ui, values, rig);
let settings = ui.global::<SettingsModel>();
settings.set_env_map_loaded(normal.env_ui.loaded);
settings.set_env_map_status(normal.env_ui.status.clone().into());
settings.set_env_map_path(normal.env_ui.path_text.clone().into());
}
const fn push_values_to_context(
ctx: &mut RenderContext,
values: &LightingValues,
rig: LightingPreset,
) {
ctx.light_yaw = values.light_yaw_deg.to_radians();
ctx.light_pitch = values
.light_pitch_deg
.to_radians()
.clamp(PITCH_RANGE_RAD.0, PITCH_RANGE_RAD.1);
ctx.exposure = values.exposure;
ctx.lighting_preset = rig;
ctx.surface_glare = values.surface_glare;
ctx.backdrop = values.backdrop;
}
fn push_values_to_controls(ui: &MainWindow, values: &LightingValues, rig: LightingPreset) {
let settings = ui.global::<SettingsModel>();
settings.set_light_yaw_deg(values.light_yaw_deg);
settings.set_light_pitch_deg(values.light_pitch_deg);
settings.set_exposure_val(values.exposure);
settings.set_surface_glare_pct(percent_from_surface_glare(values.surface_glare));
settings.set_backdrop_index(values.backdrop.index());
ui.global::<ViewportModel>()
.set_selected_lighting_index(rig.index());
}
fn publish_model(ui: &MainWindow, design_open: bool, lookup: &Lookup) {
let model = ui.global::<DesignLightingModel>();
model.set_available(design_open);
model.set_has_saved(design_open && lookup.has_row());
model.set_summary(lookup.summary_text().into());
}
fn toast_after_open(ui: &MainWindow, text: String, kind: &'static str) {
let weak = ui.as_weak();
slint::Timer::single_shot(NOTE_DELAY, move || {
let Some(ui) = weak.upgrade() else {
return;
};
if may_show_over(ui.get_toast_visible(), ui.get_toast_type().as_str()) {
show_toast(&ui, &text, kind);
}
});
}
fn use_lighting_for_design(ui: &MainWindow) {
let (Some(handles), Some(uuid)) = (handles(), open_design()) else {
show_toast(
ui,
"Open or create a design first, then save its lighting.",
"info",
);
return;
};
let settings = ui.global::<SettingsModel>();
let env_path = env_path_to_store(
pending_env_path().as_deref(),
settings.get_env_map_loaded(),
settings.get_env_map_path().as_str(),
);
let values = {
let ctx = RenderContext::lock(&handles.render_ctx);
LightingValues::from_live(&ctx, env_path)
};
if let Err(err) = store_values(&handles.db, &uuid, &values, unix_now()) {
show_toast(
ui,
&format!("Could not save the lighting for this design: {err}"),
"error",
);
return;
}
if !override_active() {
begin_override(ui, &handles);
}
SESSION.with(|session| session.borrow_mut().told.remove(&uuid));
publish_model(ui, true, &Lookup::Row(values));
show_toast(
ui,
"Saved. This design will open with this lighting.",
"success",
);
raise(ui, events::DESIGN_LIGHTING_SAVED);
}
fn forget_lighting_for_design(ui: &MainWindow) {
let (Some(handles), Some(uuid)) = (handles(), open_design()) else {
return;
};
if let Err(err) = forget_values(&handles.db, &uuid) {
show_toast(
ui,
&format!("Could not forget the lighting for this design: {err}"),
"error",
);
return;
}
restore_normal_lighting(ui, &handles);
publish_model(ui, true, &Lookup::NoRow);
show_toast(ui, "This design now uses your normal lighting.", "info");
raise(ui, events::DESIGN_LIGHTING_FORGOTTEN);
}
#[cfg(test)]
mod tests;