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
@@ -98,6 +98,7 @@ pub mod preview;
|
||||
mod primitive_units_tests;
|
||||
pub mod recovery_ui;
|
||||
pub mod ref_images;
|
||||
pub mod render_workspace;
|
||||
#[cfg(test)]
|
||||
mod revolve_edit_tests;
|
||||
#[cfg(test)]
|
||||
@@ -217,7 +218,7 @@ impl Default for UiState {
|
||||
}
|
||||
}
|
||||
|
||||
/// Host services (file pickers) injected by the app so this crate stays portable.
|
||||
/// Host services injected by the app so this crate stays portable.
|
||||
#[derive(Default)]
|
||||
pub struct Services {
|
||||
pub pick_open: Option<Box<dyn Fn() -> FilePickerFuture>>,
|
||||
@@ -247,6 +248,7 @@ pub struct SolveApp {
|
||||
pub services: Services,
|
||||
file_picker: Option<file_picker::Pending>,
|
||||
pub viewport: viewport::ViewportState,
|
||||
pub render_workspace: render_workspace::State,
|
||||
pub tool: Option<tools::Tool>,
|
||||
pub dialog: Option<dialogs::Dialog>,
|
||||
/// The Change Parameters window, when open.
|
||||
@@ -331,7 +333,16 @@ pub struct SolveApp {
|
||||
}
|
||||
|
||||
impl SolveApp {
|
||||
pub fn new(session: Session, services: Services) -> Self {
|
||||
pub fn new(mut session: Session, services: Services) -> Self {
|
||||
#[cfg(test)]
|
||||
let image_host = Some(solidcraft_engine::render::job::CpuHost::cooperative());
|
||||
#[cfg(not(test))]
|
||||
let image_host = solidcraft_engine::render::job::CpuHost::for_platform().map_err(|e| log::error!("render image host: {e}")).ok();
|
||||
if let Err(e) = session.set_render_job_host(image_host.clone()) {
|
||||
log::error!("render image host: {e}");
|
||||
}
|
||||
let mut render_workspace = render_workspace::State::default();
|
||||
render_workspace.image_host = image_host;
|
||||
SolveApp {
|
||||
session,
|
||||
ui: UiState::default(),
|
||||
@@ -343,6 +354,7 @@ impl SolveApp {
|
||||
services,
|
||||
file_picker: None,
|
||||
viewport: viewport::ViewportState::default(),
|
||||
render_workspace,
|
||||
tool: None,
|
||||
dialog: None,
|
||||
params: None,
|
||||
@@ -408,6 +420,19 @@ impl SolveApp {
|
||||
/// stencil buffer bits and MSAA sample count). Without it the viewport renders on the CPU.
|
||||
pub fn set_wgpu(&mut self, rs: &egui_wgpu::RenderState, depth_bits: u8, stencil_bits: u8, samples: u32) {
|
||||
self.viewport.gpu = Some(gpu::install(rs, depth_bits, stencil_bits, samples, !self.cpu_meshing));
|
||||
render_workspace::invalidate_scene(self);
|
||||
self.render_workspace.host = match render_workspace::host_executor(
|
||||
rs.device.limits().max_texture_dimension_2d,
|
||||
self.viewport.gpu.is_some_and(|g| g.depth.is_some()),
|
||||
samples.max(1),
|
||||
) {
|
||||
Ok(executor) => Some(executor),
|
||||
Err(error) => {
|
||||
self.set_status(error, true);
|
||||
None
|
||||
}
|
||||
};
|
||||
render_workspace::sync_backend(self);
|
||||
self.msaa_supported = rs.msaa_supported.clone();
|
||||
let info = rs.adapter.get_info();
|
||||
self.help.gpu = Some(format!("{} ({:?}, {:?})", info.name.trim(), info.backend, info.device_type));
|
||||
@@ -541,6 +566,7 @@ impl SolveApp {
|
||||
let label = language.label(command_label(id));
|
||||
self.session.echo(language.format("ui.command_done", &[("command", label)]));
|
||||
if matches!(id, "file.new" | "doc.open" | "doc.recover") {
|
||||
render_workspace::reset_document(self);
|
||||
self.sketch_choices = sketch_families::Choices::default();
|
||||
dim_view::cancel();
|
||||
self.menu.props = None;
|
||||
@@ -882,6 +908,15 @@ impl SolveApp {
|
||||
}
|
||||
|
||||
pub fn fit_view(&mut self) {
|
||||
if render_workspace::active(self) {
|
||||
self.cancel_view_animation();
|
||||
match solidcraft_engine::product_scene::extract(&self.session) {
|
||||
Ok(scene) if !scene.bounds.is_empty() => self.cam.fit(&scene.bounds),
|
||||
Err(e) => self.set_status(e.to_string(), true),
|
||||
_ => {}
|
||||
}
|
||||
return;
|
||||
}
|
||||
self.cam_anim = None;
|
||||
self.viewport.orbit_snap = None;
|
||||
self.viewport.orbit_free = None;
|
||||
@@ -960,6 +995,13 @@ impl SolveApp {
|
||||
|
||||
/// Animated view changes for the axis view and navigation bar.
|
||||
pub fn animate_view(&mut self, v: &str) {
|
||||
if render_workspace::active(self) && matches!(v, "fit" | "home") {
|
||||
if v == "home" {
|
||||
self.cam.set_view(StandardView::Iso);
|
||||
}
|
||||
self.fit_view();
|
||||
return;
|
||||
}
|
||||
self.pending_fit = None;
|
||||
let mut to = self.cam;
|
||||
match v {
|
||||
@@ -993,7 +1035,11 @@ impl SolveApp {
|
||||
/// change, or at once (control requests, so a screenshot right after shows it).
|
||||
pub fn follow_view_request(&mut self, animate: bool) {
|
||||
let Some(mut to) = self.session.camera.take() else { return };
|
||||
to.up = self.cam.up;
|
||||
if render_workspace::active(self) {
|
||||
self.ui.up_axis = to.up;
|
||||
} else {
|
||||
to.up = self.cam.up;
|
||||
}
|
||||
if animate {
|
||||
self.animate_to(to);
|
||||
} else if to.is_valid() {
|
||||
@@ -1033,6 +1079,8 @@ impl SolveApp {
|
||||
/// Per-frame logic before layout.
|
||||
pub fn logic(&mut self, ctx: &egui::Context) {
|
||||
file_picker::tick(self, ctx);
|
||||
render_workspace::sync_backend(self);
|
||||
render_workspace::tick(self, ctx);
|
||||
file_load::tick(self, ctx);
|
||||
self.esc_handled = false;
|
||||
prefs::apply(self, ctx);
|
||||
@@ -1143,8 +1191,9 @@ impl SolveApp {
|
||||
}
|
||||
}
|
||||
toolbar::toolbar(self, ui);
|
||||
let left_timeline = self.ui.show_timeline && self.preferences.timeline_left;
|
||||
if self.ui.show_timeline && !left_timeline {
|
||||
let show_timeline = self.ui.show_timeline && !render_workspace::active(self);
|
||||
let left_timeline = show_timeline && self.preferences.timeline_left;
|
||||
if show_timeline && !left_timeline {
|
||||
timeline::timeline(self, ui);
|
||||
}
|
||||
palette::palette_bar(self, ui);
|
||||
@@ -1157,9 +1206,16 @@ impl SolveApp {
|
||||
} else if left_timeline {
|
||||
timeline_left::left_panel(self, ui);
|
||||
}
|
||||
if render_workspace::active(self) {
|
||||
render_workspace::panel(self, ui);
|
||||
}
|
||||
preview::update(self, ui.ctx());
|
||||
egui::CentralPanel::default().frame(egui::Frame::NONE).show(ui, |ui| {
|
||||
viewport::show(self, ui);
|
||||
if render_workspace::active(self) {
|
||||
render_workspace::show(self, ui);
|
||||
} else {
|
||||
viewport::show(self, ui);
|
||||
}
|
||||
});
|
||||
dialogs::show(self, ui.ctx());
|
||||
issue_report::CadInput::block(self, ui, report_input);
|
||||
@@ -1303,3 +1359,6 @@ pub fn now_ms() -> f64 {
|
||||
use web_time::{SystemTime, UNIX_EPOCH};
|
||||
SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs_f64() * 1000.0).unwrap_or(0.0)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod picker_test_support;
|
||||
Reference in new issue
Block a user