Lands the Render workspace (R3), bounded preview-image jobs with cancellation and atomic PNG export (R4a), and opaque PBR looks as one change. - Render workspace: an overlay-free product preview on the installed GPU device. Returning to Design restores its camera. render.scene and render.settings.apply are added. - Image jobs: render.preflight/start/status/cancel/export run bounded CPU preview jobs. Export captures the job, overwrite choice and document, and checks the caller's file scope before writing. - Opaque PBR: looks can carry metal fraction, roughness and dielectric IOR. The product viewport and preview PNG use them; geometry and mass are unchanged. The v1 catalog export refuses PBR rather than dropping it. - Render setups and PBR looks are saved under design format 21. Labels are in en/de/ja/ko/zh/ru. Supersedes #706 and #800. Refs #620, #673, #756, #1004. Contributed by @0xlillith. Co-authored-by: lillith <35337584+0xlillith@users.noreply.github.com>
80 lines
3.6 KiB
Rust
80 lines
3.6 KiB
Rust
//! Complete headless image-job workflow through the same engine commands as the UI/MCP.
|
|
use serde_json::json;
|
|
use solidcraft_engine::{
|
|
doc::render_settings::RenderSetup,
|
|
render::job::{CpuHost, JobId, Phase},
|
|
};
|
|
|
|
pub fn run(args: &[String]) -> Result<(), String> {
|
|
let input = args.first().filter(|p| !p.starts_with('-')).ok_or("render requires an input design or script")?;
|
|
let mut out = None;
|
|
let mut setup = None;
|
|
let mut width = None;
|
|
let mut height = None;
|
|
let mut transparent = false;
|
|
let mut overwrite = false;
|
|
let mut timeout = 120u64;
|
|
let mut i = 1;
|
|
while let Some(arg) = args.get(i) {
|
|
match arg.as_str() {
|
|
"--transparent" => transparent = true,
|
|
"--overwrite" => overwrite = true,
|
|
"--out" | "--setup" | "--width" | "--height" | "--timeout" => {
|
|
i += 1;
|
|
let value = args.get(i).ok_or_else(|| format!("missing value for {arg}"))?;
|
|
match arg.as_str() {
|
|
"--out" => out = Some(value.clone()),
|
|
"--setup" => setup = Some(value.clone()),
|
|
"--width" => width = Some(value.parse::<u32>().map_err(|_| "invalid width")?),
|
|
"--height" => height = Some(value.parse::<u32>().map_err(|_| "invalid height")?),
|
|
_ => timeout = value.parse::<u64>().map_err(|_| "invalid timeout")?,
|
|
}
|
|
}
|
|
_ => return Err(format!("unknown render option: {arg}")),
|
|
}
|
|
i += 1;
|
|
}
|
|
if !(1..=3600).contains(&timeout) {
|
|
return Err("render timeout must be 1..3600 seconds".into());
|
|
}
|
|
let out = out.ok_or("render requires --out FILE.png")?;
|
|
let mut session = crate::load(input)?;
|
|
session.set_render_job_host(Some(CpuHost::for_platform().map_err(|e| e.to_string())?)).map_err(|e| e.to_string())?;
|
|
session.execute("render.scene", &json!({})).map_err(|e| e.to_string())?;
|
|
let mut settings = if let Some(name) = setup {
|
|
session.doc.render.setups.get(&name).cloned().ok_or("no saved render setup with that name")?
|
|
} else {
|
|
let mut settings = RenderSetup::default();
|
|
if let Some(camera) = session.camera {
|
|
settings.camera = camera;
|
|
} else {
|
|
settings.camera.fit(&solidcraft_engine::product_scene::extract(&session).map_err(|e| e.to_string())?.bounds);
|
|
}
|
|
settings
|
|
};
|
|
if let Some(width) = width {
|
|
settings.width = width;
|
|
}
|
|
if let Some(height) = height {
|
|
settings.height = height;
|
|
}
|
|
let params = json!({"settings":settings,"transparent_background":transparent});
|
|
let preflight = session.execute("render.preflight", ¶ms).map_err(|e| e.to_string())?;
|
|
let started = session.execute("render.start", ¶ms).map_err(|e| e.to_string())?;
|
|
let id: JobId = serde_json::from_value(started["job"].clone()).map_err(|e| e.to_string())?;
|
|
let handle = session.render_job_handle(id).map_err(|e| e.to_string())?;
|
|
let status = match handle.wait_timeout(std::time::Duration::from_secs(timeout)) {
|
|
Ok(status) => status,
|
|
Err(e) => {
|
|
let _ = session.execute("render.cancel", &json!({"job":id}));
|
|
return Err(e.to_string());
|
|
}
|
|
};
|
|
if status.progress.phase != Phase::Completed {
|
|
return Err(status.error.map_or_else(|| "render cancelled".into(), |e| e.to_string()));
|
|
}
|
|
let exported = session.execute("render.export", &json!({"job":id,"path":out,"overwrite":overwrite})).map_err(|e| e.to_string())?;
|
|
println!("{}", crate::pretty(&json!({"preflight":preflight,"export":exported})));
|
|
Ok(())
|
|
}
|