use super::{controller::unix_now, wiring::Deps};
use crate::{
ExportModel, MainWindow,
bridge::export_thread::{
self, ExportParams, RemoteSelection, SceneSnapshot, filename_template::resolve_export_name,
},
gui::render::render_export::{
queue::{
DesignNames, ExportJob, ExportSettingsView, apply_export_bounce_cap, colorspace_label,
compute_target_from_index, template_context,
},
wiring::{design_naming, fan_out_jobs, resolve_export_dir_then},
},
settings::ExportTransfer,
};
use indicatrix::color::ColorSpace;
use indicatrix_render_jobs::{
codec,
job::{DesignInfo, JOB_FORMAT_VERSION},
paths::reserve_unique_file,
};
use indicatrix_vault::model::render_job::NewRenderJob;
use slint::ComponentHandle;
use std::{
collections::VecDeque,
path::{Path, PathBuf},
};
#[derive(Debug, Clone, Copy)]
pub(super) struct StillArgs {
pub(super) width: i32,
pub(super) height: i32,
pub(super) samples: i32,
pub(super) color_space_index: i32,
pub(super) compute_target_index: i32,
pub(super) max_bounces: i32,
}
pub fn plan_still_outputs(
export_dir: &Path,
names: &[String],
taken: &[PathBuf],
exists: &dyn Fn(&Path) -> bool,
) -> Vec<PathBuf> {
let mut reserved: Vec<PathBuf> = taken.to_vec();
let mut planned = Vec::with_capacity(names.len());
for name in names {
let path = reserve_unique_file(&export_dir.join(name), &reserved, exists);
reserved.push(path.clone());
planned.push(path);
}
planned
}
pub fn still_label(design: &str, preset: &str, width: u32, height: u32) -> String {
let view = if preset.is_empty() {
"Current view"
} else {
preset
};
format!("{design} \u{b7} {view} \u{b7} {width}\u{d7}{height}")
}
pub fn still_summary(params: &ExportParams, color_space: &str, lighting: &str) -> String {
format!(
"{} \u{d7} {} \u{b7} {} samples \u{b7} {} bounces \u{b7} {color_space} \u{b7} {lighting}",
params.width, params.height, params.samples_per_pixel, params.max_bounces
)
}
fn refuse(ui: &MainWindow, message: &str) {
let model = ui.global::<ExportModel>();
model.set_has_error(true);
model.set_status_message(message.into());
}
pub(super) fn add_still_jobs(ui: &MainWindow, deps: &Deps, args: StillArgs) {
let params = match export_thread::validate_export_params(
args.width,
args.height,
args.samples,
args.max_bounces,
) {
Ok(params) => params,
Err(message) => {
refuse(ui, &message);
return;
}
};
let color_space = crate::gui::color_space_from_index(args.color_space_index);
let snapshot = deps.settings_store.snapshot();
let remote = RemoteSelection {
compute_target: compute_target_from_index(args.compute_target_index),
worker: snapshot.settings.remote_worker(),
transfer: ExportTransfer::from_index(ui.global::<ExportModel>().get_transfer_index()),
contribute_local: snapshot.settings.contribute_to_final_picture,
};
let deps = deps.clone();
let settings_store = deps.settings_store.clone();
resolve_export_dir_then(ui, &settings_store, "Nothing was added.", move |ui, dir| {
let Some(export_dir) = dir else {
return;
};
let render_ctx = deps.render_ctx.clone();
crate::gui::editor::finished_stone_then(
ui,
&render_ctx,
|ui| {
let model = ui.global::<ExportModel>();
model.set_has_error(false);
model.set_status_message("Preparing the finished stone...".into());
},
move |ui, finished| match finished {
Ok(finished) => {
let request = AddRequest {
params,
color_space,
remote,
export_dir,
};
continue_add(ui, &deps, &request, finished.as_ref());
}
Err(withheld) => refuse(
ui,
&withheld.export_message().replacen(
"Nothing was exported",
"Nothing was added",
1,
),
),
},
);
});
}
struct AddRequest {
params: ExportParams,
color_space: ColorSpace,
remote: RemoteSelection,
export_dir: PathBuf,
}
struct Prepared {
label: String,
summary: String,
output: PathBuf,
text: String,
}
struct Naming {
design: String,
designer: String,
shape: String,
ri: String,
template: String,
}
fn continue_add(
ui: &MainWindow,
deps: &Deps,
request: &AddRequest,
finished: Option<&indicatrix_solid::preview::StoneGeometryBuf>,
) {
let base_scene = match SceneSnapshot::capture_finished(&deps.render_ctx, finished) {
Ok(scene) => apply_export_bounce_cap(scene, request.params.max_bounces),
Err(reason) => {
refuse(ui, &reason);
return;
}
};
let jobs = fan_out_jobs(
ui,
&deps.settings_store,
&base_scene,
&deps.mesh_bounding_radius,
);
let (design, designer, shape, ri) = design_naming(ui, &deps.render_ctx);
let naming = Naming {
design,
designer,
shape,
ri,
template: deps
.settings_store
.snapshot()
.settings
.export_filename_template,
};
let listed = deps
.db
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.render_job_output_paths();
let taken = match listed {
Ok(paths) => paths.into_iter().map(PathBuf::from).collect::<Vec<_>>(),
Err(error) => {
refuse(
ui,
&format!("The render job list could not be read: {error}"),
);
return;
}
};
let now = unix_now();
let prepared = match prepare_jobs(request, &jobs, &naming, &taken, now) {
Ok(prepared) => prepared,
Err(message) => {
refuse(ui, &message);
return;
}
};
let total = prepared.len();
let stored = store(deps, &prepared, now);
super::controller::with_controller(super::controller::JobController::refresh);
match stored {
Ok(()) => {
let message = format!(
"Added {total} render {} to the queue. Start {} from File > Render Jobs...",
if total == 1 { "job" } else { "jobs" },
if total == 1 { "it" } else { "them" },
);
let model = ui.global::<ExportModel>();
model.set_has_error(false);
model.set_status_message(message.as_str().into());
crate::gui::show_toast(ui, &message, "info");
}
Err((stored, message)) => refuse(
ui,
&format!("{message} ({stored} of {total} jobs were added.)"),
),
}
}
fn prepare_jobs(
request: &AddRequest,
jobs: &VecDeque<ExportJob>,
naming: &Naming,
taken: &[PathBuf],
now: i64,
) -> Result<Vec<Prepared>, String> {
let params = request.params;
let colorspace = colorspace_label(request.color_space);
let names: Vec<String> = jobs
.iter()
.map(|job| {
let context = template_context(
DesignNames {
design: &naming.design,
designer: &naming.designer,
shape: &naming.shape,
ri: &naming.ri,
},
ExportSettingsView {
width: params.width,
height: params.height,
spp: params.samples_per_pixel,
bounces: params.max_bounces,
colorspace,
},
job,
);
resolve_export_name(&naming.template, &context)
})
.collect();
let outputs = plan_still_outputs(&request.export_dir, &names, taken, &|path| path.exists());
let mut prepared = Vec::with_capacity(jobs.len());
for (job, output) in jobs.iter().zip(outputs) {
let label = still_label(
&naming.design,
&job.preset_label,
params.width,
params.height,
);
let file = super::convert::build_still_job(
&super::convert::StillJobInputs {
scene: &job.scene,
params,
color_space: request.color_space,
remote: &request.remote,
output: &output,
label: label.clone(),
design: DesignInfo {
title: naming.design.clone(),
designer: naming.designer.clone(),
shape: naming.shape.clone(),
ri: naming.ri.clone(),
material: job.scene.material.name.clone(),
},
preset_label: job.preset_label.clone(),
},
now,
)?;
let text = codec::to_text(&file).map_err(|error| error.to_string())?;
prepared.push(Prepared {
label,
summary: still_summary(¶ms, colorspace, job.scene.lighting_preset.label()),
output,
text,
});
}
Ok(prepared)
}
fn store(deps: &Deps, prepared: &[Prepared], now: i64) -> Result<(), (usize, String)> {
let db = deps
.db
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
for (stored, job) in prepared.iter().enumerate() {
let output = job.output.to_string_lossy();
db.add_render_job(
&NewRenderJob {
kind: "still",
label: &job.label,
summary: &job.summary,
frames_total: 1,
output_path: &output,
snapshot_version: JOB_FORMAT_VERSION,
snapshot: &job.text,
},
now,
)
.map_err(|error| (stored, format!("The job could not be saved: {error}")))?;
}
drop(db);
Ok(())
}