Transparency: isolation mode and opacity mask editing can't be entered from each other

Enter Isolation Mode worked while an opacity mask was edited, and Edit
Opacity Mask and View Opacity Mask worked in isolation mode, so each
mode replaced the other's isolated art. In the reference app, isolation
mode can't be entered while a mask is edited, and mask editing can't be
entered in isolation mode.

object.isolate and perspective.editText, which shows type in isolation
mode, are now off while an opacity mask is edited, and a call returns
an error that ends "stop editing the opacity mask first". In isolation
mode transparency.editOpacityMask and transparency.viewOpacityMask are
off, and transparency.makeOpacityMask refuses one target, an object or
a layer, which it would take into mask editing; a call returns an error
that ends "exit isolation mode first". Make Opacity Mask with two
objects still masks one with the other. Pattern editing mode does not
count as isolation mode for these commands.

Object > Perspective > Edit Text, the canvas context menu and the
Transparency panel's menu and Make Mask button follow the commands'
enablement. A click or Alt-click on the mask thumbnail in isolation
mode shows the error and does not edit the mask. While a mask is
edited, the Contextual Task Bar leaves out Isolate, and the Properties
panel's Quick Actions leave out Isolate Group.

Docs: docs/mcp.md, docs/target-app-parity.md.
This commit is contained in:
dotCooCoo
2026-10-29 11:20:08 +07:00
committed by bflatastic
parent e251bbf159
commit dfe201dec3
38 changed files with 296 additions and 17 deletions
+81 -1
View File
@@ -1,6 +1,7 @@
//! Opacity-mask editing and the Transparency panel's command reach (M3.25): commands act on the
//! masked object while its mask is edited, accept ids without a selection, take opacity in percent,
//! report mixed values, and saves and exports never carry the editing layer.
//! report mixed values, and saves and exports never carry the editing layer. Mask editing is not
//! entered in isolation mode, and isolation mode is not entered while a mask is edited.
use serde_json::{Value, json};
use vectorcraft_geom::Affine;
@@ -228,6 +229,85 @@ fn one_object_gets_an_empty_mask_drawn_in_editing_mode() {
assert_eq!((s.doc().unwrap().isolation, s.doc().unwrap().active_layer), (Some(layer), Some(layer)));
}
/// The command `cmd` with `p` is off, for `why`.
fn off(s: &mut Session, cmd: &str, p: Value, why: &str) {
let err = s.execute(cmd, &p).unwrap_err();
assert!(matches!(&err, EngineError::Disabled(id, w) if id == cmd && w == why), "{cmd} {p}: {err}");
}
/// Isolation mode is not entered while an opacity mask is edited: Enter Isolation Mode is off for
/// a group in the mask art and for one outside it, and mask editing goes on. After Stop Editing
/// Opacity Mask it isolates again.
#[test]
fn isolation_mode_is_not_entered_while_an_opacity_mask_is_edited() {
let mut s = session();
let group = |s: &mut Session, x: f64| {
let (a, b) = (rect(s, x, 20.0), rect(s, x + 30.0, 20.0));
s.execute("select.set", &json!({"ids": [a.0, b.0]})).unwrap();
NodeId(s.execute("object.group", &json!({})).unwrap()["id"].as_u64().unwrap())
};
let outside = group(&mut s, 220.0);
editing_mask(&mut s);
let layer = s.doc().unwrap().isolation;
let inside = group(&mut s, 0.0);
for id in [inside, outside] {
off(&mut s, "object.isolate", json!({"id": id.0}), "stop editing the opacity mask first");
assert_eq!(s.doc().unwrap().isolation, layer);
assert!(doc(&s).mask_edit.is_some(), "still editing");
}
s.execute("select.set", &json!({"ids": [inside.0]})).unwrap();
off(&mut s, "object.isolate", json!({}), "stop editing the opacity mask first");
s.execute("transparency.stopEditingOpacityMask", &json!({})).unwrap();
s.execute("object.isolate", &json!({"id": outside.0})).unwrap();
assert_eq!(s.doc().unwrap().isolation, Some(outside));
}
/// Opacity mask editing is not entered in isolation mode: Edit Opacity Mask and View Opacity Mask
/// are off, and so is Make Opacity Mask for one object, which would edit its new empty mask. Make
/// Opacity Mask with two objects still masks one with the other. After Exit Isolation Mode the mask
/// is edited again.
#[test]
fn opacity_mask_editing_is_not_entered_in_isolation_mode() {
let mut s = session();
let (obj, m) = (rect(&mut s, 0.0, 200.0), rect(&mut s, 0.0, 100.0));
s.execute("transparency.makeOpacityMask", &json!({"ids": [obj.0, m.0]})).unwrap();
let other = rect(&mut s, 220.0, 30.0);
s.execute("select.set", &json!({"ids": [obj.0, other.0]})).unwrap();
let g = NodeId(s.execute("object.group", &json!({})).unwrap()["id"].as_u64().unwrap());
s.execute("object.isolate", &json!({"id": g.0})).unwrap();
s.execute("select.set", &json!({"ids": [obj.0]})).unwrap();
let why = "exit isolation mode first";
off(&mut s, "transparency.editOpacityMask", json!({}), why);
off(&mut s, "transparency.editOpacityMask", json!({"id": obj.0}), why);
off(&mut s, "transparency.viewOpacityMask", json!({}), why);
off(&mut s, "transparency.viewOpacityMask", json!({"on": true, "id": obj.0}), why);
s.execute("select.set", &json!({"ids": [other.0]})).unwrap();
off(&mut s, "transparency.makeOpacityMask", json!({}), why);
off(&mut s, "transparency.makeOpacityMask", json!({"ids": [other.0]}), why);
// A layer given as the one target is refused too.
let layer = doc(&s).layers[0].id;
let err = s.execute("transparency.makeOpacityMask", &json!({"ids": [layer.0]})).unwrap_err();
assert!(err.to_string().ends_with(why), "{err}");
let st = s.doc().unwrap();
assert!(st.doc.mask_edit.is_none() && st.mask_view.is_none() && mask(&s, other).is_none());
assert_eq!(s.doc().unwrap().isolation, Some(g));
// Two objects: the top one masks the other, without mask editing.
let top = rect(&mut s, 220.0, 10.0);
let r = s.execute("transparency.makeOpacityMask", &json!({"ids": [other.0, top.0]})).unwrap();
assert_eq!(r, json!({"id": other.0, "editing": false}));
assert_eq!(s.doc().unwrap().isolation, Some(g));
// Two members of a compound path given in `ids` are two objects: one masks the other.
let (c1, c2) = (rect(&mut s, 250.0, 10.0), rect(&mut s, 265.0, 10.0));
s.execute("select.set", &json!({"ids": [c1.0, c2.0]})).unwrap();
s.execute("object.compoundPath.make", &json!({})).unwrap();
let r = s.execute("transparency.makeOpacityMask", &json!({"ids": [c1.0, c2.0]})).unwrap();
assert_eq!(r["editing"], json!(false));
assert!(doc(&s).mask_edit.is_none());
s.execute("object.exitIsolation", &json!({})).unwrap();
s.execute("transparency.editOpacityMask", &json!({"id": obj.0})).unwrap();
assert_eq!(doc(&s).mask_edit.map(|e| e.object), Some(obj));
}
#[test]
fn mask_and_transparency_commands_take_ids_without_a_selection() {
let mut s = session();
+17
View File
@@ -141,6 +141,23 @@ fn edit_text_shows_it_flat_in_isolation_then_projects_it_again() {
assert!(s2.execute("perspective.editText", &json!({"id": rect.0})).is_err());
}
/// Edit Text, which shows the type in isolation mode, is off while an opacity mask is edited.
#[test]
fn edit_text_is_off_while_an_opacity_mask_is_edited() {
let (mut s, rect) = persp_session();
let id = persp_text(&mut s);
s.execute("transparency.makeOpacityMask", &json!({"ids": [rect.0]})).unwrap();
let layer = s.doc().unwrap().isolation;
assert!(layer.is_some(), "editing the new empty mask");
let err = s.execute("perspective.editText", &json!({"id": id.0})).unwrap_err();
assert!(matches!(&err, EngineError::Disabled(_, why) if why == "stop editing the opacity mask first"), "{err}");
assert_eq!(s.doc().unwrap().isolation, layer);
assert!(node(&s, id).projection().is_some(), "still projected");
s.execute("transparency.stopEditingOpacityMask", &json!({})).unwrap();
s.execute("perspective.editText", &json!({"id": id.0})).unwrap();
assert_eq!(s.doc().unwrap().isolation, Some(id));
}
/// Edit Text ending while a background save writes the file leaves the document saved once the
/// file is written: the flat view is never saved. Saved while editing and edited after: undoing
/// the edit once Edit Text has ended is back at what the file holds.