mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-26 10:57:20 -06:00
A command whose params document "layer":id acts on that layer, but its
availability check looked at the active layer. With another layer selected,
`layer.smartObjects.replaceContents {"layer": N, "path": …}` failed with "the
active layer is a Group layer, not a smart object", although layer N is a
smart object; likewise Rasterize, Export Contents, Merge Down and the rest.
Scripts, recorded actions and MCP agents hit this whenever the selection
differs from the layer they name.
Two allow-lists covered part of it: #466 (smart filters, Edit Contents,
Convert to Layers) and #2457 (hide, show, rename with nothing selected). The
#466 check now applies to every command that documents "layer":id (210
today; the old list is a subset): the precondition runs with the named layer
as the active one, then the active layer is restored. A name that isn't a
layer of the active document falls back to the usual check, so bad ids still
fail as before. It moves from smart_cmds to Session, as it isn't specific to
smart objects any more.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
75 lines
3.6 KiB
Rust
75 lines
3.6 KiB
Rust
//! A command that documents `"layer":id` checks its precondition on that layer, not on whichever
|
|
//! layer happens to be active (#466 for smart filters, generalised): a script, action or agent can
|
|
//! Replace Contents or Rasterize a smart object while another layer is selected.
|
|
|
|
use photocraft_doc::{LayerContent, LayerId};
|
|
use photocraft_engine::{EngineError, Session};
|
|
use photocraft_geom::Rect;
|
|
use serde_json::json;
|
|
|
|
/// A smart object (blue) under a plain pixel layer, which is active.
|
|
fn session() -> (Session, u64, u64) {
|
|
let mut s = Session::new();
|
|
s.execute("file.new", json!({"width": 8, "height": 6})).unwrap();
|
|
s.execute("layer.new.layer", json!({})).unwrap();
|
|
s.edit("paint", |doc, active| {
|
|
doc.layer_mut(active.unwrap()).unwrap().surface_mut().unwrap().fill_rect(Rect::new(0, 0, 8, 6), &[0.0, 0.0, 1.0, 1.0]);
|
|
Ok(())
|
|
})
|
|
.unwrap();
|
|
let so = s.execute("layer.smartObjects.convertToSmartObject", json!({})).unwrap()["layer"].as_u64().unwrap();
|
|
let pixels = s.execute("layer.new.layer", json!({})).unwrap()["layer"].as_u64().unwrap();
|
|
(s, so, pixels)
|
|
}
|
|
|
|
fn active(s: &Session) -> u64 {
|
|
s.active().unwrap().active_layer.unwrap().0
|
|
}
|
|
|
|
fn kind(s: &Session, id: u64) -> &'static str {
|
|
s.active().unwrap().doc.layer(LayerId(id)).unwrap().content.kind_name()
|
|
}
|
|
|
|
#[test]
|
|
fn smart_object_commands_act_on_a_named_layer_while_another_is_active() {
|
|
let (mut s, so, pixels) = session();
|
|
assert_eq!(active(&s), pixels);
|
|
// Without "layer" the active pixel layer decides, as the menus show.
|
|
assert!(!s.is_enabled("layer.smartObjects.replaceContents"));
|
|
// Replace Contents on the named smart object.
|
|
let dir = std::env::temp_dir().join(format!("pcraft-explicit-layer-{}", std::process::id()));
|
|
std::fs::create_dir_all(&dir).unwrap();
|
|
let png = dir.join("red.png");
|
|
let red = photocraft_doc::Document::with_background(
|
|
"r",
|
|
photocraft_geom::Size::new(8, 6),
|
|
photocraft_color::ColorMode::Rgb,
|
|
photocraft_color::SampleType::U8,
|
|
photocraft_color::Color::rgba(1.0, 0.0, 0.0, 1.0),
|
|
);
|
|
std::fs::write(&png, photocraft_io::export(&red, "png", &Default::default()).unwrap().bytes).unwrap();
|
|
s.execute("layer.smartObjects.replaceContents", json!({"layer": so, "path": png.to_str().unwrap()})).unwrap();
|
|
std::fs::remove_dir_all(&dir).ok();
|
|
let LayerContent::Smart(sm) = &s.active().unwrap().doc.layer(LayerId(so)).unwrap().content else { panic!("smart object") };
|
|
assert_eq!(sm.cache.as_ref().unwrap().rgba(2, 2), [1.0, 0.0, 0.0, 1.0], "the named smart object was replaced");
|
|
assert_eq!(active(&s), pixels, "the active layer stays");
|
|
// Rasterize it, still without selecting it.
|
|
s.execute("layer.rasterize.smartObject", json!({"layer": so})).unwrap();
|
|
assert_eq!(kind(&s, so), "Pixel");
|
|
assert_eq!(active(&s), pixels);
|
|
}
|
|
|
|
#[test]
|
|
fn a_named_layer_of_the_wrong_kind_or_that_does_not_exist_is_refused() {
|
|
let (mut s, _so, pixels) = session();
|
|
let before = s.active().unwrap().doc.clone();
|
|
// The named layer is checked: a pixel layer is not a smart object.
|
|
let r = s.execute("layer.smartObjects.rasterize", json!({"layer": pixels}));
|
|
assert!(matches!(r, Err(EngineError::Disabled(..))), "{r:?}");
|
|
// A layer that doesn't exist falls back to the active layer's check, and still fails.
|
|
assert!(s.execute("layer.smartObjects.rasterize", json!({"layer": 999_999})).is_err());
|
|
assert!(s.execute("layer.smartObjects.rasterize", json!({"layer": "nope"})).is_err());
|
|
assert_eq!(s.active().unwrap().doc, before);
|
|
assert_eq!(active(&s), pixels);
|
|
}
|