diff --git a/crates/engine/src/cmd/distortcmds.rs b/crates/engine/src/cmd/distortcmds.rs index 9b008500a..3da43957c 100644 --- a/crates/engine/src/cmd/distortcmds.rs +++ b/crates/engine/src/cmd/distortcmds.rs @@ -198,8 +198,8 @@ pub fn specs() -> Vec { "Edit Text", [], None, - "{id? (default: the selected type)} Object › Perspective › Edit Text: show type in perspective flat where it is drawn, in isolation mode, to edit it (text.* commands, the Type tool); exiting isolation (object.exitIsolation, Esc) projects it again → {id}", - has_doc, + "{id? (default: the selected type)} Object › Perspective › Edit Text: show type in perspective flat where it is drawn, in isolation mode, to edit it (text.* commands, the Type tool); exiting isolation (object.exitIsolation, Esc) projects it again; refused while an opacity mask is edited → {id}", + super::maskedit::outside_mask_editing, edit_text ), ] diff --git a/crates/engine/src/cmd/maskedit.rs b/crates/engine/src/cmd/maskedit.rs index c8dbf8c4d..8ae1912fd 100644 --- a/crates/engine/src/cmd/maskedit.rs +++ b/crates/engine/src/cmd/maskedit.rs @@ -5,7 +5,9 @@ //! object updates live; the renderer doesn't paint the editing layer (the mask is seen through its //! effect, as in the reference app). Leaving the mode removes the layer, and saving or exporting //! never writes it ([`Document::without_edit_modes`]). Meanwhile the Transparency panel's commands -//! act on the masked object (see `opacitymask::transparency_targets`). +//! act on the masked object (see `opacitymask::transparency_targets`). Mask editing is not entered +//! in isolation mode ([`outside_isolation_mode`]), and isolation mode is not entered while a mask is +//! edited ([`outside_mask_editing`]). use std::sync::Arc; @@ -27,8 +29,8 @@ pub fn specs() -> Vec { "Edit Opacity Mask", ["Window", "Transparency"], None, - "{id?} edit the mask art of `id` (default: the first selected object with a mask) in place, as clicking the mask thumbnail in the Transparency panel does → {layer}", - has_doc, + "{id?} edit the mask art of `id` (default: the first selected object with a mask) in place, as clicking the mask thumbnail in the Transparency panel does; refused in isolation mode → {layer}", + outside_isolation_mode, enter ), cmd!( @@ -45,13 +47,33 @@ pub fn specs() -> Vec { "View Opacity Mask", ["Window", "Transparency"], None, - "{on?: bool (default: toggle), id?} show only the opacity mask of `id` (default: the object whose mask is edited, else the first selected object with a mask) on the canvas, as greyscale coverage, and edit it, as Alt-clicking the mask thumbnail does; off shows the artwork again (still editing) → {on, id}", - has_doc, + "{on?: bool (default: toggle), id?} show only the opacity mask of `id` (default: the object whose mask is edited, else the first selected object with a mask) on the canvas, as greyscale coverage, and edit it, as Alt-clicking the mask thumbnail does; off shows the artwork again (still editing); refused in isolation mode → {on, id}", + outside_isolation_mode, view_mask ), ] } +/// Is the document in isolation mode? Pattern editing mode and opacity mask editing isolate their +/// art too, and do not count as isolation mode here. +pub(crate) fn in_isolation_mode(st: &crate::DocState) -> bool { + st.isolation.is_some() && st.doc.pattern_edit.is_none() && st.doc.mask_edit.is_none() +} + +/// A document is open and it is not in isolation mode ([`in_isolation_mode`]): opacity mask +/// editing is not entered in isolation mode. +pub(crate) fn outside_isolation_mode(s: &Session) -> std::result::Result<(), String> { + let st = s.active().ok_or("no document open")?; + if in_isolation_mode(st) { Err("exit isolation mode first".into()) } else { Ok(()) } +} + +/// A document is open and no opacity mask is being edited: isolation mode is not entered while +/// an opacity mask is edited. +pub(crate) fn outside_mask_editing(s: &Session) -> std::result::Result<(), String> { + let st = s.active().ok_or("no document open")?; + if st.doc.mask_edit.is_some() { Err("stop editing the opacity mask first".into()) } else { Ok(()) } +} + /// Mask art made of `children`: a plain group, so the mask keeps one art node. Editing spreads it /// back out onto the editing layer. pub(crate) fn mask_group(children: Vec>) -> Node { diff --git a/crates/engine/src/cmd/object/commands.rs b/crates/engine/src/cmd/object/commands.rs index 70e73d004..8e2dcca4a 100644 --- a/crates/engine/src/cmd/object/commands.rs +++ b/crates/engine/src/cmd/object/commands.rs @@ -240,8 +240,8 @@ pub fn specs() -> Vec { "Enter Isolation Mode", [], None, - "{id?} isolate a group, a layer or a single object (default: the first selected object); type, a hidden or locked object, an object in a hidden or locked group or layer, and a guide path while Lock Guides is on are refused", - has_doc, + "{id?} isolate a group, a layer or a single object (default: the first selected object); type, a hidden or locked object, an object in a hidden or locked group or layer, and a guide path while Lock Guides is on are refused, and so is every object while an opacity mask is edited", + crate::cmd::maskedit::outside_mask_editing, isolate ), cmd!("object.exitIsolation", "Exit Isolation Mode", [], None, "{}", has_doc, exit_isolation), diff --git a/crates/engine/src/cmd/opacitymask.rs b/crates/engine/src/cmd/opacitymask.rs index bccf1f4f7..1fb173daa 100644 --- a/crates/engine/src/cmd/opacitymask.rs +++ b/crates/engine/src/cmd/opacitymask.rs @@ -24,8 +24,8 @@ pub fn specs() -> Vec { "Make Opacity Mask", ["Window", "Transparency"], None, - "{ids?, clip?, invert?} the topmost of the selected objects (or of `ids`) becomes the mask of the others (grouped if several). One object gets an empty mask and enters mask editing: draw the mask, then transparency.stopEditingOpacityMask → {id, editing}", - has_doc, + "{ids?, clip?, invert?} the topmost of the selected objects (or of `ids`) becomes the mask of the others (grouped if several). One object gets an empty mask and enters mask editing: draw the mask, then transparency.stopEditingOpacityMask; one target (an object or a layer) is refused in isolation mode → {id, editing}", + can_make, make ), cmd!( @@ -162,6 +162,20 @@ fn masked_targets(s: &Session, p: &Value) -> Result> { Ok(ids) } +/// Make Opacity Mask: a document is open, and one selected object, which [`make`] gives an empty +/// mask and takes into mask editing, is not in isolation mode +/// ([`super::maskedit::outside_isolation_mode`]). The selection is one object when it has one +/// subject root, which `make` masks without `ids`, and at most one outermost object, which it +/// masks with `ids` (a call's `ids` stand in for the selection here). `make` itself refuses any +/// single target in isolation mode, a layer included. +fn can_make(s: &Session) -> std::result::Result<(), String> { + has_doc(s)?; + let Some(st) = s.active() else { return Ok(()) }; + let roots = super::appearance::subject_roots(s).map_or(0, |r| r.len()); + let outermost = super::edit::outermost(&st.doc, st.selection.in_paint_order(&st.doc)).len(); + if roots == 1 && outermost <= 1 { super::maskedit::outside_isolation_mode(s) } else { Ok(()) } +} + fn make(s: &mut Session, p: &Value) -> Result { const C: &str = "transparency.makeOpacityMask"; let d = &s.doc()?.doc; @@ -178,6 +192,10 @@ fn make(s: &mut Session, p: &Value) -> Result { None => super::appearance::subject_roots(s)?, }; let Some((&top, below)) = ids.split_last() else { return Err(bad(C, "select the art and, on top of it, the mask object")) }; + // One target gets an empty mask and enters mask editing, which isolation mode does not allow. + if below.is_empty() && super::maskedit::in_isolation_mode(s.doc()?) { + return Err(bad(C, "exit isolation mode first")); + } let clip = bool_or(p, "clip", !s.menu.new_masks_unclipped); let invert = bool_or(p, "invert", s.menu.new_masks_inverted); // Several objects: the top one is the mask. One: an empty mask, drawn in mask editing. diff --git a/crates/engine/src/tests/opacitymask.rs b/crates/engine/src/tests/opacitymask.rs index 5f69f6607..d76677bad 100644 --- a/crates/engine/src/tests/opacitymask.rs +++ b/crates/engine/src/tests/opacitymask.rs @@ -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(); diff --git a/crates/engine/src/tests/persp_text.rs b/crates/engine/src/tests/persp_text.rs index 91d7b70eb..0a32e8138 100644 --- a/crates/engine/src/tests/persp_text.rs +++ b/crates/engine/src/tests/persp_text.rs @@ -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. diff --git a/crates/ui-egui/src/canvas/task_bar.rs b/crates/ui-egui/src/canvas/task_bar.rs index 19fe62199..461730c6e 100644 --- a/crates/ui-egui/src/canvas/task_bar.rs +++ b/crates/ui-egui/src/canvas/task_bar.rs @@ -117,7 +117,10 @@ pub(super) fn task_bar(app: &mut VectorcraftApp, ui: &mut Ui, xf: &Xf) { items.push((tl!("Release Clipping Mask"), "ungroup", "object.clippingMask.release", none())); } else if is_group { items.push((tl!("Ungroup"), "ungroup", "object.ungroup", none())); - items.push((tl!("Isolate"), "square-dashed", "object.isolate", none())); + // Isolate is left out while Enter Isolation Mode is off, as it is while an opacity mask is edited. + if crate::menus::enabled(app, "object.isolate") { + items.push((tl!("Isolate"), "square-dashed", "object.isolate", none())); + } } else if is_compound { items.push(edit_path()); items.push((tl!("Release Compound Path"), "ungroup", "object.compoundPath.release", none())); diff --git a/crates/ui-egui/src/canvas/tests.rs b/crates/ui-egui/src/canvas/tests.rs index 07ee447a1..46cadeb23 100644 --- a/crates/ui-egui/src/canvas/tests.rs +++ b/crates/ui-egui/src/canvas/tests.rs @@ -6352,6 +6352,32 @@ fn the_task_bar_offers_the_artboard_tools_actions() { assert_eq!(names(&mut app), ["Resize", "Duplicate", "Export"]); } +/// A selected group's Contextual Task Bar offers Ungroup and Isolate. While an opacity mask is +/// edited, when Enter Isolation Mode is off, a group in the mask art gets Ungroup without Isolate. +#[test] +fn the_task_bar_leaves_isolate_out_while_an_opacity_mask_is_edited() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.session.execute("file.new", &json!({"width": 400, "height": 300})).unwrap(); + app.select_tool("selection"); + let ctx = egui::Context::default(); + let names = |app: &mut VectorcraftApp| task_bar_labels(app, &ctx).into_iter().map(|(t, _)| t).collect::>(); + let rect = |app: &mut VectorcraftApp, x: f64| { + app.session.execute("shape.rectangle", &json!({"x": x, "y": 100, "width": 40, "height": 40})).unwrap()["id"].clone() + }; + let group = |app: &mut VectorcraftApp, x: f64| { + let (a, b) = (rect(app, x), rect(app, x + 50.0)); + app.session.execute("select.set", &json!({"ids": [a, b]})).unwrap(); + app.session.execute("object.group", &json!({})).unwrap(); + }; + group(&mut app, 20.0); + assert_eq!(names(&mut app), ["Ungroup", "Isolate", "Duplicate"]); + let (obj, m) = (rect(&mut app, 200.0), rect(&mut app, 200.0)); + app.session.execute("transparency.makeOpacityMask", &json!({"ids": [obj, m]})).unwrap(); + app.session.execute("transparency.editOpacityMask", &json!({"id": obj})).unwrap(); + group(&mut app, 200.0); + assert_eq!(names(&mut app), ["Ungroup", "Duplicate"]); +} + /// The Artboard tool's Contextual Task Bar buttons act on its selected artboards: Resize opens /// Artboard Options for the active one, Rearrange All opens Rearrange All Artboards, Export opens /// Export for Screens with the selected artboards checked, and Duplicate copies each selected diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index 30f53cce7..e50096104 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -4213,6 +4213,7 @@ axis Both И двете @msg the object already has an opacity mask обектите вече имат маска за непрозрачност @msg the object has no geometry обектът няма геометрия @msg the object has no opacity mask обектът няма маска за непрозрачност +@msg stop editing the opacity mask first първо спрете редактирането на маската за непрозрачност @msg the object has no stroke обектът няма щрих @msg the object is beyond the plane's horizon обектът е извън хоризонта на равнината @msg the object isn't in perspective обектът не е в перспектива diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 83ee8b579..dd933ed4a 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -4619,6 +4619,7 @@ imageTrace Outline Obrys @msg the object already has an opacity mask objekt už má masku krytí @msg the object has no geometry objekt nemá žádnou geometrii @msg the object has no opacity mask objekt nemá masku krytí +@msg stop editing the opacity mask first nejprve ukončete úpravy masky krytí @msg the object has no stroke objekt nemá tah @msg the object is beyond the plane's horizon objekt je za horizontem roviny @msg the object isn't in perspective objekt není v perspektivě diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index 5f1a015da..0d455bc7f 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -4314,6 +4314,7 @@ imageTrace Outline Kontur @msg the object already has an opacity mask objektet har allerede en opacitetsmaske @msg the object has no geometry objektet har ingen geometri @msg the object has no opacity mask objektet har ingen opacitetsmaske +@msg stop editing the opacity mask first stop først redigeringen af opacitetsmasken @msg the object has no stroke objektet har ingen streg @msg the object is beyond the plane's horizon objektet er uden for planens horisont @msg the object isn't in perspective objektet er ikke i perspektiv diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 0f5e69309..a35c7a912 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -4242,6 +4242,7 @@ theme Appearance Erscheinungsbild @msg the object already has an opacity mask das Objekt hat bereits eine Deckkraftmaske @msg the object has no geometry das Objekt hat keine Geometrie @msg the object has no opacity mask das Objekt hat keine Deckkraftmaske +@msg stop editing the opacity mask first beenden Sie zuerst die Bearbeitung der Deckkraftmaske @msg the object has no stroke das Objekt hat keine Kontur @msg the object is beyond the plane's horizon das Objekt liegt jenseits des Horizonts der Perspektivebene @msg the object isn't in perspective das Objekt ist nicht in der Perspektive diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index ddbb4aecd..2763621ab 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -4275,6 +4275,7 @@ theme Appearance Εμφάνιση @msg the object already has an opacity mask το αντικείμενο έχει ήδη μάσκα αδιαφάνειας @msg the object has no geometry το αντικείμενο δεν έχει γεωμετρία @msg the object has no opacity mask το αντικείμενο δεν έχει μάσκα αδιαφάνειας +@msg stop editing the opacity mask first τερματίστε πρώτα την επεξεργασία της μάσκας αδιαφάνειας @msg the object has no stroke το αντικείμενο δεν έχει περίγραμμα @msg the object is beyond the plane's horizon το αντικείμενο βρίσκεται πέρα από τον ορίζοντα του επιπέδου @msg the object isn't in perspective το αντικείμενο δεν βρίσκεται σε προοπτική diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index befb83ebd..b8f1ee63a 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -4317,6 +4317,7 @@ axis Both Ambos @msg the object already has an opacity mask el objeto ya tiene una máscara de opacidad @msg the object has no geometry el objeto no tiene geometría @msg the object has no opacity mask el objeto no tiene máscara de opacidad +@msg stop editing the opacity mask first detén primero la edición de la máscara de opacidad @msg the object has no stroke el objeto no tiene trazo @msg the object is beyond the plane's horizon el objeto está más allá del horizonte del plano @msg the object isn't in perspective el objeto no está en perspectiva diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index 1bee5aea2..8f29f6759 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -4526,6 +4526,7 @@ imageTrace Outline Ääriviiva @msg the object already has an opacity mask objektilla on jo peittävyysmaski @msg the object has no geometry objektilla ei ole geometriaa @msg the object has no opacity mask objektilla ei ole peittävyysmaskia +@msg stop editing the opacity mask first lopeta ensin peittävyysmaskin muokkaus @msg the object has no stroke objektilla ei ole viivaa @msg the object is beyond the plane's horizon objekti on tason horisontin takana @msg the object isn't in perspective objekti ei ole perspektiivissä diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 532c76c4d..58c60a720 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -4314,6 +4314,7 @@ axis Both Les deux @msg the object already has an opacity mask l’objet a déjà un masque d’opacité @msg the object has no geometry l’objet n’a pas de géométrie @msg the object has no opacity mask l’objet n’a pas de masque d’opacité +@msg stop editing the opacity mask first arrêtez d’abord la modification du masque d’opacité @msg the object has no stroke l’objet n’a pas de contour @msg the object is beyond the plane's horizon l’objet est au-delà de l’horizon du plan @msg the object isn't in perspective l’objet n’est pas en perspective diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index cd45d3287..193e22dbf 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -5044,6 +5044,7 @@ imageTrace Outline Körvonal @msg the object already has an opacity mask az objektumnak már van átlátszósági maszkja @msg the object has no geometry az objektumnak nincs geometriája @msg the object has no opacity mask az objektumnak nincs átlátszósági maszkja +@msg stop editing the opacity mask first előbb fejezd be az átlátszósági maszk szerkesztését @msg the object has no stroke az objektumnak nincs körvonala @msg the object is beyond the plane's horizon az objektum a sík horizontján túl van @msg the object isn't in perspective az objektum nincs perspektívában diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index a4127b0ee..afeef2a41 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -4314,6 +4314,7 @@ imageTrace Outline Kontur @msg the object already has an opacity mask objek sudah memiliki masker opasitas @msg the object has no geometry objek tidak memiliki geometri @msg the object has no opacity mask objek tidak memiliki masker opasitas +@msg stop editing the opacity mask first hentikan pengeditan masker opasitas terlebih dahulu @msg the object has no stroke objek tidak memiliki goresan @msg the object is beyond the plane's horizon objek berada di luar horizon bidang @msg the object isn't in perspective objek tidak dalam perspektif diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 9d7965c26..2886160d2 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -4357,6 +4357,7 @@ axis Both Entrambi @msg the object already has an opacity mask l'oggetto ha già una maschera di opacità @msg the object has no geometry l'oggetto non ha geometria @msg the object has no opacity mask l'oggetto non ha una maschera di opacità +@msg stop editing the opacity mask first interrompi prima la modifica della maschera di opacità @msg the object has no stroke l'oggetto non ha traccia @msg the object is beyond the plane's horizon l'oggetto è oltre l'orizzonte del piano @msg the object isn't in perspective l'oggetto non è in prospettiva diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 0868c5712..c578f751e 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -4086,6 +4086,7 @@ swatch Bright Violet ビビッドバイオレット @msg select a blend or give `id` ブレンドを選択するか、`id` を指定してください @msg select a gradient mesh グラデーションメッシュを選択してください @msg the object has no opacity mask オブジェクトに不透明マスクがありません +@msg stop editing the opacity mask first 先に不透明マスクの編集を終了してください @msg Convert to Artboards: selection has no area アートボードに変換: 選択範囲に面積がありません @msg Convert to Shape: no rectangles or ellipses selected シェイプに変換: 長方形または楕円形が選択されていません @msg image would be too large; lower the resolution 画像が大きくなりすぎるため、解像度を下げてください diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index 9d14fc962..ba52ef67c 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -4078,6 +4078,7 @@ font-technology Variable ცვლადი @msg the object already has an opacity mask ობიექტს უკვე აქვს გაუმჭვირვალობის ნიღაბი @msg the object has no geometry ობიექტს გეომეტრია არ აქვს @msg the object has no opacity mask ობიექტს გაუმჭვირვალობის ნიღაბი არ აქვს +@msg stop editing the opacity mask first ჯერ დაასრულეთ გაუმჭვირვალობის ნიღბის რედაქტირება @msg the object has no stroke ობიექტს კონტური არ აქვს @msg the object is beyond the plane's horizon ობიექტი სიბრტყის ჰორიზონტს მიღმაა @msg the object isn't in perspective ობიექტი პერსპექტივაში არ არის diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index 31c0b24a5..1d63ccca3 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -4018,6 +4018,7 @@ theme Appearance 외관 @msg select a blend or give `id` 블렌드를 선택하거나 `id`를 지정하세요 @msg select a gradient mesh 그레이디언트 메시를 선택하세요 @msg the object has no opacity mask 개체에 불투명 마스크가 없습니다 +@msg stop editing the opacity mask first 먼저 불투명 마스크 편집을 종료하세요 @msg Convert to Artboards: selection has no area 아트보드로 변환: 선택 항목에 면적이 없습니다 @msg Convert to Shape: no rectangles or ellipses selected 모양으로 변환: 선택한 사각형이나 타원이 없습니다 @msg image would be too large; lower the resolution 이미지가 너무 커집니다. 해상도를 낮추세요 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index 4efce16c4..3f2683282 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -4326,6 +4326,7 @@ imageTrace Outline Contour @msg the object already has an opacity mask het object heeft al een dekkingmasker @msg the object has no geometry het object heeft geen geometrie @msg the object has no opacity mask het object heeft geen dekkingmasker +@msg stop editing the opacity mask first stop eerst met het bewerken van het dekkingmasker @msg the object has no stroke het object heeft geen lijn @msg the object is beyond the plane's horizon het object ligt voorbij de horizon van het vlak @msg the object isn't in perspective het object is niet in perspectief diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index d2a7d1ba4..2e3c245e6 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -4446,6 +4446,7 @@ imageTrace Outline Kontur @msg the object already has an opacity mask obiekt ma już maskę krycia @msg the object has no geometry obiekt nie ma geometrii @msg the object has no opacity mask obiekt nie ma maski krycia +@msg stop editing the opacity mask first najpierw zakończ edycję maski krycia @msg the object has no stroke obiekt nie ma obrysu @msg the object is beyond the plane's horizon obiekt jest za horyzontem płaszczyzny @msg the object isn't in perspective obiekt nie jest w perspektywie diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 5678f15ad..8e7c1b989 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -3889,6 +3889,7 @@ axis Both Ambos @msg the object already has an opacity mask o objeto já tem uma máscara de opacidade @msg the object has no geometry o objeto não tem geometria @msg the object has no opacity mask o objeto não tem máscara de opacidade +@msg stop editing the opacity mask first pare de editar a máscara de opacidade primeiro @msg the object has no stroke o objeto não tem traço @msg the object is beyond the plane's horizon o objeto está além do horizonte do plano @msg the object isn't in perspective o objeto não está em perspectiva diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 9f8e5212b..b3f59b850 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -4353,6 +4353,7 @@ axis Both Оба @msg the object already has an opacity mask у объекта уже есть непрозрачная маска @msg the object has no geometry у объекта нет геометрии @msg the object has no opacity mask у объекта нет непрозрачной маски +@msg stop editing the opacity mask first сначала прекратите редактирование непрозрачной маски @msg the object has no stroke у объекта нет обводки @msg the object is beyond the plane's horizon объект находится за горизонтом плоскости @msg the object isn't in perspective объект не в перспективе diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index 8e464ba87..94c74cec3 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -4226,6 +4226,7 @@ axis Both Båda @msg the object already has an opacity mask objektet har redan en opacitetsmask @msg the object has no geometry objektet saknar geometri @msg the object has no opacity mask objektet har ingen opacitetsmask +@msg stop editing the opacity mask first sluta redigera opacitetsmasken först @msg the object has no stroke objektet har ingen kontur @msg the object is beyond the plane's horizon objektet ligger bortom planets horisont @msg the object isn't in perspective objektet är inte i perspektiv diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index ed8b22978..11067ba32 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -4080,6 +4080,7 @@ axis Both Her İkisi @msg the object already has an opacity mask nesnenin zaten bir opaklık maskesi var @msg the object has no geometry nesnenin geometrisi yoktur @msg the object has no opacity mask nesnenin opaklık maskesi yok +@msg stop editing the opacity mask first önce opaklık maskesini düzenlemeyi durdur @msg the object has no stroke nesnenin konturu yok @msg the object is beyond the plane's horizon nesne uçağın ufkunun ötesinde @msg the object isn't in perspective nesne perspektifte değil diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 2b1ebeb4a..cac83cf5e 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -4341,6 +4341,7 @@ axis Both Обидві @msg the object already has an opacity mask об’єкт вже має маску непрозорості. @msg the object has no geometry об’єкт не має геометрії @msg the object has no opacity mask об’єкт не має маски непрозорості +@msg stop editing the opacity mask first спочатку припиніть редагування маски непрозорості @msg the object has no stroke об’єкт не має обведення @msg the object is beyond the plane's horizon об’єкт знаходиться за горизонтом площини @msg the object isn't in perspective об’єкт не в перспективі diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index b6209e36e..edd80b1c7 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -4701,6 +4701,7 @@ theme Appearance Giao diện @msg the object already has an opacity mask đối tượng đã có mặt nạ độ mờ @msg the object has no geometry đối tượng không có hình học @msg the object has no opacity mask đối tượng không có mặt nạ độ mờ +@msg stop editing the opacity mask first hãy dừng chỉnh sửa mặt nạ độ mờ trước @msg the object has no stroke đối tượng không có nét @msg the object is beyond the plane's horizon đối tượng vượt qua đường chân trời của mặt phẳng @msg the object isn't in perspective đối tượng không ở trong phối cảnh diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 2caf91f4e..fa99dbf31 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -3823,6 +3823,7 @@ imageTrace Outline 轮廓 @msg select a blend 请选择一个混合对象 @msg select a gradient mesh 请选择一个渐变网格 @msg the object has no opacity mask 该对象没有不透明度蒙版 +@msg stop editing the opacity mask first 请先停止编辑不透明度蒙版 @msg Convert to Artboards: selection has no area 转换为画板:所选对象没有面积 @msg Convert to Shape: no rectangles or ellipses selected 转换为形状:未选择矩形或椭圆 @msg image would be too large; lower the resolution 图像将过大;请降低分辨率 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 1d86caf46..b21ed9ccf 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -1784,6 +1784,7 @@ file-filter Templates 範本 @msg select a blend 請選取漸變物件 @msg select a gradient mesh 請選取漸層網格 @msg the object has no opacity mask 物件沒有不透明度遮色片 +@msg stop editing the opacity mask first 請先停止編輯不透明度遮色片 @msg Convert to Artboards: selection has no area 轉換為工作區域:選取範圍沒有面積 @msg Convert to Shape: no rectangles or ellipses selected 轉換為形狀:未選取矩形或橢圓形 @msg image would be too large; lower the resolution 影像會過大;請降低解析度 diff --git a/crates/ui-egui/src/panels/properties.rs b/crates/ui-egui/src/panels/properties.rs index 5db382e74..04b280631 100644 --- a/crates/ui-egui/src/panels/properties.rs +++ b/crates/ui-egui/src/panels/properties.rs @@ -130,7 +130,10 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { } if is_group { actions.push((tl!("Ungroup"), "object.ungroup")); - actions.push((tl!("Isolate Group"), "object.isolate")); + // Isolate Group is left out while Enter Isolation Mode is off, as it is while an opacity mask is edited. + if crate::menus::enabled(app, "object.isolate") { + actions.push((tl!("Isolate Group"), "object.isolate")); + } } if is_live { actions.push((tl!("Expand Shape"), "object.expandShape")); diff --git a/crates/ui-egui/src/panels/properties/tests.rs b/crates/ui-egui/src/panels/properties/tests.rs index 4b6af376e..b9bbb30e7 100644 --- a/crates/ui-egui/src/panels/properties/tests.rs +++ b/crates/ui-egui/src/panels/properties/tests.rs @@ -1342,6 +1342,28 @@ fn merge_live_paint_is_a_quick_action_for_a_live_paint_group_and_more() { assert_eq!(app.session.active().unwrap().doc.parent_of(c_id), parent, "Undo takes it out again"); } +/// A selected group has Ungroup and Isolate Group in Quick Actions. While an opacity mask is +/// edited, when Enter Isolation Mode is off, a group in the mask art has Ungroup only. +#[test] +fn isolate_group_is_not_a_quick_action_while_an_opacity_mask_is_edited() { + let mut app = new_app(); + let group = |app: &mut VectorcraftApp, x: f64| { + let (a, b) = (rect(app, x, 20.0, 40.0, 40.0), rect(app, x + 50.0, 20.0, 40.0, 40.0)); + app.run("select.set", json!({"ids": [a, b]})).unwrap(); + app.run("object.group", json!({})).unwrap(); + }; + let ctx = egui::Context::default(); + group(&mut app, 20.0); + let texts = tall(&mut app, &ctx, vec![]); + assert!(shows(&texts, "Ungroup") && shows(&texts, "Isolate Group"), "{texts:?}"); + let (obj, m) = (rect(&mut app, 200.0, 100.0, 80.0, 80.0), rect(&mut app, 200.0, 100.0, 40.0, 80.0)); + app.run("transparency.makeOpacityMask", json!({"ids": [obj, m]})).unwrap(); + app.run("transparency.editOpacityMask", json!({"id": obj})).unwrap(); + group(&mut app, 200.0); + let texts = tall(&mut app, &ctx, vec![]); + assert!(shows(&texts, "Ungroup") && !shows(&texts, "Isolate Group"), "{texts:?}"); +} + /// A stroke with a variable width shows its Width Profile in the Appearance section, as every /// stroke does with the Width tool; choosing another profile is one undo step. A uniform stroke /// shows none with other tools. diff --git a/crates/ui-egui/src/panels/tests_maskview.rs b/crates/ui-egui/src/panels/tests_maskview.rs index a0f24e1cd..f9adcd8f7 100644 --- a/crates/ui-egui/src/panels/tests_maskview.rs +++ b/crates/ui-egui/src/panels/tests_maskview.rs @@ -1,5 +1,6 @@ //! Headless frames of the Transparency panel's mask thumbnail (M3.86): Alt-click views only the -//! mask (and edits it), Alt-click again shows the artwork. +//! mask (and edits it), Alt-click again shows the artwork. In isolation mode neither a click nor an +//! Alt-click edits the mask. use serde_json::json; use vectorcraft_engine::Session; @@ -45,3 +46,57 @@ fn alt_clicking_the_mask_thumbnail_views_only_the_mask() { click_mask(&ctx, &mut app, egui::Modifiers::ALT); assert_eq!(viewed(&app), Some(a)); } + +/// Where the panel painted `label`, in a frame of it. +fn label_at(ctx: &egui::Context, app: &mut VectorcraftApp, label: &str) -> egui::Pos2 { + let mut out = ctx.run_ui(egui::RawInput::default(), |ui| transparency::show(app, ui)); + out.textures_delta.clear(); + let at = out.shapes.iter().find_map(|c| match &c.shape { + egui::Shape::Text(t) if t.galley.text() == label => Some(t.visual_bounding_rect().center()), + _ => None, + }); + at.unwrap_or_else(|| panic!("the panel draws {label:?}")) +} + +/// In isolation mode a click or an Alt-click on the mask thumbnail does not edit the mask, and the +/// status bar says to exit isolation mode first. Make Mask is off for one object, which it would +/// take into mask editing. After Exit Isolation Mode a click edits the mask. +#[test] +fn the_mask_thumbnail_does_not_edit_the_mask_in_isolation_mode() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + let run = |app: &mut VectorcraftApp, id: &str, p: serde_json::Value| app.session.execute(id, &p).unwrap(); + run(&mut app, "file.new", json!({"width": 100, "height": 100})); + let a = run(&mut app, "shape.rectangle", json!({"x": 0, "y": 0, "width": 50, "height": 50}))["id"].as_u64().unwrap(); + let b = run(&mut app, "shape.rectangle", json!({"x": 10, "y": 10, "width": 20, "height": 20}))["id"].clone(); + run(&mut app, "select.set", json!({"ids": [a, b]})); + run(&mut app, "transparency.makeOpacityMask", json!({})); + let c = run(&mut app, "shape.rectangle", json!({"x": 60, "y": 0, "width": 30, "height": 30}))["id"].clone(); + run(&mut app, "select.set", json!({"ids": [a, c]})); + let g = run(&mut app, "object.group", json!({}))["id"].as_u64().unwrap(); + run(&mut app, "object.isolate", json!({"id": g})); + run(&mut app, "select.set", json!({"ids": [a]})); + let ctx = egui::Context::default(); + for modifiers in [egui::Modifiers::NONE, egui::Modifiers::ALT] { + app.ui.status.clear(); + click_mask(&ctx, &mut app, modifiers); + let st = app.session.active().unwrap(); + assert!(st.doc.mask_edit.is_none() && st.shown_mask().is_none(), "{modifiers:?}"); + assert_eq!(st.isolation.map(|i| i.0), Some(g)); + assert!(app.ui.status.ends_with("exit isolation mode first"), "{modifiers:?}: {}", app.ui.status); + } + // One object without a mask: Make Mask is off, and a click on it does nothing. + run(&mut app, "select.set", json!({"ids": [c]})); + app.ui.status.clear(); + frame(&ctx, &mut app, vec![], egui::Modifiers::NONE); + let at = label_at(&ctx, &mut app, "Make Mask"); + let button = |pressed| egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed, modifiers: egui::Modifiers::NONE }; + frame(&ctx, &mut app, vec![egui::Event::PointerMoved(at), button(true), button(false)], egui::Modifiers::NONE); + let st = app.session.active().unwrap(); + let id = vectorcraft_doc::NodeId(c.as_u64().unwrap()); + assert!(st.doc.node(id).unwrap().extras().mask.is_none() && st.doc.mask_edit.is_none()); + assert_eq!(app.ui.status, "", "the button is off"); + run(&mut app, "object.exitIsolation", json!({})); + run(&mut app, "select.set", json!({"ids": [a]})); + click_mask(&ctx, &mut app, egui::Modifiers::NONE); + assert_eq!(app.session.active().unwrap().doc.mask_edit.map(|e| e.object.0), Some(a)); +} diff --git a/crates/ui-egui/src/panels/transparency.rs b/crates/ui-egui/src/panels/transparency.rs index cbd6b6931..5c943d928 100644 --- a/crates/ui-egui/src/panels/transparency.rs +++ b/crates/ui-egui/src/panels/transparency.rs @@ -143,7 +143,8 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { } ui.vertical(|ui| { let label = if mask.is_some() { tl!("Release") } else { tl!("Make Mask") }; - let enabled = mask.is_some() || (has && editing.is_none()); + // Make Mask is off in isolation mode for one object, which it would take into mask editing. + let enabled = mask.is_some() || (has && editing.is_none() && crate::menus::enabled(app, "transparency.makeOpacityMask")); let r = ui.add_enabled_ui(enabled, |ui| widgets::flat_button(ui, label, 96.0)).inner; if r.on_disabled_hover_text(tl!("Select the art (and, on top of it, the mask object)")).clicked() { let id = if mask.is_some() { "transparency.releaseOpacityMask" } else { "transparency.makeOpacityMask" }; @@ -196,7 +197,8 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { let editing = app.session.active().is_some_and(|d| d.doc.mask_edit.is_some()); let (_, n) = state(app, ui.ctx()); let mask = n.as_ref().and_then(|n| n.extras().mask.as_deref()); - for (label, id, enabled) in mask_items(mask, n.is_some() && !editing) { + let can_make = n.is_some() && !editing && crate::menus::enabled(app, "transparency.makeOpacityMask"); + for (label, id, enabled) in mask_items(mask, can_make) { if menu_item(ui, tl!(label), enabled, false) { app.run(id, json!({})).ok(); } diff --git a/docs/mcp.md b/docs/mcp.md index 5d2f2859a..2a2ab7516 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -1262,6 +1262,12 @@ then composite against the art below it). SVG has no non-isolated groups, so the mask is edited; called again it shows the artwork, still editing. It is view state of the open document: the canvas (and `ui.screenshot`) shows it, exports never do, and it ends when mask editing ends. +Isolation mode and mask editing exclude each other. 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". While a mask is edited, +`object.isolate` and `perspective.editText` are off, and a call returns an error that ends "stop editing the opacity +mask first". Pattern editing mode does not count as isolation mode here. + `view.transparencyGrid {on?}` (View → Show Transparency Grid; default: toggle) is view state of each open document, like the mask view: the grid shows behind the artboards of the documents that turn it on, on the canvas and in `ui.screenshot`, never in exports. It returns `{on}`, isn't undoable and doesn't mark the document changed. diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index c8a4a634d..c90db991a 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -823,6 +823,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur The Tabs panel menu's Clear All Tabs (`text.tabs.clear`), Delete Tab and Repeat Tab (`text.tabs.set`) can be chosen while the panel's ruler shows tab stops. Delete Tab removes the stop selected on the ruler. Repeat Tab replaces the stops after the selected stop with up to 20 copies of it. The copies are spaced by the selected stop's distance from the stop before it, or from the paragraph's left indent when it is the first stop, and no copy is placed at 2,000 pt or beyond. Clear All Tabs removes every stop, so the default stops every ½ inch apply again. - **Interface languages, vertical type:** Preferences ▸ User Interface (or VectorCraft ▸ Language on macOS) picks Automatic (the system locale) or a registered language, persisted as `interfaceLanguage`. Translations come from per-language catalogs (`i18n/*.tsv`): Traditional and Simplified Chinese, Japanese, German (every `de-*` locale), Spanish (neutral international Spanish, for every `es-*` locale), French (every `fr-*` locale), Italian (every `it-*` locale) Russian (every `ru-*` locale) and Ukrainian (every `uk-*` locale) cover the whole interface, Czech every menu label; Czech, German, Greek, Spanish, French, Hungarian, Italian, Japanese, Korean, Vietnamese, Polish, Brazilian Portuguese, Russian, Simplified Chinese, Traditional Chinese, Swedish, Ukrainian and Finnish also translate status-bar and error messages as they are shown (`@msg` templates, every message literal tested; the messages themselves stay English for agents); untranslated text stays English. Japanese and Chinese glyphs come from the craft-fonts build input (release builds) or the installed fonts, Czech, German, Spanish, French, Italian, Russian and Ukrainian ones from the bundled UI fonts. The web app follows the browser's languages (or `?lang=`). The Basic toolbar's Type button holds Vertical Type in its flyout. Vertical Type, Vertical Area Type and Vertical Type on a Path create vertical text (columns right to left, upright CJK glyphs and marks with `vert` alternates, Latin and longer numbers on their side, two- and three-digit numbers set as tate-chu-yoko in one em of the column (kept whole when justified), upright glyphs centred on the column (tracked or justified too), point type's anchor on the first column's centre line); kinsoku applies to horizontal and vertical Japanese (Paragraph panel › Kinsoku Set, `text.setFormat {kinsoku}`: Hard, the default, keeps closing marks, iteration marks, the prolonged sound mark and small kana off the line start and opening brackets off its end; Soft lets 々, ー and small kana start a line as JLREQ's level 3 rules do; None turns it off), and justified Japanese lines spread their room between characters (not inside Latin words); Paragraph panel › Mojikumi Set (shown with Preferences › Type › Show East Asian Options, `text.setFormat {mojikumi}`): None or Line-end Punctuation Half Width (new type's default, as in Illustrator; older documents and imported text keep None) shares one half-em space between consecutive punctuation, sets a closing mark ending a line half width and an opening bracket starting a line flush with its start (a paragraph's first line at its first-line indent, JIS X 4051's principle in JLREQ 3.1.5), and puts a quarter em between Japanese and Latin letters or digits; Paragraph panel menu › Top-to-Top / Bottom-to-Bottom Leading measures leading from em box top to top (area type's first line touches the frame) or from baseline to baseline (new type in a Japanese interface starts top to top, with em box centre character alignment, #432); Paragraph panel menu › Burasagari None / Regular / Force (`text.setFormat {burasagari: "none"|"standard"|"forced"}`) hangs an East Asian comma or full stop (、。,.、。) ending an area type line outside it, Regular (new type's default) when it doesn't fit and Force always (the rest of the line then fills the measure; closing brackets and Latin punctuation don't hang); Character panel menu › Character Alignment (with the East Asian options; `text.setFormat` or `text.setRangeStyle {charAlign}`) lines characters smaller than the largest on their line up on the Roman baseline, the em box top/right, centre or bottom/left, or the ICF (ideographic character face) top/right or bottom/left, from the font's BASE table or, without one, the average ink box of some ideographs and kana; Proportional Metrics (the same menu; `{proportionalMetrics}`) sets full-width glyphs on the font's proportional widths (OpenType `palt`) in horizontal type and upright glyphs on its proportional heights (`vpal`, shaped top to bottom; fonts without vertical metrics keep their cells) in vertical type, and Line-end Punctuation Half Width takes nothing more off the punctuation they re-spaced; Type ▸ Type Orientation switches existing text; caret, selection, hit testing and arrow keys follow the writing direction. Vertical text exports to PDF as real text and to SVG as outlines. The interface is available in Polish (`pl`): menus, panels, dialogs and messages, with one/few/many plurals; AI-drafted, native review pending (#1498). - **Transparency:** opacity masks (clip/invert/disable/link), exported as SVG `` and PDF soft masks. The constant alpha soft mask that PDF and `.ai` export draw a CMYK page with transparency through is white, so applications that read every soft mask by its luminosity show the page too (#1359, part of #1286). + Isolation mode and opacity mask editing exclude each other ([transparency and blending modes](https://web.archive.org/web/20230102045151/https://helpx.adobe.com/illustrator/using/transparency-blending-modes.html)): in isolation mode a click or Alt-click on the mask thumbnail does not edit the mask, and Make Mask is off for one object; while a mask is edited, Enter Isolation Mode and Object › Perspective › Edit Text are off, the Contextual Task Bar leaves out Isolate, and Quick Actions leave out Isolate Group. Disable, Enable, Unlink and Link Opacity Mask and Opacity Mask Options (`transparency.setOpacityMask`) record no undo step when every mask they act on already has the flags they set. - **Image Trace, thin closed outlines:** generated `fit::snap_area_tests` verify nonzero interiors after straightening, opposite closed-boundary winding and Noise controls; see [quality notes](image-trace-quality.md#thin-closed-outlines-with-snap-curves-to-lines). Scores and hours unchanged; open-piece fitting is unchanged. - **Image Trace, Document Library color groups:** with Document Swatches, the panel's Colors menu and `"colorGroup"` trace with one color group's colors instead of every swatch, as the public panel documentation describes Document Library; code-drawn tests in `crates/engine` and the panel.