mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-23 11:45:56 +02:00
* Tests: bad-layer-id checks use an id that can't exist (#3343) Layer ids come from one process-wide counter (shared with document and smart-object ids, and bumped by ensure_ids_above on file loads). A full `cargo test` run passes ids like 9999, so a layer a test just created can get the "bad" id, the command succeeds and the assertion fails at random (seen on CI in type_extra_cmds::tests::vertical_orientation_renders_columns_inside_the_canvas). Use u64::MAX, which the counter never reaches, for every "no such layer" sentinel in the engine and ui-egui tests. Test-only change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Tests: use u64::MAX for bad-layer-id sentinels added on main since (#3343) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
146 lines
6.5 KiB
Rust
146 lines
6.5 KiB
Rust
//! Layer › Rasterize acts on every selected layer, as in Photoshop ("select the layer or layers
|
||
//! you'd like to rasterize"; the Layers panel's right-click item reads "Rasterize Layers"), not
|
||
//! only on the one that was clicked. An explicit `layer` param still targets exactly that layer.
|
||
|
||
use photocraft_doc::{LayerContent, LayerId};
|
||
use photocraft_engine::Session;
|
||
use serde_json::{Value, json};
|
||
|
||
fn kind(s: &Session, id: LayerId) -> &'static str {
|
||
match &s.active().unwrap().doc.layer(id).unwrap().content {
|
||
LayerContent::Text(_) => "type",
|
||
LayerContent::Shape(_) => "shape",
|
||
LayerContent::Fill(_) => "fill",
|
||
LayerContent::Smart(_) => "smart",
|
||
LayerContent::Raster(_) => "raster",
|
||
_ => "other",
|
||
}
|
||
}
|
||
|
||
fn kinds(s: &Session, ids: &[LayerId]) -> Vec<&'static str> {
|
||
ids.iter().map(|id| kind(s, *id)).collect()
|
||
}
|
||
|
||
fn id_of(r: Value) -> LayerId {
|
||
LayerId(r["layer"].as_u64().unwrap())
|
||
}
|
||
|
||
struct Scene {
|
||
s: Session,
|
||
text: LayerId,
|
||
shape: LayerId,
|
||
smart: LayerId,
|
||
fill: LayerId,
|
||
/// A shape layer that is never selected.
|
||
bystander: LayerId,
|
||
}
|
||
|
||
fn scene() -> Scene {
|
||
let mut s = Session::new();
|
||
s.execute("file.new", json!({"width": 64, "height": 64})).unwrap();
|
||
let text = id_of(s.execute("type.create", json!({"text": "Hi", "size": 24, "x": 4, "y": 30})).unwrap());
|
||
let shape = id_of(s.execute("shape.create", json!({"kind": "ellipse", "rect": [4, 4, 40, 30]})).unwrap());
|
||
s.execute("layer.new.layer", json!({"name": "Source"})).unwrap();
|
||
s.execute("paint.stroke", json!({"points": [[20, 20]], "size": 12})).unwrap();
|
||
let smart = id_of(s.execute("layer.smartObjects.convertToSmartObject", json!({})).unwrap());
|
||
let fill = id_of(s.execute("layer.newFillLayer.solidColor", json!({"color": "#00ff00"})).unwrap());
|
||
let bystander = id_of(s.execute("shape.create", json!({"kind": "rect", "rect": [30, 30, 60, 60]})).unwrap());
|
||
let sc = Scene { s, text, shape, smart, fill, bystander };
|
||
assert_eq!(kinds(&sc.s, &[sc.text, sc.shape, sc.smart, sc.fill, sc.bystander]), ["type", "shape", "smart", "fill", "shape"]);
|
||
sc
|
||
}
|
||
|
||
impl Scene {
|
||
/// Select `ids` with the last one active (like ⌘-clicking rows, then right-clicking the last).
|
||
fn select(&mut self, ids: &[LayerId]) {
|
||
self.s.execute("layer.select", json!({"layer": ids[0].0})).unwrap();
|
||
for id in &ids[1..] {
|
||
self.s.execute("layer.select", json!({"layer": id.0, "mode": "add"})).unwrap();
|
||
}
|
||
assert_eq!(self.s.active().unwrap().selected_layers().len(), ids.len());
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn rasterize_layer_converts_every_selected_layer_in_one_step() {
|
||
let mut sc = scene();
|
||
let all = [sc.text, sc.shape, sc.smart, sc.fill];
|
||
sc.select(&all);
|
||
let before = sc.s.active().unwrap().selected_layers();
|
||
let r = sc.s.execute("layer.rasterize.layer", json!({})).unwrap();
|
||
assert_eq!(r["layers"].as_array().unwrap().len(), 4, "{r}");
|
||
assert_eq!(kinds(&sc.s, &all), ["raster"; 4]);
|
||
assert_eq!(kind(&sc.s, sc.bystander), "shape", "an unselected layer is left alone");
|
||
assert_eq!(sc.s.active().unwrap().selected_layers(), before, "the selection survives");
|
||
// One history step brings all four back.
|
||
assert!(sc.s.undo());
|
||
assert_eq!(kinds(&sc.s, &all), ["type", "shape", "smart", "fill"]);
|
||
assert!(sc.s.redo());
|
||
assert_eq!(kinds(&sc.s, &all), ["raster"; 4]);
|
||
}
|
||
|
||
#[test]
|
||
fn a_kind_command_touches_only_the_selected_layers_of_that_kind() {
|
||
let mut sc = scene();
|
||
sc.select(&[sc.text, sc.shape, sc.smart]);
|
||
sc.s.execute("layer.rasterize.type", json!({})).unwrap();
|
||
assert_eq!(kinds(&sc.s, &[sc.text, sc.shape, sc.smart]), ["raster", "shape", "smart"]);
|
||
// Nothing selected is a type layer any more.
|
||
assert!(sc.s.execute("layer.rasterize.type", json!({})).is_err());
|
||
sc.s.execute("layer.rasterize.shape", json!({})).unwrap();
|
||
assert_eq!(kinds(&sc.s, &[sc.shape, sc.smart, sc.bystander]), ["raster", "smart", "shape"]);
|
||
sc.s.execute("layer.rasterize.smartObject", json!({})).unwrap();
|
||
assert_eq!(kind(&sc.s, sc.smart), "raster");
|
||
assert_eq!(kind(&sc.s, sc.bystander), "shape");
|
||
}
|
||
|
||
#[test]
|
||
fn an_explicit_layer_param_stays_a_single_layer() {
|
||
// The "Rasterize?" prompt and scripts pass `layer`: the rest of the selection is not touched.
|
||
let mut sc = scene();
|
||
sc.select(&[sc.text, sc.shape, sc.smart]);
|
||
sc.s.execute("layer.rasterize.layer", json!({"layer": sc.shape.0})).unwrap();
|
||
assert_eq!(kinds(&sc.s, &[sc.text, sc.shape, sc.smart]), ["type", "raster", "smart"]);
|
||
sc.s.execute("layer.rasterize.shape", json!({"layer": sc.bystander.0})).unwrap();
|
||
assert_eq!(kind(&sc.s, sc.bystander), "raster");
|
||
assert_eq!(kinds(&sc.s, &[sc.text, sc.smart]), ["type", "smart"]);
|
||
}
|
||
|
||
#[test]
|
||
fn a_single_selected_layer_behaves_as_before() {
|
||
let mut sc = scene();
|
||
sc.select(&[sc.text]);
|
||
let r = sc.s.execute("layer.rasterize.layer", json!({})).unwrap();
|
||
assert_eq!(r["layer"].as_u64(), Some(sc.text.0));
|
||
assert_eq!(kinds(&sc.s, &[sc.text, sc.shape]), ["raster", "shape"]);
|
||
// A layer with nothing to rasterize reports it, and no history step is added.
|
||
let err = sc.s.execute("layer.rasterize.layer", json!({})).unwrap_err().to_string();
|
||
assert!(err.contains("nothing to rasterize"), "{err}");
|
||
let err = sc.s.execute("layer.rasterize.type", json!({})).unwrap_err().to_string();
|
||
assert!(err.contains("not a type layer"), "{err}");
|
||
}
|
||
|
||
#[test]
|
||
fn a_selection_with_nothing_to_rasterize_is_an_error_not_a_step() {
|
||
let mut sc = scene();
|
||
sc.s.execute("layer.rasterize.layer", json!({"layer": sc.text.0})).unwrap();
|
||
sc.s.execute("layer.rasterize.layer", json!({"layer": sc.shape.0})).unwrap();
|
||
sc.select(&[sc.text, sc.shape]);
|
||
let revision = sc.s.active().unwrap().revision;
|
||
let err = sc.s.execute("layer.rasterize.layer", json!({})).unwrap_err().to_string();
|
||
assert!(err.contains("none of the selected layers"), "{err}");
|
||
assert_eq!(sc.s.active().unwrap().revision, revision, "a refused command changes nothing");
|
||
}
|
||
|
||
#[test]
|
||
fn bad_targets_fail_gracefully() {
|
||
let mut s = Session::new();
|
||
for id in ["layer.rasterize.layer", "layer.rasterize.type", "layer.rasterize.shape", "layer.rasterize.fillContent", "layer.rasterize.smartObject"] {
|
||
assert!(s.execute(id, json!({})).is_err(), "{id}: no document");
|
||
}
|
||
let mut sc = scene();
|
||
for id in ["layer.rasterize.layer", "layer.rasterize.shape", "layer.rasterize.smartObject"] {
|
||
assert!(sc.s.execute(id, json!({"layer": u64::MAX})).is_err(), "{id}: no such layer");
|
||
}
|
||
}
|