From 116c8bae3c001d3e5dedb701a2303bde6dece3fa Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sun, 15 Nov 2026 00:16:48 -0800 Subject: [PATCH] Place: PSD files placed without Show Import Options take the last PSD Import Options With Show Import Options off, a PSD or PSB file placed on its own and embedded takes the conversion, hidden-layer and Import Slices choices made last in PSD Import Options, as text and DXF files take theirs. An importSlices or hiddenLayers the params give is kept. Files from bytes, which have no file to link to, are embedded even when the Place dialog's Link is on, alone and in the place cursor. With Show Import Options on, each PSD file of the place cursor asks for its options. --- crates/ui-egui/src/dialogs/psd_import.rs | 118 +++++++++++++++++++++++ crates/ui-egui/src/place.rs | 16 +-- docs/mcp.md | 10 +- docs/target-app-parity.md | 2 +- 4 files changed, 135 insertions(+), 11 deletions(-) diff --git a/crates/ui-egui/src/dialogs/psd_import.rs b/crates/ui-egui/src/dialogs/psd_import.rs index 108495e2cc..75fd0e0f11 100644 --- a/crates/ui-egui/src/dialogs/psd_import.rs +++ b/crates/ui-egui/src/dialogs/psd_import.rs @@ -96,6 +96,28 @@ pub fn queue_options(app: &VectorcraftApp, q: &mut Value) { q["importSlices"] = json!(slices); } +/// `file.place` params `p` placed without asking (Show Import Options off) that embed a PSD or PSB +/// file and give no `layers` take the PSD Import Options chosen last; `hiddenLayers` and +/// `importSlices` that the params give are kept. +pub fn last_options(app: &VectorcraftApp, p: &mut Value) { + let embedded = p.get("path").is_none() || p.get("link").and_then(Value::as_bool) == Some(false); + let name = p.get("path").or_else(|| p.get("name")).and_then(Value::as_str).unwrap_or_default(); + if embedded && p.get("layers").is_none() && matches!(fileio::extension(name).as_str(), "psd" | "pdd" | "psb") { + let (layers, hidden, slices) = last_choices(app); + p["layers"] = json!(layers); + if layers && hidden && p.get("hiddenLayers").is_none() { + p["hiddenLayers"] = json!(true); + } + if p.get("importSlices").is_none() { + p["importSlices"] = json!(slices); + } + if p.get("path").is_none() { + // A file from bytes has no file to link to; it may carry the Place dialog's Link. + p["link"] = json!(false); + } + } +} + /// OK: open or place the file with the choices. fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result { let file = app.ui.dialog_file.clone().ok_or("the file is no longer loaded")?; @@ -451,6 +473,102 @@ mod tests { assert_eq!(q["importSlices"], json!(false)); } + #[test] + fn placing_with_show_import_options_off_takes_the_last_choices() { + let b64 = vectorcraft_format::base64_encode(&sliced(&layered())); + let mut app = app(); + app.session.execute("file.new", &json!({"width": 100, "height": 100})).unwrap(); + // The file's bytes carry the Place dialog's Link, which they have no file for. + let place = |app: &mut VectorcraftApp, slices: Option| { + let mut p = json!({"name": "poster.psd", "dataBase64": b64, "link": true, "showImportOptions": false}); + if let Some(s) = slices { + p["importSlices"] = json!(s); + } + crate::place::run_for_user(app, &p).unwrap(); + assert!(app.ui.dialog.is_none(), "placed without asking"); + let st = app.session.doc().unwrap(); + let [id] = st.selection.objects[..] else { panic!("one object is selected") }; + (st.doc.node(id).unwrap().kind.clone(), st.doc.slices.len()) + }; + // At first, Convert Layers to Objects with Import Slices. + let (kind, slices) = place(&mut app, None); + assert!(matches!(kind, NodeKind::Group { .. })); + assert_eq!(slices, 1); + // An `importSlices` the params give is kept. + let (kind, slices) = place(&mut app, Some(false)); + assert!(matches!(kind, NodeKind::Group { .. })); + assert_eq!(slices, 1, "no new slice"); + // Then the choices made last. + app.ui.psd_import = json!({"layers": false, "importSlices": false}); + let (kind, slices) = place(&mut app, None); + assert!(matches!(kind, NodeKind::Image(_))); + assert_eq!(slices, 1, "no new slice"); + let (_, slices) = place(&mut app, Some(true)); + assert_eq!(slices, 2, "the given importSlices brings the slice in"); + } + + #[test] + fn files_from_bytes_placed_with_link_take_the_last_choices() { + let files: Vec = ["a.psd", "b.psd"].iter().map(|n| json!({ "name": n })).collect(); + let mut app = app(); + for n in ["a.psd", "b.psd"] { + app.place.picked.push((n.to_string(), sliced(&layered()))); + } + app.session.execute("file.new", &json!({"width": 100, "height": 100})).unwrap(); + // The dialog's Link is on, but files from bytes have no file to link to. + let d = Dialog::new(crate::dialogs::place::KIND, json!({"files": files, "link": true, "template": false, "showImportOptions": false})); + crate::place::confirm(&mut app, &d).unwrap(); + assert!(app.ui.dialog.is_none(), "the cursor loads without asking"); + let view = vectorcraft_engine::ViewInfo::default(); + for kind in [vectorcraft_tools::PointerKind::Down, vectorcraft_tools::PointerKind::Up] { + for r in app.session.pointer(&vectorcraft_tools::PointerEvent::new(kind, 10.0, 10.0), view).unwrap() { + if let vectorcraft_engine::UiRequest::Place(p) = r { + crate::place::place_from_cursor(&mut app, &p); + } + } + } + let st = app.session.doc().unwrap(); + let [id] = st.selection.objects[..] else { panic!("one object is placed") }; + // At first, Convert Layers to Objects with Import Slices. + assert!(matches!(st.doc.node(id).unwrap().kind, NodeKind::Group { .. })); + assert_eq!(st.doc.slices.len(), 1); + } + + #[test] + fn a_cursor_loaded_with_show_import_options_on_asks_for_each_psd_file() { + let dir = std::env::temp_dir().join(format!("vc-psd-import-asks-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let paths: Vec = ["a.psd", "b.psd"] + .iter() + .map(|n| { + let p = dir.join(n); + std::fs::write(&p, layered()).unwrap(); + p.to_string_lossy().into_owned() + }) + .collect(); + let files: Vec = paths.iter().map(|p| json!({ "path": p })).collect(); + let services = crate::Services { read: Some(Box::new(|p: &str| std::fs::read(p).map_err(|e| e.to_string()))), ..Default::default() }; + let mut app = VectorcraftApp::new(Session::new(), services); + app.session.execute("file.new", &json!({"width": 100, "height": 100})).unwrap(); + // The choices made last would place the layers without asking with Show Import Options off. + app.ui.psd_import = json!({"layers": true, "importSlices": true}); + let d = Dialog::new(crate::dialogs::place::KIND, json!({"files": files, "link": false, "template": false, "showImportOptions": true})); + crate::place::confirm(&mut app, &d).unwrap(); + assert!(app.ui.dialog.is_none(), "the cursor loads without asking"); + assert_eq!(app.session.tool_options()["count"], 2); + // A click on the canvas: the file the cursor's request names asks. + let view = vectorcraft_engine::ViewInfo::default(); + for kind in [vectorcraft_tools::PointerKind::Down, vectorcraft_tools::PointerKind::Up] { + for r in app.session.pointer(&vectorcraft_tools::PointerEvent::new(kind, 10.0, 10.0), view).unwrap() { + if let vectorcraft_engine::UiRequest::Place(p) = r { + crate::place::place_from_cursor(&mut app, &p); + } + } + } + assert_eq!(app.ui.dialog.as_ref().map(|d| d.kind.as_str()), Some(KIND), "the clicked file asks"); + std::fs::remove_dir_all(&dir).ok(); + } + #[test] fn scripted_places_and_places_that_give_layers_dont_ask() { let b64 = vectorcraft_format::base64_encode(&layered()); diff --git a/crates/ui-egui/src/place.rs b/crates/ui-egui/src/place.rs index 662c0c0266..01979b4537 100644 --- a/crates/ui-egui/src/place.rs +++ b/crates/ui-egui/src/place.rs @@ -61,7 +61,7 @@ pub fn is_document(name: &str) -> bool { #[derive(Default)] pub struct PlaceState { /// Web: the bytes of the files picked for the Place dialog, by name. - picked: Vec<(String, Vec)>, + pub(crate) picked: Vec<(String, Vec)>, /// The loaded cursor's thumbnails by file name. thumbs: Vec<(String, Thumb)>, /// The Control bar's `image.info`, for (document uid, revision, image id). @@ -113,10 +113,11 @@ fn engine(app: &mut VectorcraftApp, id: &str, p: &Value) -> Result Result { place_file(app, p, Read::AtOnce) } @@ -155,6 +156,7 @@ fn place_file(app: &mut VectorcraftApp, p: &Value, read: Read) -> Result Result