use super::queue::{
ExportJob, ExportQueue, apply_export_bounce_cap, apply_preset_to_scene, colorspace_label,
compute_target_from_index, start_next_export_job,
};
use crate::{
ExportModel, ExportRunModel, LibraryModel, LightingPresetItem, MainWindow,
bridge::{
export_thread::{self, ExportParams, RemoteSelection, SceneSnapshot},
render_thread::PlanesOwner,
},
gui::progress_eta::EtaEstimator,
settings::{ExportTransfer, LightingPreset as SavedLightingPreset, SettingsPersister},
};
use indicatrix::color::ColorSpace;
use indicatrix_solid::preview::StoneGeometryBuf;
use slint::{ComponentHandle, Model, ModelRc, VecModel};
use std::{
cell::RefCell,
collections::VecDeque,
path::PathBuf,
rc::Rc,
sync::{Arc, Mutex},
};
pub(in crate::gui) fn setup_render_export_callbacks(
ui: &MainWindow,
render_ctx: &Arc<Mutex<crate::bridge::render_thread::RenderContext>>,
settings_store: &Arc<SettingsPersister>,
mesh_bounding_radius: &Arc<Mutex<f64>>,
) {
setup_check_remote_availability_callback(ui, settings_store);
setup_populate_export_fanout_presets_callback(ui, settings_store);
setup_toggle_export_preset_selected_callback(ui);
setup_export_output_location_callbacks(ui, settings_store);
super::advanced::setup_advanced_in_use_callback(ui);
let export_queue: Rc<RefCell<Option<Arc<Mutex<ExportQueue>>>>> = Rc::new(RefCell::new(None));
let render_ctx_start = render_ctx.clone();
let settings_store_start = settings_store.clone();
let export_queue_start = export_queue.clone();
let mesh_bounding_radius_start = mesh_bounding_radius.clone();
let ui_weak_start = ui.as_weak();
ui.global::<ExportModel>().on_start_export(
move |width: i32,
height: i32,
samples: i32,
color_space_index: i32,
compute_target_index: i32,
max_bounces: i32| {
let Some(ui) = ui_weak_start.upgrade() else {
return;
};
if refuse_while_busy(&ui) {
return;
}
let params =
match export_thread::validate_export_params(width, height, samples, max_bounces) {
Ok(params) => params,
Err(err) => {
ui.global::<ExportModel>().set_has_error(true);
ui.global::<ExportModel>().set_status_message(err.into());
return;
}
};
let color_space = crate::gui::color_space_from_index(color_space_index);
let remote = RemoteSelection {
compute_target: compute_target_from_index(compute_target_index),
worker: settings_store_start.snapshot().settings.remote_worker(),
transfer: ExportTransfer::from_index(
ui.global::<ExportModel>().get_transfer_index(),
),
contribute_local: settings_store_start
.snapshot()
.settings
.contribute_to_final_picture,
};
let context = StartExportContext {
render_ctx: render_ctx_start.clone(),
settings_store: settings_store_start.clone(),
export_queue: export_queue_start.clone(),
mesh_bounding_radius: mesh_bounding_radius_start.clone(),
params,
color_space,
remote,
};
resolve_export_dir_then(
&ui,
&settings_store_start,
"Export cancelled.",
move |ui, dir| {
if let Some(dir) = dir {
finish_start_export(ui, dir, context);
}
},
);
},
);
let export_queue_cancel = export_queue;
ui.global::<ExportModel>().on_cancel_export(move || {
if let Some(queue) = export_queue_cancel.borrow().as_ref() {
let mut q = queue
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
q.cancelled = true;
if let Some(handle) = &q.current_handle {
handle.cancel();
}
}
});
}
fn refuse_while_busy(ui: &MainWindow) -> bool {
let Some(reason) = crate::gui::export_run::busy_refusal(ui) else {
return false;
};
ui.global::<ExportModel>().set_has_error(true);
ui.global::<ExportModel>().set_status_message(reason.into());
crate::gui::show_toast(ui, reason, "info");
true
}
pub(in crate::gui) fn resolve_export_dir_then(
ui: &MainWindow,
settings_store: &Arc<SettingsPersister>,
cancel_message: &'static str,
on_done: impl FnOnce(&MainWindow, Option<PathBuf>) + 'static,
) {
let configured = settings_store.snapshot().settings.export_directory;
if !configured.is_empty() {
on_done(ui, Some(PathBuf::from(configured)));
return;
}
let settings_store = Arc::clone(settings_store);
crate::gui::pickers::pick(
ui,
crate::gui::pickers::PickerRequest {
kind: crate::gui::pickers::PickerKind::PickFolder,
title: Some("Choose an export folder".to_string()),
filters: Vec::new(),
default_file_name: None,
starting_dir: None,
},
move |ui, dir| {
let Some(dir) = dir else {
ui.global::<ExportModel>().set_has_error(false);
ui.global::<ExportModel>()
.set_status_message(cancel_message.into());
on_done(ui, None);
return;
};
settings_store.update(|s| {
s.settings.export_directory = dir.to_string_lossy().into_owned();
});
on_done(ui, Some(dir));
},
);
}
struct StartExportContext {
render_ctx: Arc<Mutex<crate::bridge::render_thread::RenderContext>>,
settings_store: Arc<SettingsPersister>,
export_queue: Rc<RefCell<Option<Arc<Mutex<ExportQueue>>>>>,
mesh_bounding_radius: Arc<Mutex<f64>>,
params: ExportParams,
color_space: ColorSpace,
remote: RemoteSelection,
}
fn finish_start_export(ui: &MainWindow, export_dir: PathBuf, context: StartExportContext) {
let render_ctx = Arc::clone(&context.render_ctx);
crate::gui::editor::finished_stone_then(
ui,
&render_ctx,
|ui| {
ui.global::<ExportModel>().set_has_error(false);
ui.global::<ExportModel>()
.set_status_message("Preparing the finished stone...".into());
},
move |ui, finished| match finished {
Ok(finished) => continue_start_export(ui, export_dir, context, finished.as_ref()),
Err(withheld) => refuse_export(ui, &withheld),
},
);
}
fn refuse_export(ui: &MainWindow, withheld: &crate::gui::editor::Withheld) {
ui.global::<ExportModel>().set_has_error(true);
ui.global::<ExportModel>()
.set_status_message(withheld.export_message().into());
}
pub(in crate::gui) fn fan_out_jobs(
ui: &MainWindow,
settings_store: &SettingsPersister,
base_scene: &SceneSnapshot,
mesh_bounding_radius: &Mutex<f64>,
) -> VecDeque<ExportJob> {
let selected_presets: Vec<SavedLightingPreset> = {
let snapshot = settings_store.snapshot();
ui.global::<ExportModel>()
.get_fanout_presets()
.iter()
.filter(|item| item.selected)
.filter_map(|item| {
snapshot
.presets
.iter()
.find(|p| p.name == item.name.as_str())
.cloned()
})
.collect()
};
let mut jobs = VecDeque::with_capacity(1 + selected_presets.len());
jobs.push_back(ExportJob {
scene: base_scene.clone(),
preset_label: String::new(),
});
let mesh_bounding_radius = *mesh_bounding_radius
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
for preset in &selected_presets {
jobs.push_back(ExportJob {
scene: apply_preset_to_scene(base_scene.clone(), preset, mesh_bounding_radius),
preset_label: preset.name.clone(),
});
}
jobs
}
pub(in crate::gui) fn design_naming(
ui: &MainWindow,
render_ctx: &Arc<Mutex<crate::bridge::render_thread::RenderContext>>,
) -> (String, String, String, String) {
let detail = ui.global::<LibraryModel>().get_current_detail();
let planes_owner = crate::bridge::render_thread::RenderContext::lock(render_ctx).planes_owner;
match planes_owner {
PlanesOwner::Catalogue { entry_id } if i64::from(detail.id) == entry_id => (
detail.title.to_string(),
detail.designer.to_string(),
detail.shape.to_string(),
detail.ri.to_string(),
),
PlanesOwner::Builtin | PlanesOwner::Catalogue { .. } | PlanesOwner::Editor { .. } => (
"Untitled design".to_string(),
String::new(),
String::new(),
String::new(),
),
}
}
fn continue_start_export(
ui: &MainWindow,
export_dir: PathBuf,
context: StartExportContext,
finished: Option<&StoneGeometryBuf>,
) {
if ui.global::<ExportModel>().get_is_exporting() {
return;
}
if refuse_while_busy(ui) {
return;
}
let StartExportContext {
render_ctx,
settings_store,
export_queue,
mesh_bounding_radius,
params,
color_space,
remote,
} = context;
let template = settings_store.snapshot().settings.export_filename_template;
let base_scene = match SceneSnapshot::capture_finished(&render_ctx, finished) {
Ok(scene) => apply_export_bounce_cap(scene, params.max_bounces),
Err(reason) => {
ui.global::<ExportModel>().set_has_error(true);
ui.global::<ExportModel>().set_status_message(reason.into());
return;
}
};
let jobs = fan_out_jobs(ui, &settings_store, &base_scene, &mesh_bounding_radius);
let total = jobs.len();
let local_compute = {
let mut guard = crate::bridge::render_thread::RenderContext::lock(&render_ctx);
if remote.compute_target.pauses_live_viewport() {
guard.export_active_count += 1;
}
guard.local_compute_target
};
ui.global::<ExportRunModel>()
.set_live_view_paused(remote.compute_target.pauses_live_viewport());
let (design, designer, shape, ri) = design_naming(ui, &render_ctx);
let queue = Arc::new(Mutex::new(ExportQueue {
jobs,
total,
current_index: 0,
completed: Vec::new(),
failures: Vec::new(),
cancelled: false,
current_handle: None,
export_dir,
template,
design,
designer,
shape,
ri,
width: params.width,
height: params.height,
spp: params.samples_per_pixel,
bounces: params.max_bounces,
colorspace: colorspace_label(color_space).to_string(),
params,
color_space,
remote,
local_compute,
eta: EtaEstimator::default(),
}));
ui.global::<ExportModel>().set_is_exporting(true);
ui.global::<ExportModel>().set_has_error(false);
ui.global::<ExportModel>()
.set_status_message(String::new().into());
*export_queue.borrow_mut() = Some(queue.clone());
start_next_export_job(ui, &render_ctx, &queue);
}
fn setup_check_remote_availability_callback(
ui: &MainWindow,
settings_store: &Arc<SettingsPersister>,
) {
let settings_store = settings_store.clone();
let ui_weak = ui.as_weak();
ui.global::<ExportModel>()
.on_check_remote_availability(move || {
let worker = settings_store.snapshot().settings.remote_worker();
let ui_weak_result = ui_weak.clone();
std::thread::spawn(move || {
let result = export_thread::probe_remote(worker.as_ref());
let _ = ui_weak_result.upgrade_in_event_loop(move |ui| match result {
Ok(_capability) => {
ui.global::<ExportModel>().set_remote_available(true);
ui.global::<ExportModel>()
.set_remote_unavailable_reason(String::new().into());
}
Err(reason) => {
ui.global::<ExportModel>().set_remote_available(false);
ui.global::<ExportModel>()
.set_remote_unavailable_reason(reason.message().into());
}
});
});
});
}
fn setup_populate_export_fanout_presets_callback(
ui: &MainWindow,
settings_store: &Arc<SettingsPersister>,
) {
let settings_store = settings_store.clone();
let ui_weak = ui.as_weak();
ui.global::<ExportModel>()
.on_populate_fanout_presets(move || {
let Some(ui) = ui_weak.upgrade() else {
return;
};
let snapshot = settings_store.snapshot();
let items: Vec<LightingPresetItem> = snapshot
.presets
.into_iter()
.filter(|p| p.export_usable)
.map(|p| LightingPresetItem {
name: p.name.into(),
built_in: p.built_in,
export_usable: true,
has_env_map: p.env_map_path.is_some(),
selected: false,
})
.collect();
ui.global::<ExportModel>()
.set_fanout_presets(ModelRc::new(VecModel::from(items)));
ui.global::<ExportModel>()
.set_directory(snapshot.settings.export_directory.into());
ui.global::<ExportModel>()
.set_filename_template(snapshot.settings.export_filename_template.into());
ui.global::<ExportModel>().set_transfer_index(
snapshot
.settings
.remote
.map_or(0, |r| r.export_transfer.index()),
);
});
}
fn setup_export_output_location_callbacks(
ui: &MainWindow,
settings_store: &Arc<SettingsPersister>,
) {
let settings_store_dir = settings_store.clone();
let ui_weak_dir = ui.as_weak();
ui.global::<ExportModel>().on_change_directory(move || {
let Some(ui) = ui_weak_dir.upgrade() else {
return;
};
let current = settings_store_dir.snapshot().settings.export_directory;
let settings_store_dir = settings_store_dir.clone();
crate::gui::pickers::pick(
&ui,
crate::gui::pickers::PickerRequest {
kind: crate::gui::pickers::PickerKind::PickFolder,
title: Some("Choose an export folder".to_string()),
filters: Vec::new(),
default_file_name: None,
starting_dir: (!current.is_empty()).then(|| PathBuf::from(current)),
},
move |ui, dir| {
let Some(dir) = dir else {
return;
};
let dir_string = dir.to_string_lossy().into_owned();
settings_store_dir.update(|s| s.settings.export_directory.clone_from(&dir_string));
ui.global::<ExportModel>().set_directory(dir_string.into());
},
);
});
let settings_store_template = settings_store.clone();
ui.global::<ExportModel>()
.on_filename_template_changed(move |text| {
settings_store_template
.update(|s| s.settings.export_filename_template = text.to_string());
});
ui.global::<ExportModel>().on_preview_filename_template(
move |template: slint::SharedString| {
let rendered = export_thread::filename_template::render(
&template,
&example_template_context(),
std::time::SystemTime::now(),
);
format!("{rendered}.png").into()
},
);
ui.global::<ExportModel>()
.on_filename_template_unknown_placeholders(move |template: slint::SharedString| {
unknown_placeholders(&template).into()
});
}
fn example_template_context() -> export_thread::TemplateContext {
export_thread::TemplateContext {
design: "Example Design".to_string(),
designer: "Jane Doe".to_string(),
shape: "Round Brilliant".to_string(),
material: "Diamond".to_string(),
ri: "2.417".to_string(),
width: 1920,
height: 1080,
spp: 512,
bounces: 12,
colorspace: "sRGB".to_string(),
preset: "Studio".to_string(),
lighting: "D65 Daylight".to_string(),
yaw_deg: 48.0,
pitch_deg: 54.0,
distance: 3.5,
exposure: 1.0,
}
}
fn unknown_placeholders(template: &str) -> String {
let rendered = export_thread::filename_template::render(
template,
&example_template_context(),
std::time::SystemTime::now(),
);
let mut found = Vec::new();
let mut rest = rendered.as_str();
while let Some(open) = rest.find('{') {
let after_open = &rest[open + 1..];
let Some(close) = after_open.find('}') else {
break;
};
let name = &after_open[..close];
if !found.iter().any(|f: &String| f == name) {
found.push(name.to_string());
}
rest = &after_open[close + 1..];
}
found.join(", ")
}
fn setup_toggle_export_preset_selected_callback(ui: &MainWindow) {
let ui_weak = ui.as_weak();
ui.global::<ExportModel>()
.on_toggle_preset_selected(move |idx: i32| {
let Some(ui) = ui_weak.upgrade() else {
return;
};
let Ok(idx) = usize::try_from(idx) else {
return;
};
let model = ui.global::<ExportModel>().get_fanout_presets();
if let Some(mut item) = model.row_data(idx) {
item.selected = !item.selected;
model.set_row_data(idx, item);
}
});
}