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:
1 parent
e4ef6cfe29
commit
a261aaa841
70 files changed
+5860
-101
No files matched your search
@@ -0,0 +1,198 @@
|
||||
//! Product extraction shares evaluated placement and appearance resolution, not viewport policy.
|
||||
|
||||
use crate::geom::Aabb3;
|
||||
use crate::render::{FaceAppearance, ProductMesh, ProductScene};
|
||||
use crate::{EngineError, Result, Session};
|
||||
|
||||
pub const MAX_PRODUCT_BODIES: usize = 16_384;
|
||||
|
||||
/// Extract authoritative, visible geometry. Does not settle a live gesture implicitly.
|
||||
/// `render.scene` is the explicit model-reading command for that boundary.
|
||||
pub fn extract(s: &Session) -> Result<ProductScene> {
|
||||
if s.model.is_stale() {
|
||||
return Err(EngineError::Other("render scene is stale; finish the modelling gesture before previewing".into()));
|
||||
}
|
||||
let world = s.world_state();
|
||||
let mut scene = ProductScene { meshes: Vec::new(), bounds: Aabb3::EMPTY };
|
||||
for body in world.bodies.iter().filter(|b| s.body_visible(&b.name)) {
|
||||
if scene.meshes.len() >= MAX_PRODUCT_BODIES {
|
||||
return Err(EngineError::Other("render scene exceeds 16384 visible bodies".into()));
|
||||
}
|
||||
let mesh = body.mesh();
|
||||
scene.bounds = scene.bounds.union(&mesh.bounds());
|
||||
let to8 = |v: f32| (v.clamp(0.0, 1.0) * 255.0).round() as u8;
|
||||
let color = s.doc.body_color(body).map(|c| c.map(to8)).unwrap_or([176, 186, 198]);
|
||||
let faces = s
|
||||
.doc
|
||||
.face_colors(body)
|
||||
.into_iter()
|
||||
.map(|(face, look)| {
|
||||
Ok(FaceAppearance {
|
||||
face: u32::try_from(face).map_err(|_| EngineError::Other("render face index exceeds the mesh contract".into()))?,
|
||||
color: look.color,
|
||||
opacity: look.opacity.clamp(0.0, 1.0),
|
||||
surface: look.opaque_surface().map_err(|e| EngineError::Other(e.to_string()))?,
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
let surface = s.doc.model_body_look(body).map(|l| l.opaque_surface()).transpose().map_err(|e| EngineError::Other(e.to_string()))?.flatten();
|
||||
scene.meshes.push(ProductMesh {
|
||||
name: body.name.clone(),
|
||||
mesh,
|
||||
color,
|
||||
opacity: s.doc.model_body_opacity(body).clamp(0.0, 1.0),
|
||||
surface,
|
||||
faces,
|
||||
});
|
||||
}
|
||||
Ok(scene)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::Sel;
|
||||
use crate::geom::Vec3;
|
||||
use crate::render::*;
|
||||
use serde_json::json;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[test]
|
||||
fn product_scene_keeps_placed_appearances_and_meshes_without_design_overlays() {
|
||||
let mut s = Session::default();
|
||||
let component = s.execute("component.create", &json!({"name":"Part"})).unwrap();
|
||||
s.execute("solid.box", &json!({"length":10,"width":10,"height":10,"body_name":"Block"})).unwrap();
|
||||
s.execute("appearance.assign", &json!({"bodies":["Block"],"color":"#20a080","opacity":0.4})).unwrap();
|
||||
s.execute("appearance.assign", &json!({"faces":[[5,5,10]],"color":"#ff0000","opacity":0.7})).unwrap();
|
||||
s.execute("component.activate", &json!({"component":0})).unwrap();
|
||||
s.execute("occurrence.copy", &json!({"component":component["component"],"translate":[30,0,0]})).unwrap();
|
||||
s.execute("occurrence.move", &json!({"occurrence":component["occurrence"],"translate":[10,0,0]})).unwrap();
|
||||
let doc = s.doc.clone();
|
||||
s.evaluations = 0;
|
||||
let first = extract(&s).unwrap();
|
||||
assert_eq!(first.meshes.len(), 2);
|
||||
assert_eq!(first.bounds.min, Vec3::new(10.0, 0.0, 0.0));
|
||||
assert_eq!(first.bounds.max, Vec3::new(40.0, 10.0, 10.0));
|
||||
for m in &first.meshes {
|
||||
assert_eq!(m.color, [32, 160, 128]);
|
||||
assert_eq!(m.opacity, 0.4);
|
||||
assert!(m.faces.iter().any(|f| f.color == [255, 0, 0] && f.opacity == 0.7));
|
||||
assert!(Arc::ptr_eq(&m.mesh, &s.world_state().body(&m.name).unwrap().mesh()));
|
||||
}
|
||||
s.selection = vec![Sel::Body { name: first.meshes[0].name.clone() }];
|
||||
s.section = Some((Vec3::ZERO, Vec3::X));
|
||||
s.analysis = Some(crate::SurfaceAnalysis::Zebra { stripes: 10.0 });
|
||||
let after = extract(&s).unwrap();
|
||||
for (a, b) in first.meshes.iter().zip(&after.meshes) {
|
||||
assert!(Arc::ptr_eq(&a.mesh, &b.mesh));
|
||||
assert_eq!((a.color, a.opacity, &a.faces), (b.color, b.opacity, &b.faces));
|
||||
}
|
||||
s.active_component = component["component"].as_u64().unwrap();
|
||||
let active = extract(&s).unwrap();
|
||||
assert_eq!(active.bounds.min, first.bounds.min);
|
||||
assert_eq!(active.meshes[1].opacity, 0.4, "no active-component fading");
|
||||
s.visibility.hidden_bodies.push(first.meshes[1].name.clone());
|
||||
assert_eq!(extract(&s).unwrap().meshes.len(), 1);
|
||||
assert!(Arc::ptr_eq(&doc, &s.doc));
|
||||
assert_eq!(s.evaluations, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stale_geometry_requires_an_explicit_model_read_and_camera_apply_preserves_staleness() {
|
||||
let mut s = Session::default();
|
||||
for (id, p) in [
|
||||
("sketch.create", json!({"plane":"XY"})),
|
||||
("sketch.rectangle.two_point", json!({"p0":[5,5],"p1":[25,20]})),
|
||||
("sketch.dimension", json!({"entities":["l1"],"value":20})),
|
||||
("sketch.dimension", json!({"entities":["l2"],"value":15})),
|
||||
("sketch.finish", json!({})),
|
||||
("solid.extrude", json!({"distance":5})),
|
||||
("sketch.edit", json!({"sketch":"Sketch1"})),
|
||||
("sketch.drag", json!({"entity":"l1.start","from":[5,5],"to":[4,5],"live":true})),
|
||||
] {
|
||||
s.execute(id, &p).unwrap();
|
||||
}
|
||||
assert!(s.model.is_stale());
|
||||
assert!(extract(&s).is_err());
|
||||
s.evaluations = 0;
|
||||
let camera = Camera { up: UpAxis::Y, roll: 0.25, ..Default::default() };
|
||||
s.execute("render.settings.set", &json!({"name":"Photo","set":{"camera":camera}})).unwrap();
|
||||
let doc = s.doc.clone();
|
||||
let undo = s.undo.len();
|
||||
s.execute("render.settings.apply", &json!({"name":"Photo"})).unwrap();
|
||||
assert_eq!(s.camera, Some(camera));
|
||||
assert!(s.model.is_stale());
|
||||
assert_eq!(s.evaluations, 0);
|
||||
assert!(Arc::ptr_eq(&doc, &s.doc));
|
||||
assert_eq!(s.undo.len(), undo);
|
||||
let metadata = s.execute("render.scene", &json!({})).unwrap();
|
||||
assert!(!s.model.is_stale());
|
||||
assert!(s.evaluations > 0);
|
||||
assert_eq!(metadata["overlays"], false);
|
||||
assert_eq!(metadata["bodies"].as_array().unwrap().len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn host_capabilities_are_validated_runtime_only_and_survive_new_design() {
|
||||
let mut s = Session::default();
|
||||
let viewport = ExecutorDescriptor {
|
||||
id: ExecutorId::new("gpu-viewport").unwrap(),
|
||||
availability: Availability::Available,
|
||||
profiles: vec![RenderProfile {
|
||||
mode: RenderMode::Preview,
|
||||
operation: RenderOperation::Viewport,
|
||||
formats: Vec::new(),
|
||||
features: Vec::new(),
|
||||
}],
|
||||
limits: RenderLimits::default(),
|
||||
scratch: ScratchLayout { sample_scale: 1, sample_bytes_per_pixel: 8, output_bytes_per_pixel: 8, encoding_bytes_per_pixel: 0 },
|
||||
};
|
||||
let doc = s.doc.clone();
|
||||
s.set_render_viewport(Some(viewport.clone())).unwrap();
|
||||
assert!(Arc::ptr_eq(&doc, &s.doc));
|
||||
assert_eq!(s.evaluations, 0);
|
||||
assert!(!s.is_dirty());
|
||||
let backend = s.render_backend();
|
||||
s.replace_design(Session::default());
|
||||
assert_eq!(s.render_viewport(), Some(&viewport));
|
||||
assert_eq!(s.render_backend(), backend);
|
||||
assert_eq!(backend.executors.len(), 2);
|
||||
assert_eq!(s.scratch().render_backend(), backend);
|
||||
let mut invalid = viewport;
|
||||
invalid.id = ExecutorId::new("cpu-raster").unwrap();
|
||||
assert!(s.set_render_viewport(Some(invalid)).is_err());
|
||||
assert_eq!(s.render_backend(), backend);
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
assert_eq!(s.render_backend(), backend);
|
||||
assert!(!s.doc.to_json().contains("gpu-viewport"));
|
||||
s.set_render_viewport(None).unwrap();
|
||||
assert_eq!(s.render_backend(), builtin_backend());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn imported_looks_survive_product_extraction_and_design_overrides() {
|
||||
let body = crate::kernel::box_solid(Vec3::ZERO, Vec3::new(10.0, 10.0, 10.0))
|
||||
.unwrap()
|
||||
.with_color(Some([0.2, 0.6, 0.8]))
|
||||
.with_paint(Some(crate::kernel::Paint { opacity: 0.4, faces: Vec::new() }));
|
||||
let step = crate::kernel::step_export_bodies(
|
||||
&[crate::kernel::ExportBody { name: "Imported glass".into(), body: &body, color: body.color() }],
|
||||
&Default::default(),
|
||||
)
|
||||
.unwrap();
|
||||
let mut document = crate::doc::Document::default();
|
||||
document
|
||||
.add_feature(crate::doc::FeatureKind::Import { file: "original-generated.step".into(), step: step.into(), components: Vec::new() }, None)
|
||||
.unwrap();
|
||||
let mut s = Session::new(document);
|
||||
let first = extract(&s).unwrap();
|
||||
assert_eq!(first.meshes[0].color, [51, 153, 204]);
|
||||
assert!((first.meshes[0].opacity - 0.4).abs() < 1e-6);
|
||||
let name = first.meshes[0].name.clone();
|
||||
s.execute("appearance.assign", &json!({"bodies":[name],"color":"#336699","opacity":0.7})).unwrap();
|
||||
let changed = extract(&s).unwrap();
|
||||
assert_eq!(changed.meshes[0].color, [51, 102, 153]);
|
||||
assert_eq!(changed.meshes[0].opacity, 0.7);
|
||||
assert!(Arc::ptr_eq(&first.meshes[0].mesh, &changed.meshes[0].mesh));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user