Render: integrated workspace, preview image jobs and opaque PBR (#1046)

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>
This commit is contained in:
selune authored and GitHub committed 2026-10-14 19:24:36 -04:00
1 parent e4ef6cfe29
commit a261aaa841
70 files changed
+5860 -101

No files matched your search

+37 -3
View File
@@ -12,11 +12,34 @@ use crate::Document;
/// A look: a name (a library entry's, or "Custom"), sRGB colour and opacity (1 = opaque).
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(try_from = "LookWire")]
pub struct Look {
pub name: String,
pub color: [u8; 3],
#[serde(default = "opaque")]
pub opacity: f64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pbr: Option<crate::pbr_appearance::PbrParameters>,
}
#[derive(Deserialize)]
struct LookWire {
name: String,
color: [u8; 3],
#[serde(default = "opaque")]
opacity: f64,
#[serde(default)]
pbr: Option<crate::pbr_appearance::PbrParameters>,
}
impl TryFrom<LookWire> for Look {
type Error = String;
fn try_from(value: LookWire) -> std::result::Result<Self, Self::Error> {
if value.pbr.is_some() && value.opacity != 1.0 {
return Err("opaque PBR requires opacity 1; refraction/transmission is not supported".into());
}
Ok(Self { name: value.name, color: value.color, opacity: value.opacity, pbr: value.pbr })
}
}
fn opaque() -> f64 {
@@ -25,7 +48,13 @@ fn opaque() -> f64 {
impl Look {
pub fn custom(color: [u8; 3], opacity: f64) -> Look {
Look { name: "Custom".into(), color, opacity: opacity.clamp(0.0, 1.0) }
Look { name: "Custom".into(), color, opacity: opacity.clamp(0.0, 1.0), pbr: None }
}
pub fn opaque_surface(&self) -> crate::Result<Option<solidcraft_render::surface::OpaqueSurface>> {
if self.pbr.is_some() && self.opacity != 1.0 {
return Err(crate::DocError::Invalid("opaque PBR requires opacity 1".into()));
}
self.pbr.map(|p| p.surface(self.color).map_err(|e| crate::DocError::Invalid(e.to_string()))).transpose()
}
/// Colour as 0–1 fractions.
pub fn rgb(&self) -> [f32; 3] {
@@ -109,14 +138,19 @@ pub use defaults::LIBRARY;
/// A library appearance by name (case-insensitive).
pub fn library(name: &str) -> Option<Look> {
LIBRARY.iter().find(|(n, _, _)| n.eq_ignore_ascii_case(name.trim())).map(|(n, c, o)| Look { name: n.to_string(), color: *c, opacity: *o })
LIBRARY.iter().find(|(n, _, _)| n.eq_ignore_ascii_case(name.trim())).map(|(n, c, o)| Look {
name: n.to_string(),
color: *c,
opacity: *o,
pbr: None,
})
}
/// The look a physical material gives a body that has no appearance of its own.
pub fn material_look(material: &str) -> Option<Look> {
let color = crate::material_color(material)?;
let opacity = if material == "Glass" { 0.25 } else { 1.0 };
Some(Look { name: material.to_string(), color, opacity })
Some(Look { name: material.to_string(), color, opacity, pbr: None })
}
impl Document {