pub mod batch;
pub mod filename_template;
#[cfg(test)]
mod icc_profile;
mod params;
mod preview;
pub mod remote;
mod sample_cursor;
mod scene_snapshot;
mod tonemap_png;
mod worker;
#[cfg(test)]
mod hybrid_export_tests;
#[cfg(test)]
mod render_setup_pins;
#[cfg(test)]
mod tests;
pub use filename_template::{DEFAULT_TEMPLATE, TemplateContext, resolve_export_path};
pub use params::{ComputeTarget, ExportParams, RemoteSelection, validate_export_params};
pub use remote::{forget_final_picture_refusals, probe_remote};
pub use scene_snapshot::SceneSnapshot;
pub use worker::{AccumulationCarry, RenderedImage, render_image_rgba};
use crate::settings::LocalComputeTarget;
use indicatrix::color::ColorSpace;
use slint::{ComponentHandle, Rgba8Pixel, SharedPixelBuffer, Weak};
use std::{
path::PathBuf,
sync::{
Arc,
atomic::{AtomicBool, Ordering},
},
thread,
};
pub use worker::run_export;
#[must_use]
pub fn scene_state_for_job(
snapshot: &SceneSnapshot,
width: u32,
height: u32,
) -> indicatrix_net::SceneState {
remote::scene_state_from_snapshot(snapshot, width, height, snapshot.yaw, snapshot.pitch)
}
#[derive(Debug)]
pub enum ExportOutcome {
Completed(PathBuf),
Cancelled,
Failed(String),
}
pub struct ExportProgress {
pub fraction: f32,
pub samples_done: u32,
pub samples_total: u32,
pub preview: Option<SharedPixelBuffer<Rgba8Pixel>>,
pub note: Option<String>,
}
pub struct ExportHandle {
cancel: Arc<AtomicBool>,
}
impl ExportHandle {
pub fn cancel(&self) {
self.cancel.store(true, Ordering::Relaxed);
}
}
#[expect(
clippy::too_many_arguments,
reason = "every argument is a distinct piece of one export request's own identity \
(scene, output params/path/color-space, the remote selection, the local \
compute choice, and the two UI callbacks) -- bundling them into a struct \
would just move the same count into field access, not reduce it"
)]
pub fn spawn_export<T, P, D>(
ui_weak: Weak<T>,
scene: SceneSnapshot,
params: ExportParams,
color_space: ColorSpace,
output_path: PathBuf,
remote: RemoteSelection,
local_compute: LocalComputeTarget,
on_progress: P,
on_done: D,
) -> ExportHandle
where
T: ComponentHandle + 'static,
P: Fn(&T, ExportProgress) + Send + 'static + Clone,
D: Fn(&T, ExportOutcome) + Send + 'static + Clone,
{
let cancel = Arc::new(AtomicBool::new(false));
let cancel_worker = cancel.clone();
let ui_weak_done = ui_weak.clone();
thread::spawn(move || {
let progress_ui_weak = ui_weak;
let report_progress = move |progress: ExportProgress| {
let on_progress = on_progress.clone();
let _ = progress_ui_weak.upgrade_in_event_loop(move |ui| on_progress(&ui, progress));
};
let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
run_export(
&scene,
params,
color_space,
&output_path,
&remote,
local_compute,
&cancel_worker,
report_progress,
)
}))
.unwrap_or_else(|payload| {
let message = payload
.downcast_ref::<&str>()
.map(|s| (*s).to_string())
.or_else(|| payload.downcast_ref::<String>().cloned())
.unwrap_or_else(|| "the export worker panicked".to_string());
ExportOutcome::Failed(format!("Export failed unexpectedly: {message}"))
});
let _ = ui_weak_done.upgrade_in_event_loop(move |ui| on_done(&ui, outcome));
});
ExportHandle { cancel }
}