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>
167 lines
7.6 KiB
Rust
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))));
|
|
}
|