Files
OneCAD/crates/ui-egui/src/dialogs_library_tests.rs
T
selune a261aaa841 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>
2026-10-14 19:24:36 -04:00

167 lines
7.6 KiB
Rust

use super::*;
use crate::{dialogs::Kind, scenario::Harness};
use solidcraft_engine::{Sel, Session};
#[test]
fn catalog_clicked_load_apply_export_and_ok_keep_one_design_undo_step() {
let mut h = Harness::new();
h.call("ui.resize", json!({"width":1600,"height":1400}));
let path = h.dir.join("source-library.json").to_string_lossy().into_owned();
let mut source = Session::default();
source.execute("solid.box", &json!({"length":10,"width":10,"height":10})).unwrap();
source.execute("appearance.assign", &json!({"bodies":["Body1"],"appearance":"Warm oak colour","color":[180,120,70],"opacity":0.8})).unwrap();
source.execute("library.export", &json!({"path":path})).unwrap();
h.app.run("solid.box", json!({"length":10,"width":10,"height":10})).unwrap();
h.app
.run(
"appearance.assign",
json!({"bodies":["Body1"],"color":[20,20,20],
"pbr":{"metallic":1.0,"roughness":0.3,"dielectric_ior":1.5}}),
)
.unwrap();
h.app.session.mark_saved();
h.app.session.selection = vec![Sel::Body { name: "Body1".into() }];
h.app.start("appearance.assign");
if let Some(dialog) = &mut h.app.dialog
&& let Kind::Appearance(state) = &mut dialog.kind
{
state.local.path = path;
}
h.frames(3);
let undo = h.app.session.undo.len();
h.step(&json!({"widget":"Load library"})).unwrap();
h.step(&json!({"click":{"handle":"library:entry:0"}})).unwrap();
h.step(&json!({"widget":"Apply library appearance"})).unwrap();
assert_eq!(h.app.session.doc.appearances.bodies["Body1"].name, "Warm oak colour");
assert!(h.app.session.doc.appearances.bodies["Body1"].pbr.is_none());
assert_eq!(h.app.session.undo.len(), undo + 1);
let revision = h.app.session.revision;
h.step(&json!({"widget":"Export design library..."})).unwrap();
let exported = h.dir.join("my-library.solidcraft-library.json");
let catalog = Catalog::read(&exported.to_string_lossy()).unwrap();
assert_eq!(catalog.entries.len(), 1);
assert_eq!(catalog.entries[0].definition.name(), "Warm oak colour");
assert_eq!(h.app.session.revision, revision);
h.step(&json!({"widget":"OK"})).unwrap();
assert!(h.app.dialog.is_none());
assert_eq!(h.app.session.undo.len(), undo + 1);
}
fn appearance_fixture() -> Result<(Harness, Arc<()>), String> {
let mut h = Harness::new();
h.app.run("solid.box", json!({"length":10,"width":10,"height":10}))?;
h.app.run("appearance.assign", json!({"bodies":["Body1"],"appearance":"Warm oak colour","color":[180,120,70]}))?;
h.app.session.mark_saved();
let token = start_appearance(&mut h)?;
Ok((h, token))
}
fn start_appearance(h: &mut Harness) -> Result<Arc<()>, String> {
h.app.start("appearance.assign");
match h.app.dialog.as_ref().map(|dialog| &dialog.kind) {
Some(Kind::Appearance(k)) => Ok(k.local.token.clone()),
_ => Err("appearance dialog missing".into()),
}
}
#[test]
fn async_library_export_holds_its_document_until_the_answer() {
use crate::picker_test_support::{Slot, save};
let (mut h, token) = appearance_fixture().unwrap();
let original = h.app.session.doc.clone();
let path = h.dir.join("delayed-library.json");
let ctx = egui::Context::default();
let slot: Slot = Default::default();
save(&mut h.app, &slot);
save_library(&mut h.app, token, false);
crate::file_picker::tick(&mut h.app, &ctx);
assert!(!path.exists());
crate::documents::new_design(&mut h.app);
let other = h.app.session.doc.clone();
*slot.borrow_mut() = Some(Some(path.to_string_lossy().into_owned()));
crate::file_picker::tick(&mut h.app, &ctx);
assert_eq!(h.app.docs.active, 0, "upstream holds tab changes while a picker is pending");
assert!(Arc::ptr_eq(&other, &h.app.session.doc));
let catalog = Catalog::read(&path.to_string_lossy()).unwrap();
assert_eq!(catalog.entries.len(), 1);
assert_eq!(catalog.entries[0].definition.name(), "Warm oak colour");
crate::documents::switch(&mut h.app, 0);
assert!(Arc::ptr_eq(&original, &h.app.session.doc));
assert!(!h.app.session.is_dirty());
}
#[test]
fn async_library_export_captures_overwrite_consent_and_cancel_writes_nothing() {
use crate::picker_test_support::{Slot, save};
let (mut h, token) = appearance_fixture().unwrap();
let path = h.dir.join("existing-library.json");
std::fs::write(&path, b"keep this file").unwrap();
let original = h.app.session.doc.clone();
let ctx = egui::Context::default();
let slot: Slot = Default::default();
save(&mut h.app, &slot);
save_library(&mut h.app, token.clone(), false);
crate::file_picker::tick(&mut h.app, &ctx);
editor(&mut h.app, &token).unwrap().local.overwrite = true;
*slot.borrow_mut() = Some(Some(path.to_string_lossy().into_owned()));
crate::file_picker::tick(&mut h.app, &ctx);
assert_eq!(std::fs::read(&path).unwrap(), b"keep this file");
assert!(matches!(&h.app.status, Some((_, _, true))));
save_library(&mut h.app, token, false);
crate::file_picker::tick(&mut h.app, &ctx);
*slot.borrow_mut() = Some(None);
crate::file_picker::tick(&mut h.app, &ctx);
assert!(!h.app.file_picker_busy());
assert_eq!(std::fs::read(&path).unwrap(), b"keep this file");
assert!(Arc::ptr_eq(&original, &h.app.session.doc));
}
#[test]
fn async_library_open_does_not_replace_a_new_appearance_editor() {
use crate::picker_test_support::{Slot, save};
let (mut h, token) = appearance_fixture().unwrap();
let path = h.dir.join("source-library.json");
h.app.run("library.export", json!({"path":path})).unwrap();
let original = h.app.session.doc.clone();
let ctx = egui::Context::default();
let slot: Slot = Default::default();
save(&mut h.app, &slot);
// Prepare a different editor before the picker holds user commands, then model
// an internal host replacing it while the old asynchronous answer is pending.
let original_editor = h.app.dialog.take();
let newer = start_appearance(&mut h).unwrap();
editor(&mut h.app, &newer).unwrap().local.path = "new editor draft".into();
let replacement_editor = h.app.dialog.take();
h.app.dialog = original_editor;
crate::picker_test_support::open(&mut h.app, &slot);
open_library(&mut h.app, token);
crate::file_picker::tick(&mut h.app, &ctx);
h.app.dialog = replacement_editor;
*slot.borrow_mut() = Some(Some(path.to_string_lossy().into_owned()));
crate::file_picker::tick(&mut h.app, &ctx);
let k = editor(&mut h.app, &newer).unwrap();
assert_eq!(k.local.path, "new editor draft");
assert!(k.local.catalog.is_none());
assert!(Arc::ptr_eq(&original, &h.app.session.doc));
}
#[test]
fn delayed_library_export_refuses_an_internally_changed_design_without_overwriting() {
use crate::picker_test_support::{Slot, save};
let (mut h, token) = appearance_fixture().unwrap();
let path = h.dir.join("keep.json");
std::fs::write(&path, b"keep library").unwrap();
let slot: Slot = Default::default();
save(&mut h.app, &slot);
save_library(&mut h.app, token, true);
h.app.session.execute("appearance.assign", &json!({"bodies":["Body1"],"color":[1,2,3]})).unwrap();
let changed = h.app.session.doc.clone();
let history = (h.app.session.undo.len(), h.app.session.revision);
*slot.borrow_mut() = Some(Some(path.to_string_lossy().into_owned()));
crate::file_picker::tick(&mut h.app, &egui::Context::default());
assert_eq!(std::fs::read(path).unwrap(), b"keep library");
assert!(Arc::ptr_eq(&changed, &h.app.session.doc));
assert_eq!((h.app.session.undo.len(), h.app.session.revision), history);
assert!(matches!(&h.app.status, Some((_, _, true))));
}