mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-24 09:57:08 -06:00
Merge PR #6352: fix(engine): honor lock all on layer mask commands
This commit is contained in:
@@ -80,7 +80,7 @@ pub(crate) fn paint_surface<'a>(doc: &'a mut Document, id: LayerId, p: &Value) -
|
||||
}
|
||||
}
|
||||
|
||||
fn check_all_unlocked(doc: &Document, id: LayerId) -> Result<()> {
|
||||
pub(crate) fn check_all_unlocked(doc: &Document, id: LayerId) -> Result<()> {
|
||||
let locks = doc.effective_locks(id);
|
||||
let l = doc.layer(id).ok_or(EngineError::NoLayer(id))?;
|
||||
if locks.all {
|
||||
@@ -461,6 +461,7 @@ fn destructive_adjust(s: &mut Session, label: &str, adj: Adjustment, p: &Value)
|
||||
// The targeted layer mask (#780): ⌘I inverts it, as in Photoshop.
|
||||
let id = layer_param(s, &Value::Null)?;
|
||||
return s.edit(label, |doc, _| {
|
||||
check_all_unlocked(doc, id)?;
|
||||
let sel = doc.selection.clone();
|
||||
let l = doc.layer_mut(id).ok_or(EngineError::NoLayer(id))?;
|
||||
let mask = l.mask.as_mut().ok_or_else(|| EngineError::Other("layer has no mask".into()))?;
|
||||
@@ -1694,6 +1695,7 @@ fn set_mask_with(s: &mut Session, p: &Value, label: &str, mask: Option<LayerMask
|
||||
s.edit(label, |doc, _| {
|
||||
let mask = mask.clone().map(|mask| mask_at_depth(mask, doc.depth)).transpose()?;
|
||||
for id in &ids {
|
||||
check_all_unlocked(doc, *id)?;
|
||||
if mask.is_some() {
|
||||
crate::extra_cmds::background_to_layer_for_mask(doc, *id);
|
||||
}
|
||||
|
||||
@@ -366,6 +366,13 @@ fn paste_into(s: &mut Session, p: &Value, outside: bool) -> Result<Value> {
|
||||
params["target"] = p.get("target").cloned().unwrap_or_default();
|
||||
return crate::edit_cmds::paste_to_target(s, ¶ms, false, Some(&limit), label);
|
||||
}
|
||||
// The pasted layer lands beside the active one, inside its group, and a Lock-All group refuses
|
||||
// its layer mask: refuse before pasting rather than leave an unmasked paste behind.
|
||||
let d = s.active().ok_or(EngineError::NoDocument)?;
|
||||
let group = d.active_layer.and_then(|a| d.doc.path_of(a)).and_then(|p| d.doc.layer_at(p.split_last()?.1).map(|g| g.id));
|
||||
if let Some(g) = group {
|
||||
crate::commands::check_all_unlocked(&d.doc, g)?;
|
||||
}
|
||||
let key = step_key(s, "pasteInto");
|
||||
params["coalesce"] = json!(key);
|
||||
params["target"] = json!("pixels");
|
||||
|
||||
@@ -204,6 +204,7 @@ fn hide_selection(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
mask.surface.write_region(bounds, &v);
|
||||
mask.surface.prune();
|
||||
for id in ids {
|
||||
crate::commands::check_all_unlocked(doc, id)?;
|
||||
crate::extra_cmds::background_to_layer_for_mask(doc, id);
|
||||
doc.layer_mut(id).ok_or(EngineError::NoLayer(id))?.mask = Some(mask.clone());
|
||||
}
|
||||
@@ -219,6 +220,7 @@ fn mask_all_objects(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let id = layer_param(s, p)?;
|
||||
let d = s.active().ok_or(EngineError::NoDocument)?;
|
||||
let doc = d.doc.clone();
|
||||
crate::commands::check_all_unlocked(&doc, id)?;
|
||||
let layer = doc.layer(id).ok_or(EngineError::NoLayer(id))?;
|
||||
let region: Option<Region> = match layer.surface() {
|
||||
Some(surf) if matches!(layer.content, LayerContent::Raster(_)) => {
|
||||
|
||||
@@ -44,6 +44,7 @@ mod layer_mask_depth;
|
||||
mod layer_props_several;
|
||||
mod layer_range_panel_order;
|
||||
mod live_line_transform;
|
||||
mod mask_lock_all;
|
||||
mod merge_transparent_bounds;
|
||||
mod missing_layer_error;
|
||||
mod nonfinite_style;
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
use photocraft_doc::LayerMask;
|
||||
use photocraft_engine::Session;
|
||||
use serde_json::json;
|
||||
|
||||
const COMMANDS: [&str; 7] = [
|
||||
"layer.layerMask.revealAll",
|
||||
"layer.layerMask.hideAll",
|
||||
"layer.layerMask.revealSelection",
|
||||
"layer.layerMask.hideSelection",
|
||||
"layer.layerMask.delete",
|
||||
"layer.maskAllObjects",
|
||||
"image.adjustments.invert",
|
||||
];
|
||||
|
||||
fn masked(all: bool) -> Session {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", json!({"width": 40, "height": 30})).unwrap();
|
||||
s.execute("layer.new.layer", json!({})).unwrap();
|
||||
s.execute("paint.stroke", json!({"points": [[5, 10], [20, 10]], "size": 8, "hardness": 1.0, "color": "#ff0000"})).unwrap();
|
||||
s.execute("layer.layerMask.revealAll", json!({})).unwrap();
|
||||
s.execute("paint.stroke", json!({"points": [[25, 20], [35, 20]], "size": 6, "hardness": 1.0, "color": "#000000", "target": "mask"})).unwrap();
|
||||
s.execute("select.rect", json!({"x": 2, "y": 2, "width": 20, "height": 15})).unwrap();
|
||||
s.execute("layer.setProps", json!({"locks": {"all": all}})).unwrap();
|
||||
s
|
||||
}
|
||||
|
||||
fn mask(s: &Session) -> Option<LayerMask> {
|
||||
let st = s.active().unwrap();
|
||||
st.doc.layer(st.active_layer.unwrap()).unwrap().mask.clone()
|
||||
}
|
||||
|
||||
fn undo_depth(s: &Session) -> usize {
|
||||
s.active().unwrap().history.past_len()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lock_all_refuses_every_mask_command() {
|
||||
for cmd in COMMANDS {
|
||||
let mut s = masked(true);
|
||||
let before = mask(&s);
|
||||
let undo = undo_depth(&s);
|
||||
let err = s.execute(cmd, json!({"target": "mask"})).unwrap_err();
|
||||
assert_eq!(err.to_string(), "Could not complete your request because the layer \"Layer 1\" is locked", "{cmd}");
|
||||
assert_eq!(mask(&s), before, "{cmd} left the mask unchanged");
|
||||
assert_eq!(undo_depth(&s), undo, "{cmd} added no history step");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unlocked_layers_still_take_every_mask_command() {
|
||||
for cmd in COMMANDS {
|
||||
let mut s = masked(false);
|
||||
let before = mask(&s);
|
||||
s.execute(cmd, json!({"target": "mask"})).unwrap_or_else(|e| panic!("{cmd}: {e}"));
|
||||
assert_ne!(mask(&s), before, "{cmd} changed the mask");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_locked_layer_among_the_selected_layers_refuses_the_whole_mask() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", json!({"width": 40, "height": 30})).unwrap();
|
||||
s.execute("layer.new.layer", json!({})).unwrap();
|
||||
let first = s.active().unwrap().active_layer.unwrap();
|
||||
s.execute("layer.new.layer", json!({})).unwrap();
|
||||
s.execute("layer.setProps", json!({"locks": {"all": true}})).unwrap();
|
||||
s.execute("layer.select", json!({"layer": first.0, "mode": "add"})).unwrap();
|
||||
assert_eq!(s.active().unwrap().selected_layers.len(), 2);
|
||||
assert!(s.execute("layer.layerMask.hideAll", json!({})).is_err());
|
||||
assert!(s.active().unwrap().doc.walk().iter().all(|(_, _, l)| l.mask.is_none()), "no layer got a mask");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn paste_into_a_lock_all_group_refuses_before_pasting() {
|
||||
for cmd in ["edit.pasteSpecial.pasteInto", "edit.pasteSpecial.pasteOutside"] {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", json!({"width": 40, "height": 30})).unwrap();
|
||||
let child = s.execute("layer.new.layer", json!({"name": "Child"})).unwrap()["layer"].as_u64().unwrap();
|
||||
s.execute("paint.stroke", json!({"points": [[5, 10], [20, 10]], "size": 8, "hardness": 1.0, "color": "#ff0000"})).unwrap();
|
||||
s.execute("select.all", json!({})).unwrap();
|
||||
s.execute("edit.copy", json!({})).unwrap();
|
||||
let group = s.execute("layer.groupLayers", json!({})).unwrap()["layer"].as_u64().unwrap();
|
||||
s.execute("layer.setProps", json!({"layer": group, "locks": {"all": true}})).unwrap();
|
||||
s.execute("layer.select", json!({"layer": child, "mode": "replace"})).unwrap();
|
||||
s.execute("select.rect", json!({"x": 2, "y": 2, "width": 20, "height": 15})).unwrap();
|
||||
let layers = s.active().unwrap().doc.walk().len();
|
||||
let undo = undo_depth(&s);
|
||||
assert!(s.execute(cmd, json!({})).is_err(), "{cmd}");
|
||||
assert_eq!(s.active().unwrap().doc.walk().len(), layers, "{cmd} pasted no layer");
|
||||
assert_eq!(undo_depth(&s), undo, "{cmd} added no history step");
|
||||
}
|
||||
}
|
||||
@@ -23,8 +23,8 @@ fn busy() -> photocraft_engine::Session {
|
||||
s.execute("layer.layerStyle.dropShadow", json!({"layer": a})).unwrap();
|
||||
// #153: the vector mask thumbnail and both link chains take row width too.
|
||||
s.execute("layer.vectorMask.revealAll", json!({"layer": a})).unwrap();
|
||||
s.execute("layer.setProps", json!({"layer": a, "blend": "Multiply", "locks": {"all": true}})).unwrap();
|
||||
s.execute("layer.layerMask.revealAll", json!({"layer": a})).unwrap();
|
||||
s.execute("layer.setProps", json!({"layer": a, "blend": "Multiply", "locks": {"all": true}})).unwrap();
|
||||
let b = s.execute("layer.new.layer", json!({"name": format!("{LONG} b{depth}")})).unwrap()["layer"].as_u64().unwrap();
|
||||
s.execute("layer.setProps", json!({"layer": b, "clipped": true})).unwrap();
|
||||
s.execute("layer.select", json!({"layer": a})).unwrap();
|
||||
|
||||
Reference in New Issue
Block a user