Background: an opacity, fill, blend mode or Select and Mask layer mask makes it Layer 0 (#4837)

The Background has none of them. layer.setProps applied them to it as it
was, and Select and Mask's Layer Mask output gave it a mask. Like
Photoshop (and like PhotoCraft already does for moving, transforming,
erasing, styles and Layer > Layer Mask), it now becomes a normal layer,
"Layer 0", first. Values that leave its look alone (opacity and fill
100 %, Normal) keep it the Background.

Fixes #4836
This commit is contained in:
Gert Pellin - GPE authored and GitHub committed 2026-10-14 01:58:55 -04:00
1 parent 7c916d9ecd
commit 2da2889546
3 files changed
+34

No files matched your search

+26
View File
@@ -144,6 +144,32 @@ mod tests {
(s, [bg, a, b])
}
#[test]
fn opacity_fill_or_a_blend_mode_turn_the_background_into_a_layer() {
// Like Photoshop: the Background takes none of them, so it becomes "Layer 0" first.
for (key, value) in [("opacity", json!(0.5)), ("fill", json!(0.5)), ("blend", json!("Multiply"))] {
let (mut s, [bg, ..]) = with_background();
s.execute("layer.setProps", json!({"layer": bg.0, key: value})).unwrap();
let l = doc(&s).layer(bg).unwrap();
assert!(!doc(&s).is_background(bg) && l.name == "Layer 0", "{key}");
assert!(l.opacity < 1.0 || l.fill_opacity < 1.0 || l.blend == photocraft_color::BlendMode::Multiply, "{key}: applied");
}
// Values that leave its look alone keep it the Background.
let (mut s, [bg, ..]) = with_background();
s.execute("layer.setProps", json!({"layer": bg.0, "opacity": 1.0, "fill": 1.0, "blend": "Normal", "visible": false})).unwrap();
assert!(doc(&s).is_background(bg));
}
#[test]
fn select_and_mask_output_to_a_layer_mask_turns_the_background_into_a_layer() {
let (mut s, [bg, ..]) = with_background();
s.execute("layer.select", json!({"layer": bg.0})).unwrap();
s.execute("select.rect", json!({"x": 2, "y": 2, "width": 6, "height": 6})).unwrap();
s.execute("select.refineEdge", json!({"output": "layerMask"})).unwrap();
let l = doc(&s).layer(bg).unwrap();
assert!(l.mask.is_some() && !doc(&s).is_background(bg) && l.name == "Layer 0");
}
#[test]
fn makes_the_layer_the_background_at_the_bottom() {
for depth in [8, 16, 32] {
+7
View File
@@ -688,6 +688,13 @@ fn build() -> Vec<CommandSpec> {
// Clipping and channel changes reach the layers around it: recomposite everything.
let local = p.get("clipped").is_none() && p.get("channels").is_none();
s.edit(label, |doc, _| {
// The Background has no opacity, fill or blend mode: like Photoshop, setting
// one that changes its look turns it into a normal layer ("Layer 0") first.
let num = |k: &str| p.get(k).and_then(Value::as_f64).is_some_and(|v| v < 1.0);
let blends = p.get("blend").and_then(Value::as_str).and_then(blend_from_str).is_some_and(|b| b != photocraft_color::BlendMode::Normal);
if num("opacity") || num("fill") || blends {
crate::extra_cmds::background_to_layer(doc, id);
}
let l = doc.layer_mut(id).ok_or(EngineError::NoLayer(id))?;
if let Some(v) = p.get("name").and_then(Value::as_str) {
l.name = v.to_string();
+1
View File
@@ -417,6 +417,7 @@ fn refine_edge(s: &mut Session, p: &Value) -> Result<Value> {
s.edit("Select and Mask", |doc, _| {
let surface =
region.as_ref().map(matting::region_surface).unwrap_or_else(|| photocraft_raster::Surface::new(photocraft_color::PixelFormat::GRAY8));
crate::extra_cmds::background_to_layer_for_mask(doc, id);
let existing = doc.layer(id).and_then(|l| l.mask.as_ref()).cloned().unwrap_or_else(LayerMask::reveal_all);
doc.layer_mut(id).ok_or(EngineError::NoLayer(id))?.mask = Some(LayerMask { surface, ..existing });
doc.selection = None;