From 6bed87b5fbc2050786d21acfffdf30455e3c49f3 Mon Sep 17 00:00:00 2001 From: Dan Green Date: Mon, 19 Oct 2026 14:11:22 -0700 Subject: [PATCH] Libraries panel: the list scrolls; items and groups reorder by dragging, items drag into groups The list fills the panel down to its bottom bar and scrolls, also while a drag nears its top or bottom edge (in the dock and in a flyout, as the Layers panel does). Dragging in the list: - an item (colour, text style or graphic) dropped on another of its kind lands just before or after it, by the pointer's half, an accent line showing where, and joins that item's group; - dropped elsewhere on a group (its header too, even collapsed) it goes into that group, and on the ungrouped items out of its group, the section outlined; with every item grouped, a "Drop here to take it out of its group" strip shows while one is dragged; - a group dragged by its header goes before or after another group. Colours and graphics still drag onto art and the canvas as before. Engine: library.moveItem takes `before` / `after` (an item of the same kind) to place the item in its kind's order, which the panel shows; the new library.moveGroup {group, to} moves a group. Text styles drag as PanelDrag::LibraryItem, groups as PanelDrag::LibraryGroup, and a library colour's Paint drag names its library item, so the panel tells it from a swatch dragged in. Tests: reordering and moving in the engine, the commands in the library fuzz test, and panel tests that drop styles and group headers and check the list scrolls and scrolls while dragging. Strings translated in the 23 catalogs; docs/mcp.md and the parity row updated. --- crates/engine/src/cmd/library.rs | 70 ++- crates/engine/src/tests_library.rs | 55 +++ crates/engine/tests/import_fuzz.rs | 11 +- crates/ui-egui/src/canvas.rs | 4 +- crates/ui-egui/src/dock.rs | 6 +- crates/ui-egui/src/i18n/bg.tsv | 4 +- crates/ui-egui/src/i18n/cs.tsv | 4 +- crates/ui-egui/src/i18n/da.tsv | 4 +- crates/ui-egui/src/i18n/de.tsv | 4 +- crates/ui-egui/src/i18n/el.tsv | 4 +- crates/ui-egui/src/i18n/es.tsv | 4 +- crates/ui-egui/src/i18n/fi.tsv | 4 +- crates/ui-egui/src/i18n/fr.tsv | 4 +- crates/ui-egui/src/i18n/hu.tsv | 4 +- crates/ui-egui/src/i18n/id.tsv | 4 +- crates/ui-egui/src/i18n/it.tsv | 4 +- crates/ui-egui/src/i18n/ja.tsv | 4 +- crates/ui-egui/src/i18n/ka.tsv | 4 +- crates/ui-egui/src/i18n/ko.tsv | 4 +- crates/ui-egui/src/i18n/nl.tsv | 4 +- crates/ui-egui/src/i18n/pl.tsv | 4 +- crates/ui-egui/src/i18n/pt-br.tsv | 4 +- crates/ui-egui/src/i18n/ru.tsv | 4 +- crates/ui-egui/src/i18n/sv.tsv | 4 +- crates/ui-egui/src/i18n/uk.tsv | 4 +- crates/ui-egui/src/i18n/vi.tsv | 4 +- crates/ui-egui/src/i18n/zh-hans.tsv | 4 +- crates/ui-egui/src/i18n/zh-hant.tsv | 4 +- crates/ui-egui/src/panels/gradient.rs | 2 +- crates/ui-egui/src/panels/graphic_styles.rs | 7 +- crates/ui-egui/src/panels/libraries.rs | 445 +++++++++++++++++--- crates/ui-egui/src/panels/swatches.rs | 3 +- crates/ui-egui/src/widgets.rs | 14 +- docs/mcp.md | 10 +- docs/target-app-parity.md | 5 +- 35 files changed, 621 insertions(+), 103 deletions(-) diff --git a/crates/engine/src/cmd/library.rs b/crates/engine/src/cmd/library.rs index 227c0d46e..74a70fd28 100644 --- a/crates/engine/src/cmd/library.rs +++ b/crates/engine/src/cmd/library.rs @@ -5,7 +5,8 @@ //! one copies the selection as Copy does, and placing it pastes a copy as Paste does (its images, //! symbols, patterns and swatches come along; a swatch name the document gives another colour is //! merged into the document's). Items can be put in user-named groups, and a library can be -//! exported to a `.vclibrary` file and imported from one (#926). +//! exported to a `.vclibrary` file and imported from one (#926). Items and groups are kept in the +//! order the Libraries panel shows them, which dragging them there changes. use std::collections::{HashMap, HashSet}; @@ -281,6 +282,25 @@ impl Library { } } + /// Move item `r` next to `to`, an item of its kind: just before it, or just after it when + /// `after`, in their kind's list (the order the panel shows them in). + fn place_next_to(&mut self, r: &ItemRef, to: &ItemRef, after: bool) { + fn shift(list: &mut Vec, key: impl Fn(&T) -> &str, from: &str, to: &str, after: bool) { + let Some(i) = list.iter().position(|x| key(x) == from) else { return }; + let item = list.remove(i); + let j = list.iter().position(|x| key(x) == to).map_or(list.len(), |j| j + usize::from(after)); + list.insert(j.min(list.len()), item); + } + let (from, to_key) = (r.item.as_str(), to.item.as_str()); + match r.kind { + ItemKind::Color => shift(&mut self.colors, |c| c.name.as_str(), from, to_key, after), + ItemKind::CharStyle => shift(&mut self.char_styles, |c| c.name.as_str(), from, to_key, after), + ItemKind::ParaStyle => shift(&mut self.para_styles, |c| c.name.as_str(), from, to_key, after), + ItemKind::Graphic => shift(&mut self.graphics, |g| g.id.as_str(), from, to_key, after), + ItemKind::Unknown => {} + } + } + /// Make a library read from a file (hand-edited, imported) safe and consistent: at most /// [`MAX_ITEMS`] items of each kind and [`MAX_GROUPS`] groups, names clipped to [`MAX_NAME`] /// characters and unique within their kind (a repeated one gets a number), sizes finite, a @@ -552,10 +572,19 @@ pub fn specs() -> Vec { "Move Library Item to Group", [], None, - "{library?, kind, item: name (graphic: id or name), group?: name (null or none: out of its group)} put an item in a group (out of the one it was in) → {library, kind, item, group: name or null}", + "{library?, kind, item: name (graphic: id or name), group?: name (null or none: out of its group), before?: item | after?: item (an item of the same kind, named as `item` is)} put an item in a group (out of the one it was in); with `before` or `after`, also move it just before or after that item in the order its kind is shown in, as dragging it in the Libraries panel does → {library, kind, item, group: name or null}", always, move_item ), + cmd!( + "library.moveGroup", + "Move Library Group", + [], + None, + "{library?, group: name, to: index} move a group to position `to` of the library's groups (0 = first; past the end: last), as dragging its header in the Libraries panel does → {library, group, to: where it went}", + always, + move_group + ), cmd!( query "library.export", "Export Library", @@ -1023,13 +1052,30 @@ fn move_item(s: &mut Session, p: &Value) -> Result { const C: &str = "library.moveItem"; let (id, kind, item) = item_param(s, p, C)?; let group = str_param(p, "group"); + let next_to = match (str_param(p, "before"), str_param(p, "after")) { + (Some(_), Some(_)) => return Err(bad(C, "give `before` or `after`, not both")), + (Some(b), None) => Some((b, false)), + (None, Some(a)) => Some((a, true)), + (None, None) => None, + }; let (key, to) = s.libraries.change(&id, |lib| { - let r = lib.item_ref(kind.item_kind(), item).ok_or_else(|| bad(C, format!("the library has no item `{item}`")))?; + let missing = |item: &str| bad(C, format!("the library has no item `{item}`")); + let r = lib.item_ref(kind.item_kind(), item).ok_or_else(|| missing(item))?; let at = group.map(|g| group_at(lib, g, C)).transpose()?; + let next_to = next_to + .map(|(other, after)| Ok::<_, EngineError>((lib.item_ref(kind.item_kind(), other).ok_or_else(|| missing(other))?, after))) + .transpose()?; + if let Some((other, after)) = &next_to + && *other != r + { + lib.place_next_to(&r, other, *after); + } lib.ungroup(&r); let key = r.item.clone(); let to = at.and_then(|i| lib.groups.get_mut(i)).map(|g| { - g.items.push(r); + // Next to the other item when that is in the group too, else last. + let near = next_to.as_ref().and_then(|(other, after)| g.items.iter().position(|x| x == other).map(|j| j + usize::from(*after))); + g.items.insert(near.unwrap_or(g.items.len()).min(g.items.len()), r); g.name.clone() }); Ok((key, to)) @@ -1037,6 +1083,22 @@ fn move_item(s: &mut Session, p: &Value) -> Result { Ok(json!({ "library": id, "kind": str_param(p, "kind"), "item": key, "group": to })) } +fn move_group(s: &mut Session, p: &Value) -> Result { + const C: &str = "library.moveGroup"; + let id = library_id(s, p, C)?; + let group = group_param(p, C)?; + let to = p.get("to").and_then(Value::as_u64).ok_or_else(|| bad(C, "missing `to` (an index)"))?; + let (name, at) = s.libraries.change(&id, |lib| { + let i = group_at(lib, group, C)?; + let g = lib.groups.remove(i); + let at = usize::try_from(to).unwrap_or(usize::MAX).min(lib.groups.len()); + let name = g.name.clone(); + lib.groups.insert(at, g); + Ok((name, at)) + })?; + Ok(json!({ "library": id, "group": name, "to": at })) +} + fn export(s: &mut Session, p: &Value) -> Result { const C: &str = "library.export"; let id = library_id(s, p, C)?; diff --git a/crates/engine/src/tests_library.rs b/crates/engine/src/tests_library.rs index bdc9c1e1b..15c8d9be8 100644 --- a/crates/engine/src/tests_library.rs +++ b/crates/engine/src/tests_library.rs @@ -276,6 +276,61 @@ fn items_are_grouped_moved_and_ungrouped() { assert_eq!(run(&mut s, "library.list", json!({}))["libraries"][0]["groups"], 1); } +/// The names of the current library's `list` ("colors", "graphics"…), in their order. +fn order(s: &mut Session, list: &str, key: &str) -> Vec { + let got = run(s, "library.get", json!({})); + got[list].as_array().unwrap().iter().map(|v| v[key].as_str().unwrap().to_string()).collect() +} + +/// Dragging in the Libraries panel: an item moves just before or after another of its kind, into +/// that one's group or out of every group, and groups move to another place in the list. +#[test] +fn items_and_groups_are_reordered() { + let mut s = session(); + run(&mut s, "library.create", json!({"name": "Brand"})); + for (name, color) in [("Red", "#ff0000"), ("Green", "#00ff00"), ("Blue", "#0000ff"), ("Gray", "#808080")] { + run(&mut s, "library.add", json!({"kind": "fillColor", "color": color, "name": name})); + } + // Before an item, and after one; on itself it stays. + run(&mut s, "library.moveItem", json!({"kind": "fillColor", "item": "Blue", "before": "Red"})); + assert_eq!(order(&mut s, "colors", "name"), ["Blue", "Red", "Green", "Gray"]); + run(&mut s, "library.moveItem", json!({"kind": "fillColor", "item": "Blue", "after": "Gray"})); + assert_eq!(order(&mut s, "colors", "name"), ["Red", "Green", "Gray", "Blue"]); + run(&mut s, "library.moveItem", json!({"kind": "fillColor", "item": "Red", "before": "Red"})); + assert_eq!(order(&mut s, "colors", "name"), ["Red", "Green", "Gray", "Blue"]); + // Into a group next to an item there: the group lists it next to that one too. + run( + &mut s, + "library.createGroup", + json!({"name": "Cool", "items": [{"kind": "fillColor", "item": "Green"}, {"kind": "fillColor", "item": "Blue"}]}), + ); + let r = run(&mut s, "library.moveItem", json!({"kind": "fillColor", "item": "Gray", "group": "Cool", "before": "Blue"})); + assert_eq!(r["group"], "Cool"); + assert_eq!(order(&mut s, "colors", "name"), ["Red", "Green", "Gray", "Blue"]); + assert_eq!(groups(&mut s), [("Cool".into(), refs(&[("color", "Green"), ("color", "Gray"), ("color", "Blue")]))]); + // Next to an ungrouped item, without a group: out of its group. + run(&mut s, "library.moveItem", json!({"kind": "fillColor", "item": "Blue", "before": "Red"})); + assert_eq!(order(&mut s, "colors", "name"), ["Blue", "Red", "Green", "Gray"]); + assert_eq!(groups(&mut s), [("Cool".into(), refs(&[("color", "Green"), ("color", "Gray")]))]); + // Wrong asks change nothing. + assert!(s.execute("library.moveItem", &json!({"kind": "fillColor", "item": "Red", "before": "Nope"})).is_err()); + assert!(s.execute("library.moveItem", &json!({"kind": "fillColor", "item": "Red", "before": "Blue", "after": "Gray"})).is_err()); + assert!(s.execute("library.moveItem", &json!({"kind": "charStyle", "item": "Red", "before": "Blue"})).is_err(), "not a style"); + assert_eq!(order(&mut s, "colors", "name"), ["Blue", "Red", "Green", "Gray"]); + // Groups: to an index, past the end to the last place. + run(&mut s, "library.createGroup", json!({"name": "Warm"})); + run(&mut s, "library.createGroup", json!({"name": "Neutral"})); + let names = |s: &mut Session| groups(s).into_iter().map(|(n, _)| n).collect::>(); + assert_eq!(run(&mut s, "library.moveGroup", json!({"group": "neutral", "to": 0}))["to"], 0); + assert_eq!(names(&mut s), ["Neutral", "Cool", "Warm"]); + assert_eq!(run(&mut s, "library.moveGroup", json!({"group": "Neutral", "to": 99}))["to"], 2); + assert_eq!(names(&mut s), ["Cool", "Warm", "Neutral"]); + assert!(s.execute("library.moveGroup", &json!({"group": "Nope", "to": 0})).is_err()); + assert!(s.execute("library.moveGroup", &json!({"group": "Cool", "to": -1})).is_err()); + assert!(s.execute("library.moveGroup", &json!({"group": "Cool"})).is_err()); + assert_eq!(groups(&mut s)[0].1, refs(&[("color", "Green"), ("color", "Gray")]), "a group keeps its items when it moves"); +} + /// A library goes out to a file with its art and groups and comes back as a new library, numbered /// when its name is taken; the graphic it brings places in a document (#926). #[test] diff --git a/crates/engine/tests/import_fuzz.rs b/crates/engine/tests/import_fuzz.rs index b420725ed..b530f7883 100644 --- a/crates/engine/tests/import_fuzz.rs +++ b/crates/engine/tests/import_fuzz.rs @@ -988,7 +988,8 @@ fn styles(what: &str, data: &str) -> Result<(), TestCaseError> { } /// Libraries panel library file `data` imported, then each kind's first item used on the rich -/// document and moved into its first group, the group deleted and the library exported (#926). +/// document, moved into its first group and next to its kind's last item, the group moved and +/// deleted and the library exported (#926). fn libraries(what: &str, data: &str) -> Result<(), TestCaseError> { survive_library(what, "library.import", data, |s, r| { let lib = r["id"].clone(); @@ -1001,9 +1002,17 @@ fn libraries(what: &str, data: &str) -> Result<(), TestCaseError> { let _ = no_panic(s.execute("library.use", &json!({"library": lib, "kind": kind, "item": item[key]}))); let to = json!({"library": lib, "kind": kind, "item": item[key], "group": got["groups"][0]["name"]}); let _ = no_panic(s.execute("library.moveItem", &to)); + // Next to the last item of its kind, whatever group that is in. + let last = got[list].as_array().and_then(|a| a.last()).map(|v| v[key].clone()).unwrap_or_default(); + for side in ["before", "after"] { + let _ = no_panic(s.execute("library.moveItem", &json!({"library": lib, "kind": kind, "item": item[key], side: last}))); + } } } if let Some(g) = got["groups"].get(0) { + for to in [0, 1, 1_000_000] { + let _ = no_panic(s.execute("library.moveGroup", &json!({"library": lib, "group": g["name"], "to": to}))); + } let _ = no_panic(s.execute("library.deleteGroup", &json!({"library": lib, "group": g["name"]}))); } let _ = no_panic(s.execute("library.export", &json!({"library": lib}))); diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 265eb9a4c..d919375af 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -2040,6 +2040,8 @@ fn panel_drop(app: &mut VectorcraftApp, ui: &Ui, resp: &egui::Response, xf: &Xf) // Art dragged back onto the canvas: its move was already dropped. (A symbol was placed // above.) widgets::PanelDrag::Art(_) | widgets::PanelDrag::Symbol(_) | widgets::PanelDrag::LibraryGraphic { .. } => return, + // A Libraries panel style or group only moves within that panel. + widgets::PanelDrag::LibraryItem { .. } | widgets::PanelDrag::LibraryGroup { .. } => return, }; if let Err(e) = app.run(cmd, params) { app.status(e); @@ -6958,7 +6960,7 @@ mod tests { let fill = |app: &VectorcraftApp| app.session.active().unwrap().doc.node(vectorcraft_doc::NodeId(id)).unwrap().appearance.fill_paint(); let red = Paint::solid(Color::from_hex("#ed1c24").unwrap()); let rows = SwatchRows { grabbed: "Red".into(), names: vec!["Red".into()], groups: false }; - let swatch = PanelDrag::Paint { paint: red.clone(), params: json!({"swatch": "Red"}), rows: Some(rows) }; + let swatch = PanelDrag::Paint { paint: red.clone(), params: json!({"swatch": "Red"}), rows: Some(rows), library: None }; // Off the art nothing happens. drop(&mut app, swatch.clone(), Point::new(300.0, 250.0)); assert_eq!(fill(&app), Paint::solid(Color::WHITE)); diff --git a/crates/ui-egui/src/dock.rs b/crates/ui-egui/src/dock.rs index e881559d1..8e0c63dcf 100644 --- a/crates/ui-egui/src/dock.rs +++ b/crates/ui-egui/src/dock.rs @@ -467,7 +467,11 @@ pub(crate) fn panel_body(app: &mut VectorcraftApp, ui: &mut Ui, id: &str, width: ui.set_max_height(tall); panels::layers::show(app, ui); } - Some(DockTab::Libraries) => panels::libraries(app, ui), + Some(DockTab::Libraries) => { + // Its list scrolls inside `tall` (as in the dock) instead of growing the flyout. + ui.set_max_height(tall); + panels::libraries(app, ui); + } None => panels::show_icon_panel(app, ui, id), } }); diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index 85c7f28a8..c17103c02 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -3093,7 +3093,7 @@ Delete Library Item Изтриване на елемент от библиотека No results Няма резултати No items in this library match “{query}”. Няма елементи в тази библиотека, съвпадащи с „{query}“. - Right-click an item to move it into this group. Щракнете с десния бутон върху елемент, за да го преместите в тази група. + Drag an item here, or right-click one, to put it in this group. Плъзнете елемент тук или щракнете с десния бутон върху него, за да го поставите в тази група. New Group Нова група New Group… Нова група… Move to Group Преместване в група @@ -5204,5 +5204,7 @@ theme Appearance Външен вид Plastic Wrap Найлоново фолио Chrome… Хром… Chrome Хром + Drop here to take it out of its group Пуснете тук, за да го извадите от групата му + Move Library Group Преместване на библиотечна група Plaster… Гипс… Plaster Гипс diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 847b759d7..92cab140b 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -1657,7 +1657,7 @@ lighting Light Světlo This library is empty Tato knihovna je prázdná No results Žádné výsledky No items in this library match “{query}”. Žádná položka v této knihovně neodpovídá „{query}“. - Right-click an item to move it into this group. Kliknutím pravým tlačítkem na položku ji přesunete do této skupiny. + Drag an item here, or right-click one, to put it in this group. Přetáhněte sem položku nebo na ni klikněte pravým tlačítkem a vložte ji do této skupiny. New Group Nová skupina New Group… Nová skupina… Move to Group Přesunout do skupiny @@ -5243,5 +5243,7 @@ mark-style Japanese Japonské Plastic Wrap Plastová fólie Chrome… Chrom… Chrome Chrom + Drop here to take it out of its group Pusťte sem, chcete-li ji vyjmout ze skupiny + Move Library Group Přesunout skupinu knihovny Plaster… Sádra… Plaster Sádra diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index 83bcfc641..44a68868a 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -1489,7 +1489,7 @@ lighting Light Lys This library is empty Dette bibliotek er tomt No results Ingen resultater No items in this library match “{query}”. Ingen elementer i dette bibliotek matcher »{query}«. - Right-click an item to move it into this group. Højreklik på et element for at flytte det til denne gruppe. + Drag an item here, or right-click one, to put it in this group. Træk et element hertil, eller højreklik på et, for at lægge det i denne gruppe. New Group Ny gruppe New Group… Ny gruppe… Move to Group Flyt til gruppe @@ -5222,5 +5222,7 @@ swatch Bright Violet Kraftig violet Plastic Wrap Plastfolie Chrome… Krom… Chrome Krom + Drop here to take it out of its group Slip her for at tage det ud af gruppen + Move Library Group Flyt biblioteksgruppe Plaster… Gips… Plaster Gips diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 53ef2d1b7..3a20ea232 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -4362,7 +4362,7 @@ theme Appearance Erscheinungsbild Move to Group In Gruppe verschieben No results Keine Ergebnisse No items in this library match “{query}”. Keine Elemente in dieser Bibliothek entsprechen „{query}“. - Right-click an item to move it into this group. Klicken Sie mit der rechten Maustaste auf ein Element, um es in diese Gruppe zu verschieben. + Drag an item here, or right-click one, to put it in this group. Ziehen Sie ein Element hierher oder klicken Sie mit der rechten Maustaste darauf, um es in diese Gruppe zu legen. @msg Imported library {name} Bibliothek {name} importiert # Linked files changed on disk. Update Changed Links Geänderte Verknüpfungen aktualisieren @@ -4823,5 +4823,7 @@ effect Soft Weich Plastic Wrap Kunststofffolie Chrome… Chrom… Chrome Chrom + Drop here to take it out of its group Hier ablegen, um es aus seiner Gruppe zu nehmen + Move Library Group Bibliotheksgruppe verschieben Plaster… Gipsabdruck… Plaster Gipsabdruck diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 469f19ee8..fd6177520 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -3492,7 +3492,7 @@ theme Appearance Εμφάνιση Delete Library Item Διαγραφή στοιχείου βιβλιοθήκης No results Δεν βρέθηκαν αποτελέσματα No items in this library match “{query}”. Κανένα στοιχείο αυτής της βιβλιοθήκης δεν αντιστοιχεί στο «{query}». - Right-click an item to move it into this group. Κάντε δεξί κλικ σε στοιχείο για να το μετακινήσετε σε αυτήν την ομάδα. + Drag an item here, or right-click one, to put it in this group. Σύρετε εδώ ένα στοιχείο ή κάντε δεξί κλικ σε ένα για να το βάλετε σε αυτήν την ομάδα. Move to Group Μετακίνηση σε ομάδα No Group Χωρίς ομάδα Import Library… Εισαγωγή βιβλιοθήκης… @@ -5195,5 +5195,7 @@ effect Points Σημεία Plastic Wrap Πλαστική μεμβράνη Chrome… Χρώμιο… Chrome Χρώμιο + Drop here to take it out of its group Αφήστε εδώ για να το βγάλετε από την ομάδα του + Move Library Group Μετακίνηση ομάδας βιβλιοθήκης Plaster… Γύψος… Plaster Γύψος diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 422aa4c73..4582ff4ff 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -3152,7 +3152,7 @@ Delete Library Item Eliminar elemento de la biblioteca No results Sin resultados No items in this library match “{query}”. Ningún elemento de esta biblioteca coincide con “{query}”. - Right-click an item to move it into this group. Haz clic con el botón derecho en un elemento para moverlo a este grupo. + Drag an item here, or right-click one, to put it in this group. Arrastra aquí un elemento, o haz clic con el botón derecho en uno, para ponerlo en este grupo. New Group Nuevo grupo New Group… Nuevo grupo… Move to Group Mover al grupo @@ -5185,5 +5185,7 @@ mark-style Japanese Japonesas Plastic Wrap Envoltura plástica Chrome… Cromo… Chrome Cromo + Drop here to take it out of its group Suelta aquí para sacarlo de su grupo + Move Library Group Mover grupo de biblioteca Plaster… Yeso… Plaster Yeso diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index 092762a31..b0f96dff1 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -1620,7 +1620,7 @@ lighting Light Valo This library is empty Tämä kirjasto on tyhjä No results Ei tuloksia No items in this library match “{query}”. Mikään tämän kirjaston kohde ei vastaa hakua ”{query}”. - Right-click an item to move it into this group. Siirrä kohde tähän ryhmään napsauttamalla sitä hiiren kakkospainikkeella. + Drag an item here, or right-click one, to put it in this group. Vedä kohde tähän tai napsauta sitä hiiren kakkospainikkeella lisätäksesi sen tähän ryhmään. New Group Uusi ryhmä New Group… Uusi ryhmä… Move to Group Siirrä ryhmään @@ -5274,5 +5274,7 @@ swatch Bright Violet Kirkas violetti Plastic Wrap Muovikalvo Chrome… Kromi… Chrome Kromi + Drop here to take it out of its group Pudota tähän poistaaksesi sen ryhmästään + Move Library Group Siirrä kirjastoryhmä Plaster… Kipsi… Plaster Kipsi diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 316c84b0f..3624a86b6 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -3149,7 +3149,7 @@ Delete Library Item Supprimer l’élément de la bibliothèque No results Aucun résultat No items in this library match “{query}”. Aucun élément de cette bibliothèque ne correspond à « {query} ». - Right-click an item to move it into this group. Cliquez avec le bouton droit sur un élément pour le placer dans ce groupe. + Drag an item here, or right-click one, to put it in this group. Faites glisser un élément ici, ou cliquez dessus avec le bouton droit, pour le placer dans ce groupe. New Group Nouveau groupe New Group… Nouveau groupe… Move to Group Déplacer vers le groupe @@ -5242,5 +5242,7 @@ theme Appearance Apparence Plastic Wrap Emballage plastique Chrome… Chrome… Chrome Chrome + Drop here to take it out of its group Déposez ici pour le sortir de son groupe + Move Library Group Déplacer le groupe de bibliothèque Plaster… Plâtre… Plaster Plâtre diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index ebc796cf9..4de781a7b 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -3171,7 +3171,7 @@ mark-style Japanese Japán Right Indent Jobb behúzás Right Plane Jobb sík Right to Left Jobbról balra - Right-click an item to move it into this group. Egy elemet jobb gombbal kattintva helyezhetsz át ebbe a csoportba. + Drag an item here, or right-click one, to put it in this group. Húzz ide egy elemet, vagy kattints rá jobb gombbal, hogy ebbe a csoportba kerüljön. Right-Justified Tab Jobbra igazító tabulátor Right-to-Left Paragraph Direction Jobbról balra haladó bekezdés Right: Jobb: @@ -5229,5 +5229,7 @@ imageTrace Outline Körvonal Plastic Wrap Fóliázás Chrome… Króm… Chrome Króm + Drop here to take it out of its group Engedd el itt, hogy kikerüljön a csoportjából + Move Library Group Könyvtárcsoport áthelyezése Plaster… Gipsz… Plaster Gipsz diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index d12d02b72..e6ed1b753 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -1489,7 +1489,7 @@ lighting Light Cahaya This library is empty Pustaka ini kosong No results Tidak ada hasil No items in this library match “{query}”. Tidak ada item di pustaka ini yang cocok dengan “{query}”. - Right-click an item to move it into this group. Klik kanan item untuk memindahkannya ke grup ini. + Drag an item here, or right-click one, to put it in this group. Seret item ke sini, atau klik kanan item, untuk memasukkannya ke grup ini. New Group Grup baru New Group… Grup baru… Move to Group Pindahkan ke grup @@ -5222,5 +5222,7 @@ swatch Bright Violet Violet cerah Plastic Wrap Pembungkus plastik Chrome… Krom… Chrome Krom + Drop here to take it out of its group Lepaskan di sini untuk mengeluarkannya dari grupnya + Move Library Group Pindahkan grup pustaka Plaster… Plester… Plaster Plester diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index c2df0063a..7145e2147 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -3185,7 +3185,7 @@ mark-style Japanese Giapponese Delete Library Item Elimina elemento della libreria No results Nessun risultato No items in this library match “{query}”. Nessun elemento di questa libreria corrisponde a “{query}”. - Right-click an item to move it into this group. Fai clic con il pulsante destro su un elemento per spostarlo in questo gruppo. + Drag an item here, or right-click one, to put it in this group. Trascina qui un elemento, o fai clic con il pulsante destro su uno, per metterlo in questo gruppo. New Group Nuovo gruppo New Group… Nuovo gruppo… Move to Group Sposta nel gruppo @@ -4857,5 +4857,7 @@ lighting Light Luce Plastic Wrap Pellicola plastica Chrome… Cromo… Chrome Cromo + Drop here to take it out of its group Rilascia qui per toglierlo dal suo gruppo + Move Library Group Sposta gruppo di libreria Plaster… Gesso… Plaster Gesso diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 12bb4413c..ae8f3b869 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -2917,7 +2917,7 @@ mark-style Japanese 日本式 Delete Library Item ライブラリ項目を削除 No results 結果はありません No items in this library match “{query}”. このライブラリには「{query}」に一致する項目がありません。 - Right-click an item to move it into this group. 項目を右クリックすると、このグループに移動できます。 + Drag an item here, or right-click one, to put it in this group. 項目をここにドラッグするか、項目を右クリックすると、このグループに入れられます。 New Group 新規グループ New Group… 新規グループ… Move to Group グループに移動 @@ -4897,5 +4897,7 @@ lighting Light ライト Plastic Wrap ラップ Chrome… クロム… Chrome クロム + Drop here to take it out of its group ここにドロップするとグループから外れます + Move Library Group ライブラリグループを移動 Plaster… 石膏… Plaster 石膏 diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index 8a5913c2a..0a186e642 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -1398,7 +1398,7 @@ lighting Light სინათლე This library is empty ეს ბიბლიოთეკა ცარიელია No results შედეგები არ არის No items in this library match “{query}”. ამ ბიბლიოთეკაში „{query}“-ს შესაბამისი ელემენტი არ არის. - Right-click an item to move it into this group. ამ ჯგუფში გადასატანად დაწკაპეთ ელემენტზე მარჯვენა ღილაკით. + Drag an item here, or right-click one, to put it in this group. ამ ჯგუფში ჩასასმელად ჩაავლეთ ელემენტი აქ ან დაწკაპეთ მასზე მარჯვენა ღილაკით. New Group ახალი ჯგუფი New Group… ახალი ჯგუფი… Move to Group ჯგუფში გადატანა @@ -5150,5 +5150,7 @@ imageTrace Outline კონტური Plastic Wrap პლასტიკური აფსკი Chrome… ქრომი… Chrome ქრომი + Drop here to take it out of its group ჯგუფიდან ამოსაღებად ჩააგდეთ აქ + Move Library Group ბიბლიოთეკის ჯგუფის გადატანა Plaster… თაბაშირი… Plaster თაბაშირი diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index b73f39c8f..6b896f0e5 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -2911,7 +2911,7 @@ mark-style Japanese 일본식 Delete Library Item 라이브러리 항목 삭제 No results 결과 없음 No items in this library match “{query}”. 이 라이브러리에 “{query}”와(과) 일치하는 항목이 없습니다. - Right-click an item to move it into this group. 항목을 마우스 오른쪽 버튼으로 클릭해 이 그룹으로 이동하세요. + Drag an item here, or right-click one, to put it in this group. 항목을 여기로 끌어 놓거나 마우스 오른쪽 버튼으로 클릭해 이 그룹에 넣으세요. New Group 새 그룹 New Group… 새 그룹… Move to Group 그룹으로 이동 @@ -4820,5 +4820,7 @@ lighting Light 광원 Plastic Wrap 비닐 랩 Chrome… 크롬… Chrome 크롬 + Drop here to take it out of its group 그룹에서 빼려면 여기에 놓으세요 + Move Library Group 라이브러리 그룹 이동 Plaster… 석고… Plaster 석고 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index cad5aa8e2..f3b5b2f6b 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -1503,7 +1503,7 @@ lighting Light Licht This library is empty Deze bibliotheek is leeg No results Geen resultaten No items in this library match “{query}”. Geen items in deze bibliotheek komen overeen met „{query}”. - Right-click an item to move it into this group. Klik met de rechtermuisknop op een item om het naar deze groep te verplaatsen. + Drag an item here, or right-click one, to put it in this group. Sleep een item hierheen, of klik er met de rechtermuisknop op, om het in deze groep te zetten. New Group Nieuwe groep New Group… Nieuwe groep… Move to Group Verplaatsen naar groep @@ -5264,5 +5264,7 @@ swatch Bright Violet Felviolet Plastic Wrap Plasticfolie Chrome… Chroom… Chrome Chroom + Drop here to take it out of its group Zet het hier neer om het uit de groep te halen + Move Library Group Bibliotheekgroep verplaatsen Plaster… Gips… Plaster Gips diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 6890802c5..3516fa39f 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -1577,7 +1577,7 @@ lighting Light Światło This library is empty Ta biblioteka jest pusta No results Brak wyników No items in this library match “{query}”. Żaden element tej biblioteki nie pasuje do „{query}”. - Right-click an item to move it into this group. Kliknij element prawym przyciskiem, aby przenieść go do tej grupy. + Drag an item here, or right-click one, to put it in this group. Przeciągnij tu element albo kliknij go prawym przyciskiem, aby umieścić go w tej grupie. New Group Nowa grupa New Group… Nowa grupa… Move to Group Przenieś do grupy @@ -5095,5 +5095,7 @@ mark-style Japanese Japońskie Plastic Wrap Folia plastikowa Chrome… Chrom… Chrome Chrom + Drop here to take it out of its group Upuść tutaj, aby wyjąć go z grupy + Move Library Group Przenieś grupę biblioteki Plaster… Gips… Plaster Gips diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index d03a04614..9a4e7cc21 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -3242,7 +3242,7 @@ lighting Light Luz Delete Library Item Excluir item da biblioteca No results Nenhum resultado No items in this library match “{query}”. Nenhum item desta biblioteca corresponde a “{query}”. - Right-click an item to move it into this group. Clique com o botão direito em um item para movê-lo para este grupo. + Drag an item here, or right-click one, to put it in this group. Arraste um item para cá, ou clique com o botão direito em um, para colocá-lo neste grupo. New Group Novo grupo Move to Group Mover para o grupo Import Library Importar biblioteca @@ -5160,5 +5160,7 @@ mark-style Japanese Japonesas Plastic Wrap Envoltório plástico Chrome… Cromo… Chrome Cromo + Drop here to take it out of its group Solte aqui para tirá-lo do grupo + Move Library Group Mover grupo de biblioteca Plaster… Gesso… Plaster Gesso diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 64fd768f7..dbae43d88 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -3181,7 +3181,7 @@ mark-style Japanese Японский Delete Library Item Удалить элемент библиотеки No results Ничего не найдено No items in this library match “{query}”. В этой библиотеке нет элементов, соответствующих «{query}». - Right-click an item to move it into this group. Щёлкните элемент правой кнопкой мыши, чтобы переместить его в эту группу. + Drag an item here, or right-click one, to put it in this group. Перетащите сюда элемент или щёлкните его правой кнопкой мыши, чтобы поместить его в эту группу. New Group Новая группа New Group… Новая группа… Move to Group Переместить в группу @@ -4853,5 +4853,7 @@ lighting Light Свет Plastic Wrap Целлофановая упаковка Chrome… Хром… Chrome Хром + Drop here to take it out of its group Отпустите здесь, чтобы вынуть его из группы + Move Library Group Переместить группу библиотеки Plaster… Гипс… Plaster Гипс diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index 7e32d6905..f55b892be 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -3093,7 +3093,7 @@ Delete Library Item Radera biblioteksobjekt No results Inga resultat No items in this library match “{query}”. Inga objekt i det här biblioteket matchar ”{query}”. - Right-click an item to move it into this group. Högerklicka på ett objekt för att flytta det till den här gruppen. + Drag an item here, or right-click one, to put it in this group. Dra hit ett objekt, eller högerklicka på ett, för att lägga det i den här gruppen. New Group Ny grupp New Group… Ny grupp… Move to Group Flytta till grupp @@ -5199,5 +5199,7 @@ theme Appearance Utseende Plastic Wrap Plastfolie Chrome… Krom… Chrome Krom + Drop here to take it out of its group Släpp här för att ta ut det ur gruppen + Move Library Group Flytta biblioteksgrupp Plaster… Gips… Plaster Gips diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index c60cf49ba..a137a105e 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -3178,7 +3178,7 @@ effect Points Точки Delete Library Item Видалити елемент бібліотеки No results Нічого не знайдено No items in this library match “{query}”. У цій бібліотеці немає елементів, що відповідають «{query}». - Right-click an item to move it into this group. Клацніть елемент правою кнопкою миші, щоб перемістити його в цю групу. + Drag an item here, or right-click one, to put it in this group. Перетягніть сюди елемент або клацніть його правою кнопкою миші, щоб покласти його в цю групу. New Group Нова група New Group… Нова група… Move to Group Перемістити в групу @@ -4837,5 +4837,7 @@ lighting Light Світло Plastic Wrap Целофанова плівка Chrome… Хром… Chrome Хром + Drop here to take it out of its group Відпустіть тут, щоб вийняти його з групи + Move Library Group Перемістити групу бібліотеки Plaster… Гіпс… Plaster Гіпс diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index 0257cbea5..8d948c2b5 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -2926,7 +2926,7 @@ mark-style Japanese Kiểu Nhật Right Indent Thụt lề phải Right Plane Mặt phẳng phải Right to Left Phải sang trái - Right-click an item to move it into this group. Bấm chuột phải vào mục để di chuyển vào nhóm này. + Drag an item here, or right-click one, to put it in this group. Kéo một mục vào đây, hoặc bấm chuột phải vào mục, để đưa vào nhóm này. Right-Justified Tab Tab căn phải Right-to-Left Paragraph Direction Hướng đoạn văn từ phải sang trái Right: Phải: @@ -4837,5 +4837,7 @@ theme Appearance Giao diện Plastic Wrap Màng bọc nhựa Chrome… Crôm… Chrome Crôm + Drop here to take it out of its group Thả vào đây để đưa ra khỏi nhóm + Move Library Group Di chuyển nhóm thư viện Plaster… Thạch cao… Plaster Thạch cao diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index d3338d2be..b425b436d 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -3036,7 +3036,7 @@ font-script Japanese 汉字 Delete Library Item 删除库项目 No results 无结果 No items in this library match “{query}”. 此库中没有与“{query}”匹配的项目。 - Right-click an item to move it into this group. 右键单击项目可将其移到此组。 + Drag an item here, or right-click one, to put it in this group. 将项目拖到此处,或右键单击项目,即可将其放入此组。 New Group 新建组 New Group… 新建组… Move to Group 移到组 @@ -5080,5 +5080,7 @@ lighting Light 光源 Plastic Wrap 塑料包装 Chrome… 铬黄… Chrome 铬黄 + Drop here to take it out of its group 放到此处可将其移出所在组 + Move Library Group 移动库组 Plaster… 塑料效果… Plaster 塑料效果 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index caaf244b6..1e4940d16 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -3904,7 +3904,7 @@ font-script Japanese 漢字 Delete Library Item 刪除資料庫項目 No results 沒有結果 No items in this library match “{query}”. 此資料庫中沒有符合「{query}」的項目。 - Right-click an item to move it into this group. 在項目上按一下右鍵,即可將其移至此群組。 + Drag an item here, or right-click one, to put it in this group. 將項目拖到此處,或在項目上按一下右鍵,即可將其放入此群組。 New Group 新增群組 New Group… 新增群組… Move to Group 移至群組 @@ -4875,5 +4875,7 @@ lighting Light 光源 Plastic Wrap 塑膠包裝 Chrome… 鉻黃… Chrome 鉻黃 + Drop here to take it out of its group 放到此處即可將其移出所屬群組 + Move Library Group 移動資料庫群組 Plaster… 塑膠效果… Plaster 塑膠效果 diff --git a/crates/ui-egui/src/panels/gradient.rs b/crates/ui-egui/src/panels/gradient.rs index 2213ed934..32c446829 100644 --- a/crates/ui-egui/src/panels/gradient.rs +++ b/crates/ui-egui/src/panels/gradient.rs @@ -1044,7 +1044,7 @@ mod tests { // A spot swatch tile dropped on a stop links it: editing the swatch recolours the stop. app.run("swatch.new", json!({"name": "Ink", "color": "#cc0066", "spot": true})).unwrap(); let ink = app.session.doc().unwrap().doc.swatch("Ink").unwrap().paint.clone(); - egui::DragAndDrop::set_payload(&p.ctx, PanelDrag::Paint { paint: ink, params: json!({"swatch": "Ink"}), rows: None }); + egui::DragAndDrop::set_payload(&p.ctx, PanelDrag::Paint { paint: ink, params: json!({"swatch": "Ink"}), rows: None, library: None }); p.frame(&mut app, vec![Event::PointerMoved(a)], Modifiers::NONE); p.frame( &mut app, diff --git a/crates/ui-egui/src/panels/graphic_styles.rs b/crates/ui-egui/src/panels/graphic_styles.rs index d20e54a22..6c6ba2791 100644 --- a/crates/ui-egui/src/panels/graphic_styles.rs +++ b/crates/ui-egui/src/panels/graphic_styles.rs @@ -351,7 +351,12 @@ fn zone_input(ui: &Ui, zone: &Response, d: &Document, ev: &mut Events) { PanelDrag::GraphicStyle(n) => (Some(n), None), PanelDrag::Art(ids) => (None, ids.first().copied()), PanelDrag::Appearance(id) => (None, Some(*id)), - PanelDrag::Paint { .. } | PanelDrag::Symbol(_) | PanelDrag::Brush { .. } | PanelDrag::LibraryGraphic { .. } => (None, None), + PanelDrag::Paint { .. } + | PanelDrag::Symbol(_) + | PanelDrag::Brush { .. } + | PanelDrag::LibraryGraphic { .. } + | PanelDrag::LibraryItem { .. } + | PanelDrag::LibraryGroup { .. } => (None, None), }; if style.is_none() && source.is_none() { return; diff --git a/crates/ui-egui/src/panels/libraries.rs b/crates/ui-egui/src/panels/libraries.rs index f0ec3093a..8fa48da92 100644 --- a/crates/ui-egui/src/panels/libraries.rs +++ b/crates/ui-egui/src/panels/libraries.rs @@ -8,6 +8,12 @@ //! The search field filters the items by name; items can be moved into user-named groups, shown as //! collapsible sections after the ungrouped items; and the panel menu exports the library to a //! `.vclibrary` file and imports one as a new library (#926). +//! +//! The list fills the panel down to its bottom bar and scrolls (also while a drag nears its top or +//! bottom edge). An item dragged within it lands before or after another of its kind, in that one's +//! group (`library.moveItem` with `before`/`after`), or, dropped elsewhere on a group or on the +//! ungrouped items, goes into that group or out of its own; a group dragged by its header goes +//! before or after another group (`library.moveGroup`). use std::collections::HashMap; @@ -24,6 +30,13 @@ use crate::{VectorcraftApp, icons}; const TILE: f32 = 56.0; /// The size of a colour chip. const CHIP: f32 = 22.0; +/// The height under the list the bottom bar takes (its divider and buttons) until it has been +/// measured. +const BAR: f32 = 42.0; +/// The least height the list takes. +const MIN_LIST: f32 = 140.0; +/// How near the list's top or bottom edge a drag scrolls it. +const EDGE: f32 = 28.0; /// The current library: its id and a copy of it (the panel menu acts on it after reading). fn current(app: &VectorcraftApp) -> Option<(String, Library)> { @@ -75,6 +88,25 @@ enum Act { RenameGroup(String), } +/// What a drag in the panel holds of the library shown: one of its items, or one of its groups +/// (by the group's header). +#[derive(Clone, Debug, PartialEq)] +enum Grab { + Item(ItemKind, String), + Group(String), +} + +/// The item or group of library `library` that `drag` holds, if it holds one. +fn grabbed(drag: &PanelDrag, library: &str) -> Option { + match drag { + PanelDrag::Paint { library: Some((l, item)), .. } if l == library => Some(Grab::Item(ItemKind::Color, item.clone())), + PanelDrag::LibraryGraphic { library: l, item } if l == library => Some(Grab::Item(ItemKind::Graphic, item.clone())), + PanelDrag::LibraryItem { library: l, kind, item } if l == library => Some(Grab::Item(*kind, item.clone())), + PanelDrag::LibraryGroup { library: l, group } if l == library => Some(Grab::Group(group.clone())), + _ => None, + } +} + /// What the item list draws from. struct Items<'a> { library: &'a str, @@ -82,6 +114,8 @@ struct Items<'a> { /// The group of each grouped item ([`Library::group_index`]). groups: HashMap<(ItemKind, &'a str), usize>, fill_active: bool, + /// What a drag in progress holds of this library. + grab: Option, } impl Items<'_> { @@ -121,7 +155,8 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let query = query.trim().to_lowercase(); let searching = !query.is_empty(); let shown = |name: &str| !searching || name.to_lowercase().contains(&query); - let items = Items { library: &lib_id, lib, groups: lib.group_index(), fill_active }; + let grab = egui::DragAndDrop::payload::(ui.ctx()).and_then(|d| grabbed(&d, &lib_id)); + let items = Items { library: &lib_id, lib, groups: lib.group_index(), fill_active, grab }; // The items shown in each section: the ungrouped ones first, then each group's. let mut counts = vec![0usize; lib.groups.len() + 1]; for (kind, key, _) in lib.items().filter(|(_, _, name)| shown(name)) { @@ -130,76 +165,113 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { } } ui.add_space(4.0); + // The list takes the panel's height down to the bottom bar (as tall as it was last frame), and + // scrolls. + let bar_id = egui::Id::new("library-bottom-bar"); + let bar: f32 = ui.data(|d| d.get_temp(bar_id)).unwrap_or(BAR); + let list_h = ui.available_height() - bar; + let list_h = if list_h.is_finite() { list_h.max(MIN_LIST) } else { MIN_LIST }; + // Each section's rows: the ungrouped items (None), then each group (its index), header included. + let mut sections: Vec<(Option, Rect)> = vec![]; + // Whether an item takes the drag in progress (dropped next to it), so its section doesn't. + let mut over_item = false; let list_rect = widgets::list_box(ui, |ui| { - ui.set_min_height(140.0); - ui.set_width(ui.available_width()); - if empty { - super::empty_state( - ui, - "library", - tl!("This library is empty"), - tl!("Drag art or swatches here, or use + to add the selection's colors and text styles."), - ); - return ui.min_rect(); - } - if searching && counts.iter().all(|n| *n == 0) { - let body = crate::i18n::fmt(tl!("No items in this library match “{query}”."), &[("query", &query)]); - super::empty_state(ui, "search", tl!("No results"), &body); - return ui.min_rect(); - } - egui::Frame::NONE.inner_margin(egui::Margin::same(6)).show(ui, |ui| { - items.show(ui, &mut act, |kind, key, name| items.group_of(kind, key).is_none() && shown(name)); - for (i, g) in lib.groups.iter().enumerate() { - let n = counts.get(i + 1).copied().unwrap_or_default(); - if searching && n == 0 { - continue; - } - // Open unless closed; open while searching, to show what matched. - let key = format!("library-group-closed:{lib_id}:{}", g.name); - let closed: bool = super::pstate(ui.ctx(), &key); - let open = searching || !closed; - let header = widgets::section_toggle_name(ui, &g.name, open); - ui.painter().text( - header.rect.right_center() - vec2(6.0, 0.0), - egui::Align2::RIGHT_CENTER, - n.to_string(), - egui::FontId::proportional(11.5), - t.text_dim, + let out = egui::ScrollArea::vertical().id_salt("library-scroll").max_height(list_h).auto_shrink([false, false]).show(ui, |ui| { + ui.set_width(ui.available_width()); + if empty { + super::empty_state( + ui, + "library", + tl!("This library is empty"), + tl!("Drag art or swatches here, or use + to add the selection's colors and text styles."), ); - if header.clicked() && !searching { - super::set_pstate(ui.ctx(), &key, !closed); + return; + } + if searching && counts.iter().all(|n| *n == 0) { + let body = crate::i18n::fmt(tl!("No items in this library match “{query}”."), &[("query", &query)]); + super::empty_state(ui, "search", tl!("No results"), &body); + return; + } + egui::Frame::NONE.inner_margin(egui::Margin::same(6)).show(ui, |ui| { + let span = |ui: &Ui, top: f32| Rect::from_x_y_ranges(ui.max_rect().x_range(), top..=ui.cursor().top()); + let top = ui.cursor().top(); + if counts.first().is_some_and(|n| *n > 0) { + items.show(ui, &mut act, &mut over_item, |kind, key, name| items.group_of(kind, key).is_none() && shown(name)); + } else if let Some(Grab::Item(kind, item)) = &items.grab + && items.group_of(*kind, item).is_some() + { + no_group_strip(ui); } - header.context_menu(|ui| { - if menu_item(ui, "Rename Group…", true, false) { - act = Some(Act::RenameGroup(g.name.clone())); - ui.close(); + sections.push((None, span(ui, top))); + for (i, g) in lib.groups.iter().enumerate() { + let n = counts.get(i + 1).copied().unwrap_or_default(); + if searching && n == 0 { + continue; } - if menu_item(ui, "Delete Group", true, false) { - act = Some(Act::Run("library.deleteGroup", json!({ "group": g.name }))); - ui.close(); + let top = ui.cursor().top(); + // Open unless closed; open while searching, to show what matched. + let key = format!("library-group-closed:{lib_id}:{}", g.name); + let closed: bool = super::pstate(ui.ctx(), &key); + let open = searching || !closed; + // The header also drags the group to another place in the list. + let header = widgets::section_toggle_name(ui, &g.name, open).interact(Sense::drag()); + widgets::drag_source(ui, &header, || PanelDrag::LibraryGroup { library: lib_id.clone(), group: g.name.clone() }); + ui.painter().text( + header.rect.right_center() - vec2(6.0, 0.0), + egui::Align2::RIGHT_CENTER, + n.to_string(), + egui::FontId::proportional(11.5), + t.text_dim, + ); + if header.clicked() && !searching { + super::set_pstate(ui.ctx(), &key, !closed); } - }); - if !open { - continue; - } - ui.add_space(2.0); - if n == 0 { - ui.label(egui::RichText::new(tl!("Right-click an item to move it into this group.")).size(11.5).color(t.text_dim)); - ui.add_space(6.0); - } else { - items.show(ui, &mut act, |kind, key, name| items.group_of(kind, key) == Some(i) && shown(name)); + header.context_menu(|ui| { + if menu_item(ui, "Rename Group…", true, false) { + act = Some(Act::RenameGroup(g.name.clone())); + ui.close(); + } + if menu_item(ui, "Delete Group", true, false) { + act = Some(Act::Run("library.deleteGroup", json!({ "group": g.name }))); + ui.close(); + } + }); + if open { + ui.add_space(2.0); + if n == 0 { + ui.label( + egui::RichText::new(tl!("Drag an item here, or right-click one, to put it in this group.")) + .size(11.5) + .color(t.text_dim), + ); + ui.add_space(6.0); + } else { + items.show(ui, &mut act, &mut over_item, |kind, key, name| items.group_of(kind, key) == Some(i) && shown(name)); + } + } + sections.push((Some(i), span(ui, top))); } + }); + if items.grab.is_some() { + scroll_at_edges(ui); } }); - ui.min_rect() + out.inner_rect }); - // Art dragged off the canvas, or a swatch, dropped on the list is added to the library. + if let Some(grab) = &items.grab + && !over_item + { + section_drop(ui, &items, grab, §ions, list_rect, &mut act); + } + // Art dragged off the canvas, or a swatch (not one of this library's colours), dropped on the + // list is added to the library. let zone = ui.interact(list_rect, ui.id().with("libraries-drop"), Sense::hover()); if let Some(ids) = widgets::art_drop(ui, &zone) { act = Some(Act::Run("library.add", json!({ "kind": "graphic", "ids": ids }))); } if let Some(drag) = zone.dnd_hover_payload::() - && let PanelDrag::Paint { paint: Paint::Solid { color, .. }, .. } = &*drag + && let PanelDrag::Paint { paint: Paint::Solid { color, .. }, library, .. } = &*drag + && library.as_ref().is_none_or(|(l, _)| *l != lib_id) { ui.painter().rect_stroke(zone.rect, 0.0, Stroke::new(1.5, t.accent), StrokeKind::Inside); let color = *color; @@ -207,6 +279,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { act = Some(Act::Run("library.add", json!({ "kind": "fillColor", "color": color }))); } } + let bar_top = ui.cursor().top(); widgets::bottom_bar(ui, |ui| { if widgets::icon_button(ui, "group", tl!("New Group"), false, 24.0).clicked() { act = Some(Act::NewGroup(json!([]))); @@ -227,6 +300,10 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { } }); }); + let bar_h = ui.min_rect().bottom() - bar_top; + if bar_h.is_finite() && bar_h > 0.0 { + ui.data_mut(|d| d.insert_temp(bar_id, bar_h)); + } match act { Some(Act::Run(cmd, mut p)) => { if cmd != "library.setCurrent" { @@ -248,8 +325,9 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { impl Items<'_> { /// The colours, character and paragraph styles and graphics `keep` keeps (given the kind, key - /// and name of each), each kind under its heading. - fn show(&self, ui: &mut Ui, act: &mut Option, keep: impl Fn(ItemKind, &str, &str) -> bool) { + /// and name of each), each kind under its heading. Each drags to another place in the list; + /// `over` is set while one of them takes the drag in progress. + fn show(&self, ui: &mut Ui, act: &mut Option, over: &mut bool, keep: impl Fn(ItemKind, &str, &str) -> bool) { let t = Tokens::get(ui.ctx()); let lib = self.lib; let colors: Vec<_> = lib.colors.iter().filter(|c| keep(ItemKind::Color, &c.name, &c.name)).collect(); @@ -262,7 +340,13 @@ impl Items<'_> { let paint = Paint::solid(c.color); widgets::swatch_tile(ui, r, &paint, false, false); ui.painter().rect_stroke(r, 0.0, Stroke::new(1.0, t.border), StrokeKind::Inside); - widgets::drag_source(ui, &resp, || PanelDrag::Paint { paint: paint.clone(), params: json!({ "color": c.color }), rows: None }); + widgets::drag_source(ui, &resp, || PanelDrag::Paint { + paint: paint.clone(), + params: json!({ "color": c.color }), + rows: None, + library: Some((self.library.to_string(), c.name.clone())), + }); + self.drop_next_to(ui, &resp, ItemKind::Color, &c.name, false, act, over); let resp = resp.on_hover_text(&c.name); if resp.clicked() { let to = if self.fill_active { "fill" } else { "stroke" }; @@ -282,13 +366,15 @@ impl Items<'_> { } heading(ui, title); for st in list { - let (r, resp) = ui.allocate_exact_size(vec2(ui.available_width(), 22.0), Sense::click()); + let (r, resp) = ui.allocate_exact_size(vec2(ui.available_width(), 22.0), Sense::click_and_drag()); if resp.hovered() { ui.painter().rect_filled(r, 0.0, t.hover); } let icon = if kind == ItemKind::ParaStyle { "pilcrow" } else { "type" }; icons::paint(ui, icon, Rect::from_center_size(r.left_center() + vec2(10.0, 0.0), vec2(14.0, 14.0)), t.icon); ui.painter().text(r.left_center() + vec2(24.0, 0.0), egui::Align2::LEFT_CENTER, &st.name, egui::FontId::proportional(12.5), t.text); + widgets::drag_source(ui, &resp, || PanelDrag::LibraryItem { library: self.library.to_string(), kind, item: st.name.clone() }); + self.drop_next_to(ui, &resp, kind, &st.name, true, act, over); let resp = resp.on_hover_text(tl!("Click to apply to the selected type")); if resp.clicked() { *act = Some(Act::Run("library.use", json!({ "kind": kind_param(kind), "item": st.name }))); @@ -308,6 +394,7 @@ impl Items<'_> { ui.painter().rect_stroke(r, 0.0, Stroke::new(1.0, if resp.hovered() { t.accent } else { t.border }), StrokeKind::Inside); // Dragged onto the canvas, a copy is placed where it is dropped. widgets::drag_source(ui, &resp, || PanelDrag::LibraryGraphic { library: self.library.to_string(), item: g.id.clone() }); + self.drop_next_to(ui, &resp, ItemKind::Graphic, &g.id, false, act, over); let resp = resp.on_hover_text(&g.name); let place = || Act::Run("library.use", json!({ "kind": "graphic", "item": g.id })); if resp.double_clicked() { @@ -326,6 +413,41 @@ impl Items<'_> { } } + /// An item of `kind` dragged over item `key`'s cell `resp` lands just before or after it, by + /// the pointer's half (`row`: rows stack downwards; else chips and tiles run across), in its + /// group: an accent line shows where, and `over` is set. Dropped on itself it stays. + #[allow(clippy::too_many_arguments)] + fn drop_next_to(&self, ui: &Ui, resp: &egui::Response, kind: ItemKind, key: &str, row: bool, act: &mut Option, over: &mut bool) { + let Some(Grab::Item(grabbed_kind, moving)) = &self.grab else { return }; + if *grabbed_kind != kind || resp.dnd_hover_payload::().is_none() { + return; + } + *over = true; + if moving == key { + resp.dnd_release_payload::(); + return; + } + let r = resp.rect; + let at = ui.input(|i| i.pointer.interact_pos()).unwrap_or(r.center()); + let after = if row { at.y > r.center().y } else { at.x > r.center().x }; + let accent = Stroke::new(2.0, Tokens::get(ui.ctx()).accent); + if row { + let y = if after { r.bottom() } else { r.top() }; + ui.painter().line_segment([egui::pos2(r.left(), y), egui::pos2(r.right(), y)], accent); + } else { + let x = if after { r.right() + 2.0 } else { r.left() - 2.0 }; + ui.painter().line_segment([egui::pos2(x, r.top()), egui::pos2(x, r.bottom())], accent); + } + if resp.dnd_release_payload::().is_some() { + let mut p = json!({ "kind": kind_param(kind), "item": moving }); + p[if after { "after" } else { "before" }] = json!(key); + if let Some(g) = self.group_of(kind, key).and_then(|i| self.lib.groups.get(i)) { + p["group"] = json!(g.name); + } + *act = Some(Act::Run("library.moveItem", p)); + } + } + /// An item's context menu entries: Move to Group (No Group, the library's groups, New /// Group…) and Delete. fn item_menu(&self, ui: &mut Ui, act: &mut Option, kind: ItemKind, key: &str) { @@ -371,6 +493,82 @@ fn kind_param(kind: ItemKind) -> &'static str { } } +/// Where an item dragged out of its group can be dropped when no ungrouped item is showing. +fn no_group_strip(ui: &mut Ui) { + let t = Tokens::get(ui.ctx()); + let (r, _) = ui.allocate_exact_size(vec2(ui.available_width(), 28.0), Sense::hover()); + ui.painter().rect_stroke(r, 3.0, Stroke::new(1.0, t.border), StrokeKind::Inside); + ui.painter().text( + r.center(), + egui::Align2::CENTER_CENTER, + tl!("Drop here to take it out of its group"), + egui::FontId::proportional(11.5), + t.text_dim, + ); + ui.add_space(6.0); +} + +/// While something is dragged near the top or bottom edge of the scrolled list (`ui`: its +/// contents), scroll that way, faster nearer the edge. +fn scroll_at_edges(ui: &mut Ui) { + let Some(pos) = ui.ctx().pointer_hover_pos() else { return }; + let view = ui.clip_rect(); + if !view.x_range().contains(pos.x) { + return; + } + let speed = |depth: f32| if (0.0..EDGE).contains(&depth) { 2.0 + 10.0 * (1.0 - depth / EDGE) } else { 0.0 }; + let delta = speed(pos.y - view.top()) - speed(view.bottom() - pos.y); + if delta != 0.0 { + ui.scroll_with_delta(vec2(0.0, delta)); + ui.ctx().request_repaint(); + } +} + +/// The drag in progress held over a section of the list (`sections`, seen through `clip`), away +/// from the items that take one: an item goes into that section's group (the ungrouped section: +/// out of its group), the section outlined; a group goes before or after that group, by the +/// pointer's half, an accent line showing where. +fn section_drop(ui: &Ui, items: &Items, grab: &Grab, sections: &[(Option, Rect)], clip: Rect, act: &mut Option) { + // By the pointer, not by hover zones: the headers and items over the sections take the hover. + let Some(pos) = ui.ctx().pointer_latest_pos() else { return }; + let released = ui.input(|i| i.pointer.any_released()); + let take = || egui::DragAndDrop::take_payload::(ui.ctx()).is_some(); + let t = Tokens::get(ui.ctx()); + let Some((group, rect)) = sections.iter().find(|(_, rect)| rect.intersect(clip).contains(pos)) else { return }; + let r = rect.intersect(clip); + let name = |i: &usize| items.lib.groups.get(*i).map(|g| g.name.clone()); + match grab { + Grab::Item(kind, item) => { + if items.group_of(*kind, item) == *group { + return; + } + ui.painter().rect_stroke(r, 3.0, Stroke::new(1.5, t.accent), StrokeKind::Inside); + if released && take() { + let mut p = json!({ "kind": kind_param(*kind), "item": item }); + if let Some(g) = group.as_ref().and_then(name) { + p["group"] = json!(g); + } + *act = Some(Act::Run("library.moveItem", p)); + } + } + Grab::Group(moving) => { + // Groups only go among the groups; dropped on itself, one stays. + let (Some(to), Some(from)) = (*group, items.lib.groups.iter().position(|g| g.name == *moving)) else { return }; + if from == to { + return; + } + let after = pos.y > rect.center().y; + let y = if after { rect.bottom() } else { rect.top() }; + ui.painter().line_segment([egui::pos2(r.left(), y), egui::pos2(r.right(), y)], Stroke::new(2.0, t.accent)); + if released && take() { + let to = to + usize::from(after); + let to = if from < to { to - 1 } else { to }; + *act = Some(Act::Run("library.moveGroup", json!({ "group": moving, "to": to }))); + } + } + } +} + /// A section heading in the list. fn heading(ui: &mut Ui, text: &str) { let t = Tokens::get(ui.ctx()); @@ -584,6 +782,131 @@ mod tests { assert!(none.iter().any(|t| t == "No results"), "{none:?}"); } + /// A library imported from `styles` (character style names) and `groups` ([(name, its styles)]) + /// → its id. + fn styled(app: &mut VectorcraftApp, styles: &[&str], groups: &[(&str, &[&str])]) -> String { + let styles: Vec = styles.iter().enumerate().map(|(i, n)| json!({"name": n, "attrs": {"size": 8 + i}})).collect(); + let groups: Vec = groups + .iter() + .map(|(g, items)| json!({"name": g, "items": items.iter().map(|i| json!({"kind": "charStyle", "item": i})).collect::>()})) + .collect(); + let data = json!({"name": "Order", "charStyles": styles, "groups": groups}).to_string(); + app.run("library.import", json!({"data": data})).unwrap()["id"].as_str().unwrap().to_string() + } + + /// One frame of the panel in a 260 × `tall` window with `events` → where each text it shows is + /// (its rect), and the panel's height. + fn frame_at(app: &mut VectorcraftApp, ctx: &egui::Context, tall: f32, events: Vec) -> (Vec<(String, Rect)>, f32) { + let raw = egui::RawInput { screen_rect: Some(Rect::from_min_size(egui::pos2(0.0, 0.0), vec2(260.0, tall))), events, ..Default::default() }; + let mut height = 0.0; + let mut out = ctx.run_ui(raw, |ui| { + show(app, ui); + height = ui.min_rect().height(); + }); + out.textures_delta.clear(); + let mut texts = vec![]; + for s in out.shapes { + if let egui::epaint::Shape::Text(t) = s.shape { + texts.push((t.galley.text().to_string(), t.galley.rect.translate(t.pos.to_vec2()))); + } + } + (texts, height) + } + + /// Where text `name` is in `texts`. + fn at(texts: &[(String, Rect)], name: &str) -> Rect { + texts.iter().find(|(t, _)| t == name).map(|(_, r)| *r).unwrap_or_else(|| panic!("{name} isn't shown: {texts:?}")) + } + + /// `drag` released at `pos`. + fn drop_at(app: &mut VectorcraftApp, ctx: &egui::Context, drag: PanelDrag, pos: egui::Pos2) { + egui::DragAndDrop::set_payload(ctx, drag); + let up = egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed: false, modifiers: Default::default() }; + frame_at(app, ctx, 600.0, vec![egui::Event::PointerMoved(pos), up]); + frame_at(app, ctx, 600.0, vec![]); + } + + fn style_order(app: &mut VectorcraftApp) -> Vec { + let got = app.run("library.get", json!({})).unwrap(); + got["charStyles"].as_array().unwrap().iter().map(|s| s["name"].as_str().unwrap().to_string()).collect() + } + + fn group_names(app: &mut VectorcraftApp) -> Vec<(String, Vec)> { + let got = app.run("library.get", json!({})).unwrap(); + let items = |g: &Value| g["items"].as_array().unwrap().iter().map(|i| i["item"].as_str().unwrap().to_string()).collect(); + got["groups"].as_array().unwrap().iter().map(|g| (g["name"].as_str().unwrap().to_string(), items(g))).collect() + } + + /// Dragging in the list: a style dropped on the top or bottom half of another lands before or + /// after it, in its group; one dropped on a group's header goes into that group; a group's + /// header dropped on another group moves the group; and with every item grouped, a strip + /// takes an item out of its group. + #[test] + fn items_and_groups_move_by_dragging() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width": 200, "height": 200})).unwrap(); + let lib = styled(&mut app, &["A", "B", "C", "D"], &[("Warm", &["D"]), ("Cool", &[])]); + let ctx = egui::Context::default(); + crate::theme::install_fonts(&ctx); + let (texts, _) = frame_at(&mut app, &ctx, 600.0, vec![]); + let item = |name: &str| PanelDrag::LibraryItem { library: lib.clone(), kind: ItemKind::CharStyle, item: name.into() }; + // C onto A's top half: first. + let a = at(&texts, "A"); + drop_at(&mut app, &ctx, item("C"), egui::pos2(a.center().x, a.top() - 4.0)); + assert_eq!(style_order(&mut app), ["C", "A", "B", "D"]); + // A onto D's bottom half (in Warm): after D, in Warm. + let (texts, _) = frame_at(&mut app, &ctx, 600.0, vec![]); + let d = at(&texts, "D"); + drop_at(&mut app, &ctx, item("A"), egui::pos2(d.center().x, d.bottom() + 4.0)); + assert_eq!(style_order(&mut app), ["C", "B", "D", "A"]); + assert_eq!(group_names(&mut app), [("Warm".into(), vec!["D".to_string(), "A".into()]), ("Cool".into(), vec![])]); + // B onto Cool's header: into Cool, the order kept. + let (texts, _) = frame_at(&mut app, &ctx, 600.0, vec![]); + drop_at(&mut app, &ctx, item("B"), at(&texts, "Cool").center()); + assert_eq!(style_order(&mut app), ["C", "B", "D", "A"]); + assert_eq!(group_names(&mut app)[1], ("Cool".into(), vec!["B".to_string()])); + // Cool's header onto Warm's header (its top half): Cool first. + let (texts, _) = frame_at(&mut app, &ctx, 600.0, vec![]); + let warm = at(&texts, "Warm"); + drop_at(&mut app, &ctx, PanelDrag::LibraryGroup { library: lib.clone(), group: "Cool".into() }, egui::pos2(warm.center().x, warm.top())); + assert_eq!(group_names(&mut app).iter().map(|(g, _)| g.as_str()).collect::>(), ["Cool", "Warm"]); + // Every item grouped: while one is dragged a strip takes it out of its group. + app.run("library.moveItem", json!({"kind": "charStyle", "item": "C", "group": "Warm"})).unwrap(); + egui::DragAndDrop::set_payload(&ctx, item("C")); + let (texts, _) = frame_at(&mut app, &ctx, 600.0, vec![]); + let strip = at(&texts, "Drop here to take it out of its group"); + drop_at(&mut app, &ctx, item("C"), strip.center()); + assert!(group_names(&mut app).iter().all(|(_, items)| !items.contains(&"C".to_string())), "{:?}", group_names(&mut app)); + // Without a drag there is no strip. + let (texts, _) = frame_at(&mut app, &ctx, 600.0, vec![]); + assert!(!texts.iter().any(|(t, _)| t == "Drop here to take it out of its group")); + } + + /// A long list scrolls inside the panel (its bottom bar stays in the window), and a drag held + /// near the list's bottom edge scrolls it. + #[test] + fn a_long_list_scrolls_also_while_dragging() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width": 200, "height": 200})).unwrap(); + let names: Vec = (0..60).map(|i| format!("Style {i}")).collect(); + let lib = styled(&mut app, &names.iter().map(String::as_str).collect::>(), &[]); + let ctx = egui::Context::default(); + crate::theme::install_fonts(&ctx); + let (texts, height) = frame_at(&mut app, &ctx, 400.0, vec![]); + assert!(height <= 400.0, "the panel fits the window: {height}"); + let first = at(&texts, "Style 0"); + // The list's bottom edge: the last text drawn before the window's bottom bar area. + let bottom = texts.iter().filter(|(t, _)| t.starts_with("Style ")).map(|(_, r)| r.bottom()).fold(0.0, f32::max).min(400.0 - BAR - 4.0); + egui::DragAndDrop::set_payload(&ctx, PanelDrag::LibraryItem { library: lib, kind: ItemKind::CharStyle, item: "Style 0".into() }); + let hold = egui::pos2(first.center().x, bottom - 4.0); + for _ in 0..5 { + frame_at(&mut app, &ctx, 400.0, vec![egui::Event::PointerMoved(hold)]); + } + let (texts, _) = frame_at(&mut app, &ctx, 400.0, vec![egui::Event::PointerMoved(hold)]); + let moved = texts.iter().find(|(t, _)| t == "Style 0").map_or(f32::NEG_INFINITY, |(_, r)| r.top()); + assert!(moved < first.top(), "the list scrolled down: {} → {moved}", first.top()); + } + /// New Group… and Rename Group… ask for a name and act on the library and group they came /// from; the dialog shows only the name. #[test] diff --git a/crates/ui-egui/src/panels/swatches.rs b/crates/ui-egui/src/panels/swatches.rs index 33ac5b4b1..56c48d23c 100644 --- a/crates/ui-egui/src/panels/swatches.rs +++ b/crates/ui-egui/src/panels/swatches.rs @@ -410,7 +410,7 @@ fn drag_of(e: &Entry, names: Vec) -> PanelDrag { Entry::Folder(_) => Paint::None, }; let rows = SwatchRows { grabbed: e.name().to_string(), names, groups: e.is_folder() }; - PanelDrag::Paint { paint, params: swatch_params(e).unwrap_or_default(), rows: Some(rows) } + PanelDrag::Paint { paint, params: swatch_params(e).unwrap_or_default(), rows: Some(rows), library: None } } /// What the tiles or rows saw this frame. @@ -614,6 +614,7 @@ pub(crate) fn drag_preview(app: &VectorcraftApp, ctx: &egui::Context) { }, PanelDrag::Brush { def, .. } => return pointer_chip(ctx, |ui, r| super::brushes::chip(ui, r, def)), PanelDrag::LibraryGraphic { library, item } => return pointer_chip(ctx, |ui, r| super::libraries::chip(app, ui, r, library, item)), + PanelDrag::LibraryItem { .. } | PanelDrag::LibraryGroup { .. } => return, }; pointer_chip(ctx, |ui, r| { if registration { diff --git a/crates/ui-egui/src/widgets.rs b/crates/ui-egui/src/widgets.rs index 6c469f0da..e154a7876 100644 --- a/crates/ui-egui/src/widgets.rs +++ b/crates/ui-egui/src/widgets.rs @@ -2034,8 +2034,9 @@ pub enum PanelDrag { /// is dropped on takes `params` (`{swatch}`, `{color}`, `{gradient}`…; null for a colour group, /// which paints nothing) through the active proxy's `paint.setFill`/`paint.setStroke`; the /// Gradient panel's ramp takes its colour as a stop; the Swatches panel moves `rows`, or makes - /// a swatch of a paint dragged from elsewhere. - Paint { paint: Paint, params: serde_json::Value, rows: Option }, + /// a swatch of a paint dragged from elsewhere. `library`: the Libraries panel colour it is + /// (library id, colour name), which that panel moves to where it is dropped. + Paint { paint: Paint, params: serde_json::Value, rows: Option, library: Option<(String, String)> }, /// The Appearance panel's thumbnail: art it is dropped on takes object `0`'s appearance /// (`appearance.copyFrom`); the Graphic Styles panel makes a style of it. Appearance(vectorcraft_doc::NodeId), @@ -2054,8 +2055,13 @@ pub enum PanelDrag { /// dropped on takes it (`brush.apply`). Brush { name: String, def: serde_json::Value }, /// A Libraries panel graphic (`item`: its id in `library`): the canvas places a copy centred - /// where it is dropped (`library.use`). + /// where it is dropped (`library.use`); the panel moves it to where it is dropped. LibraryGraphic { library: String, item: String }, + /// A Libraries panel text style (`item`: its name in `library`), which the panel moves to where + /// it is dropped. + LibraryItem { library: String, kind: vectorcraft_engine::cmd::library::ItemKind, item: String }, + /// A Libraries panel group, by its header, which the panel moves to where it is dropped. + LibraryGroup { library: String, group: String }, } /// A panel list `zone` that takes art dragged off the canvas: outlined while art is held over it; @@ -2088,7 +2094,7 @@ impl PanelDrag { g.geom = None; g.freeform = None; } - Self::Paint { params: crate::panels::paint_params(&paint), paint, rows: None } + Self::Paint { params: crate::panels::paint_params(&paint), paint, rows: None, library: None } } /// The dragged colour (a solid paint's), which the Gradient panel's ramp takes. pub fn color(&self) -> Option { diff --git a/docs/mcp.md b/docs/mcp.md index 1d5a9fcb1..cd2711e11 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -1564,9 +1564,13 @@ Items can be put in user-named groups, which the panel shows as collapsible sect search field filters every item by name; `library.get` lists them all to filter). `library.createGroup {library?, name?, items?: [{kind, item}]}` makes one (a name another group has, ignoring case, gets a number) with those items moved into it; `library.renameGroup {library?, group, name}` renames one; `library.deleteGroup {library?, group}` -deletes one, its items staying in the library ungrouped; and `library.moveItem {library?, kind, item, group?}` moves -an item into a group (without `group`, out of its group). An item is in one group at most; `library.get` lists the -groups as `groups: [{name, items: [{kind: "color"|"charStyle"|"paraStyle"|"graphic", item}]}]` (a graphic by its id). +deletes one, its items staying in the library ungrouped; and `library.moveItem {library?, kind, item, group?, before? +| after?}` moves an item into a group (without `group`, out of its group) and, with `before` or `after` (another item +of its kind), just before or after that item in the order its kind is listed and shown in. `library.moveGroup +{library?, group, to}` moves a group to place `to` of the list (0: first; past the end: last). Dragging items and +group headers in the panel runs these. An item is in one group at most; `library.get` lists the groups as `groups: +[{name, items: [{kind: "color"|"charStyle"|"paraStyle"|"graphic", item}]}]` (a graphic by its id), and each kind's +items in the order the panel shows them. `library.export {library?, path?}` writes a library (its colours, styles, graphics with their art and thumbnails, and its groups) as a `.vclibrary` file, the JSON the library folder keeps (without `path`: `{data}`, the file's text). diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 977102523..c3ff929be 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1,6 +1,6 @@ # Parity with Adobe Illustrator -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-19 · **Change:** minor (PDF invisible text opens as type with no fill or stroke and is written back invisible, #2675; status-readout sample widths cached by language, document unit and pixels per point; status messages and readouts use stable responsive allocations sized for localized labels and ordinary coordinates; cached canvas dimensions match the renderer limit before cache-key creation, view construction and worker submission; cached canvas reprojection preserves scale during resize, rotation and DPI changes; interactive Mirror Repeat axis; bounding-box handles take priority over the axis body while the center and end handles retain their move and rotation actions; 3D lighting: one to ten colored lights, smooth colored Inflate, legacy white-light appearance preserved; surface batching and restored seam minimum; scores and estimates unchanged; Blend spine Anchor Point conversions, selection/stacking priority and Shift regression coverage (#2544); scores and hours unchanged; Artboard Options' X and Y with a reference point, #1876; Image Trace: Auto Color keeps rare but distinct colours; Image Trace: large images trace with a warning; anchor limit raised to 8,000,000; Image Trace: Raster::new returns an error for a buffer of the wrong length; M12 maximal-disc graph pruning and junction merging measured; M12 crop EDT and Guo–Hall thinning measured; M12 centre-lines baseline measured; scores and hours unchanged; installed plug-ins stay installed after a restart, #2541; Group Selection picks and moves one path of a compound path, #2441 in part; the Move dialog opens on the last move and its last checkboxes; Image Trace: a higher Paths never gives fewer segments on collapsed loops; Image Trace: the notch rule reads only its own piece; Image Trace: an edge's sub-pixel shift is the same from either side; Image Trace Flat Logo traces line art on white paper instead of a blank result; score and hours unchanged; Image Trace: a simpler contour walk with no precomputed direction table; transparency grid visibility saved in native documents, #1968; MCP cancellation, progress and busy answers during long exports, #2165, #2171, #2172; EPS editing data: legacy mesh corners, vector opacity masks and compound member paint retained; raster page comparison filters minification without relaxing limits; scores and hours unchanged; native family-name fallback from English-only typographic names, #2208; scores unchanged; batch exporting, #1935; G2: Effect › Sketch › Halftone Pattern; Photoshop filters 30/57, 27 remaining (22–38 h); G5: Symbol Options Export Type (Graphic / Movie Clip); Localization: Brazilian Portuguese complete under the tests, #1914, 71% → 74%; G6 image variables: Automation ~60% → ~62% / detail ~65% → ~68%, hours lowered; Warp Options controls verified by focused checks and original-image review; estimates unchanged; G11: Asset Export Art Optimized seam (#983 / #1037) and supersampled thumbs; Export for Screens 85% → 90%; existing-guide Escape cancellation validated 2026-10-14; scores and hours unchanged; open-path anchor Delete keeps surviving runs separate; estimates unchanged; PDF.R1: shared PDF reader with windowed content scanning and explicit incomplete-content errors; scores and estimates unchanged; PDF.W1: guarded shared PDF writer; output behavior, scores and estimates unchanged; Artboard tab-switch follow-up historically validated on `a0671777`; scores and hours unchanged; Artboard clean active-tab close/Delete follow-up validated 2026-10-15; later 2026-10-14 merges: anchor X/Y/W/H, #1824; Snap to Grid over Snap to Pixel, #1816; toolbar and All Tools, #1751; Actions panel selection, #1629 in part; redo during a drag, #1719; preferences written on OK, #1747; type in symbols after reopening, #1738; macOS save sheets, #1855, the Format menu withdrawn; stroke outlines shared, #1667; untrusted files, #1852, #1862–#1873; failed-save dialog, #1685; effects, #1864, Dim Images, #1683; duplicated rows of the 2026-10-05 table noted for the owner; scores unchanged; Image Trace: Shapes rework, stroke caps, enhanced presets, gradients over noise, measured in `image-trace-quality.md`, score unchanged; verified PngSuite downloads, #1729; synthetic control-input budgets, #1709; 2026-10-14 merges: anchor Align and Distribute, #1636; Direct Selection and Delete fixes, #1674, #1666, #1743; Escape while typing, #1686; Pen clicks, #1663; Expand on repeats, #1756; Roughen and Scribble, #1643, #1648; file and input hardening, #1776–#1783, #1786, #1807; MCP limits and envelope, #1727, #1728; Image Trace levelling and Flat Logo on scans, #1647, #1657; scores unchanged; later 2026-10-13 merges: SVG import, #1565, #1596, #1568, #1598, #1601; type on a path from PDF and `.ai`, #1585, #1586; ICC tag alignment, #1584; saves, #1620 in part; Warp Arc, #1578; Zig Zag, #1558; Pathfinder strokes, #1570; OpenType features, #1607; intersect Smart Guides, #1602 in part; a guide's position in the Transform panel, #1597 in part, a convenience with no parity claim; scores unchanged; TIFF is available in Export for Screens and Asset Export with shared format settings; scores and hours unchanged; G1/M11: Artboard deletion and history selection normalization; history regressions and full workspace CI pass; scores and hours unchanged; Character styles retain explicit Auto leading, propagate resets and preserve local leading overrides; scores and hours unchanged; Width Point Edit: Shift-double-click enables adjoining adjustment; plain double-click keeps it off; scores and hours unchanged; Units › Identify Objects By (`identifyObjectsBy`) acts on Layers, Variables and Layer Options, #394, scores unchanged; Appearance of Black › Printing / Exporting (`blackOutput`) acts on RGB raster export and Print as Bitmap, #394, scores unchanged; per-corner radius fields with a link toggle, split from #1594; control transport deadlines, #1726; Color panel: the mode picked last comes back after a restart, #1675; Image Trace: shared edges, memory, thin lines, strokes, corners, and the Shapes, Auto Grouping and Gradients options, measured in `image-trace-quality.md`, score unchanged; 2026-10-13 merges: Polish interface, #1498; compact native saves, #1492; PDF and EPS paste as vector art, #1499; import fixes for `.ai`, PDF, EPS and SVG, #1528, #1530, #1531, #1544, #1551, #1553; guide objects, #1534; Join after Average, #1533; Keyboard Shortcuts lists commands outside the menus, #1539; correction: Average and Join with a corner or smooth choice is missing; scores unchanged; late 2026-10-12 merges: type that does not fit its frame is reported, #1471; Tabs panel drags, #1458; template layers in Outline view, #1486; dialog slider rails, #1483; Direct Selection anchor precedence, #1508; print-size images, #1517; 3D on outlined type, #1481; French catalog, #1511; scores unchanged; Split Into Grid options, #1462; Transform Objects and Reflect across an angle, #1448 and #1460; Pathfinder panel on a group, #1450; unknown parameter keys refused, #1442; grayscale TIFFs with alpha, #1466 in part; Localization: Vietnamese catalog added, 63% → 71%, hours 60–100 → 55–90, full readiness unchanged at ~62%; scores and hours unchanged; Image Trace places edges to a fraction of a pixel in every mode, measured in `image-trace-quality.md`, score unchanged; 3D and Materials: Extrude & Bevel, Inflate and Rotate added, 3D ~8% → ~25% (estimated), menu stubs 25 → 22; Affinity text frames too short for their first line show it, #1423; Transform dialogs preview, #1412, and the transform tools and commands act on direct-selected anchors, #1408; `.ai` compound path paint, #1410; strokes wider than their shape, #1396; corner drags hide the bounding box, #1229; scores and hours unchanged; PDF and `.ai` import keep overprint, #1388, scores unchanged; `.ai` editing-data follow-ups: gradient stops, type of objects with an appearance, short area-type frames (#1390, #1391, #1392); dialog keyboard focus on every open (#1382, #1395); art flush with an artboard (#1375); font rescan (#1376); symbol instance redraw cache (#1386); scores and hours unchanged; grid tool options, #1377 in part, scores unchanged; `.ai` placed (linked) files read from the editing data, #1264, scores unchanged; gradient shapes import as paths, #1304; white constant alpha masks, #1286 in part; Live Corners widget click selects its corner, #1239; scores and hours unchanged; custom ruler origin, #1212; Artboard click-to-fit verified for objects, groups, visible bounds, undo and cancellation (#1227); scores and hours unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; Localization: Korean catalog added, 54% → 63%, full readiness unchanged at ~62%; desktop print transport adopts craft-print, Windows 7 fallback retained, scores unchanged; Image Trace: an image over 64 megapixels traces scaled down instead of being refused, scores unchanged; Czech render-site localization follow-up; scores and estimates unchanged; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; Path operations: Simplify preview/counts and component selection (#1272); scores unchanged; G2: Film Grain added; Photoshop effects 29/57; 28 filters remain; Platforms: RPM runtime capabilities and glibc floor (#1275); scores unchanged; Raster effects: squared-distance texture redraws (#1257); pixels unchanged; scores and hours unchanged; Drawing: Curvature point display improved (#1236, in part); scores unchanged; Type core: full-name font matching (#1258), scores and hours unchanged; Selection: no-op anchor Align preserves history and saved state (#1180 follow-up); scores unchanged; UI chrome: Eyedropper cursor and precise-setting regressions (#1269); scores unchanged; Colour: Eyedropper reads visible locked and outside-isolation sources (#1251); scores and hours unchanged; Selection: segment click/Delete preserves neighboring edges (#1278); scores unchanged; Colour: unpainted zero-width strokes regain 1 pt when painted (#1253); scores and hours unchanged; graphs: designs and markers, data, Graph Type and label options; UI chrome: native macOS save Format menu, #866, scores unchanged; compound paths inherit the backmost appearance and member fill-rule edits reach their owner, #1197; scores unchanged; Type core: Middle Eastern digit types done, Character panel menu › Digits; PDF export warns when the pixel cap lowers raster-effect resolution; Drawing tools: Live Corners round corners between curved sides, #1144; Type advanced: proportional vertical metrics (`vpal`) done, #966; Shape Builder: #893 fixed, merged regions of crossing lines no longer keep seams; scores unchanged while #937 is open; #1034: document-point character controls after object/group scaling; full readiness recomputed as the standard's additive weighted sum, ~62%; per-audience hours; essentials-user score ~75% added; ready-for-real-work re-examined: 50% → 55%, mainstream 65% added; earlier the same day: full re-measure against Illustrator 2026 30.x; merged the ROADMAP's parity estimate, honest assessment and "Shipped so far" into this file; SVG knockout groups of opaque art export without masks; Image Trace: colour presets no longer shatter noisy flat art and photos trace into smooth tones, measured in `image-trace-quality.md`, score unchanged; Split Into Grid options, #1462; Transform Objects and Reflect across an angle, #1448 and #1460; Pathfinder panel on a group, #1450; unknown parameter keys refused, #1442; grayscale TIFFs with alpha, #1466 in part; scores and hours unchanged; Image Trace places edges to a fraction of a pixel in every mode, measured in `image-trace-quality.md`, score unchanged; 3D and Materials: Extrude & Bevel, Inflate and Rotate added, 3D ~8% → ~25% (estimated), menu stubs 25 → 22; Affinity text frames too short for their first line show it, #1423; Transform dialogs preview, #1412, and the transform tools and commands act on direct-selected anchors, #1408; `.ai` compound path paint, #1410; strokes wider than their shape, #1396; corner drags hide the bounding box, #1229; scores and hours unchanged; PDF and `.ai` import keep overprint, #1388, scores unchanged; `.ai` editing-data follow-ups: gradient stops, type of objects with an appearance, short area-type frames (#1390, #1391, #1392); dialog keyboard focus on every open (#1382, #1395); art flush with an artboard (#1375); font rescan (#1376); symbol instance redraw cache (#1386); scores and hours unchanged; grid tool options, #1377 in part, scores unchanged; `.ai` placed (linked) files read from the editing data, #1264, scores unchanged; gradient shapes import as paths, #1304; white constant alpha masks, #1286 in part; Live Corners widget click selects its corner, #1239; scores and hours unchanged; custom ruler origin, #1212; Artboard click-to-fit verified for objects, groups, visible bounds, undo and cancellation (#1227); scores and hours unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; Localization: Korean catalog added, 54% → 63%, full readiness unchanged at ~62%; desktop print transport adopts craft-print, Windows 7 fallback retained, scores unchanged; Image Trace: an image over 64 megapixels traces scaled down instead of being refused, scores unchanged; Czech render-site localization follow-up; scores and estimates unchanged; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; Path operations: Simplify preview/counts and component selection (#1272); scores unchanged; G2: Film Grain added; Photoshop effects 29/57; 28 filters remain; Platforms: RPM runtime capabilities and glibc floor (#1275); scores unchanged; Raster effects: squared-distance texture redraws (#1257); pixels unchanged; scores and hours unchanged; Drawing: Curvature point display improved (#1236, in part); scores unchanged; Type core: full-name font matching (#1258), scores and hours unchanged; Selection: no-op anchor Align preserves history and saved state (#1180 follow-up); scores unchanged; UI chrome: Eyedropper cursor and precise-setting regressions (#1269); scores unchanged; Colour: Eyedropper reads visible locked and outside-isolation sources (#1251); scores and hours unchanged; Selection: segment click/Delete preserves neighboring edges (#1278); scores unchanged; Colour: unpainted zero-width strokes regain 1 pt when painted (#1253); scores and hours unchanged; graphs: designs and markers, data, Graph Type and label options; UI chrome: native macOS save Format menu, #866, scores unchanged; compound paths inherit the backmost appearance and member fill-rule edits reach their owner, #1197; scores unchanged; Type core: Middle Eastern digit types done, Character panel menu › Digits; PDF export warns when the pixel cap lowers raster-effect resolution; Drawing tools: Live Corners round corners between curved sides, #1144; Type advanced: proportional vertical metrics (`vpal`) done, #966; Shape Builder: #893 fixed, merged regions of crossing lines no longer keep seams; scores unchanged while #937 is open; #1034: document-point character controls after object/group scaling; full readiness recomputed as the standard's additive weighted sum, ~62%; per-audience hours; essentials-user score ~75% added; ready-for-real-work re-examined: 50% → 55%, mainstream 65% added; earlier the same day: full re-measure against Illustrator 2026 30.x; merged the ROADMAP's parity estimate, honest assessment and "Shipped so far" into this file; SVG knockout groups of opaque art export without masks; MCP `add_text` exposes the existing `vertical` option for point, area and path type; horizontal remains the default; scores and hours unchanged; Vertical fullwidth question and exclamation marks keep their nominal glyph upright when the font has no vertical alternate; scores and hours unchanged.; Curvature curve geometry, #1236 in part; Width-point finite-arithmetic follow-up validated; scores and hours unchanged; Image Trace adjustment discovery; File interop: legacy `.ai` of version 7 saved with type as text, hidden objects kept and an artboard picker, #868, scores unchanged; Revolve: automatic smooth shading and default-on Cap with continuous rims; PDF.R1: shared PDF reader with windowed content scanning and explicit incomplete-content errors; scores and estimates unchanged; PDF.W1: guarded shared PDF writer; output behavior, scores and estimates unchanged; later 2026-10-14 merges: anchor X/Y/W/H, #1824; Snap to Grid over Snap to Pixel, #1816; toolbar and All Tools, #1751; Actions panel selection, #1629 in part; redo during a drag, #1719; preferences written on OK, #1747; type in symbols after reopening, #1738; macOS save sheets, #1855, the Format menu withdrawn; stroke outlines shared, #1667; untrusted files, #1852, #1862–#1873; failed-save dialog, #1685; effects, #1864, Dim Images, #1683; duplicated rows of the 2026-10-05 table noted for the owner; scores unchanged; Image Trace: Shapes rework, stroke caps, enhanced presets, gradients over noise, measured in `image-trace-quality.md`, score unchanged; verified PngSuite downloads, #1729; synthetic control-input budgets, #1709; 2026-10-14 merges: anchor Align and Distribute, #1636; Direct Selection and Delete fixes, #1674, #1666, #1743; Escape while typing, #1686; Pen clicks, #1663; Expand on repeats, #1756; Roughen and Scribble, #1643, #1648; file and input hardening, #1776–#1783, #1786, #1807; MCP limits and envelope, #1727, #1728; Image Trace levelling and Flat Logo on scans, #1647, #1657; scores unchanged; later 2026-10-13 merges: SVG import, #1565, #1596, #1568, #1598, #1601; type on a path from PDF and `.ai`, #1585, #1586; ICC tag alignment, #1584; saves, #1620 in part; Warp Arc, #1578; Zig Zag, #1558; Pathfinder strokes, #1570; OpenType features, #1607; intersect Smart Guides, #1602 in part; a guide's position in the Transform panel, #1597 in part, a convenience with no parity claim; scores unchanged; TIFF is available in Export for Screens and Asset Export with shared format settings; scores and hours unchanged; G1/M11: Artboard deletion and history selection normalization; history regressions and full workspace CI pass; scores and hours unchanged; Character styles retain explicit Auto leading, propagate resets and preserve local leading overrides; scores and hours unchanged; Width Point Edit: Shift-double-click enables adjoining adjustment; plain double-click keeps it off; scores and hours unchanged; Units › Identify Objects By (`identifyObjectsBy`) acts on Layers, Variables and Layer Options, #394, scores unchanged; Appearance of Black › Printing / Exporting (`blackOutput`) acts on RGB raster export and Print as Bitmap, #394, scores unchanged; per-corner radius fields with a link toggle, split from #1594; control transport deadlines, #1726; Color panel: the mode picked last comes back after a restart, #1675; Image Trace: shared edges, memory, thin lines, strokes, corners, and the Shapes, Auto Grouping and Gradients options, measured in `image-trace-quality.md`, score unchanged; 2026-10-13 merges: Polish interface, #1498; compact native saves, #1492; PDF and EPS paste as vector art, #1499; import fixes for `.ai`, PDF, EPS and SVG, #1528, #1530, #1531, #1544, #1551, #1553; guide objects, #1534; Join after Average, #1533; Keyboard Shortcuts lists commands outside the menus, #1539; correction: Average and Join with a corner or smooth choice is missing; scores unchanged; late 2026-10-12 merges: type that does not fit its frame is reported, #1471; Tabs panel drags, #1458; template layers in Outline view, #1486; dialog slider rails, #1483; Direct Selection anchor precedence, #1508; print-size images, #1517; 3D on outlined type, #1481; French catalog, #1511; scores unchanged; Split Into Grid options, #1462; Transform Objects and Reflect across an angle, #1448 and #1460; Pathfinder panel on a group, #1450; unknown parameter keys refused, #1442; grayscale TIFFs with alpha, #1466 in part; Localization: Vietnamese catalog added, 63% → 71%, hours 60–100 → 55–90, full readiness unchanged at ~62%; scores and hours unchanged; Image Trace places edges to a fraction of a pixel in every mode, measured in `image-trace-quality.md`, score unchanged; 3D and Materials: Extrude & Bevel, Inflate and Rotate added, 3D ~8% → ~25% (estimated), menu stubs 25 → 22; Affinity text frames too short for their first line show it, #1423; Transform dialogs preview, #1412, and the transform tools and commands act on direct-selected anchors, #1408; `.ai` compound path paint, #1410; strokes wider than their shape, #1396; corner drags hide the bounding box, #1229; scores and hours unchanged; PDF and `.ai` import keep overprint, #1388, scores unchanged; `.ai` editing-data follow-ups: gradient stops, type of objects with an appearance, short area-type frames (#1390, #1391, #1392); dialog keyboard focus on every open (#1382, #1395); art flush with an artboard (#1375); font rescan (#1376); symbol instance redraw cache (#1386); scores and hours unchanged; grid tool options, #1377 in part, scores unchanged; `.ai` placed (linked) files read from the editing data, #1264, scores unchanged; gradient shapes import as paths, #1304; white constant alpha masks, #1286 in part; Live Corners widget click selects its corner, #1239; scores and hours unchanged; custom ruler origin, #1212; Artboard click-to-fit verified for objects, groups, visible bounds, undo and cancellation (#1227); scores and hours unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; Localization: Korean catalog added, 54% → 63%, full readiness unchanged at ~62%; desktop print transport adopts craft-print, Windows 7 fallback retained, scores unchanged; Image Trace: an image over 64 megapixels traces scaled down instead of being refused, scores unchanged; Czech render-site localization follow-up; scores and estimates unchanged; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; Path operations: Simplify preview/counts and component selection (#1272); scores unchanged; G2: Film Grain added; Photoshop effects 29/57; 28 filters remain; Platforms: RPM runtime capabilities and glibc floor (#1275); scores unchanged; Raster effects: squared-distance texture redraws (#1257); pixels unchanged; scores and hours unchanged; Drawing: Curvature point display improved (#1236, in part); scores unchanged; Type core: full-name font matching (#1258), scores and hours unchanged; Selection: no-op anchor Align preserves history and saved state (#1180 follow-up); scores unchanged; UI chrome: Eyedropper cursor and precise-setting regressions (#1269); scores unchanged; Colour: Eyedropper reads visible locked and outside-isolation sources (#1251); scores and hours unchanged; Selection: segment click/Delete preserves neighboring edges (#1278); scores unchanged; Colour: unpainted zero-width strokes regain 1 pt when painted (#1253); scores and hours unchanged; graphs: designs and markers, data, Graph Type and label options; UI chrome: native macOS save Format menu, #866, scores unchanged; compound paths inherit the backmost appearance and member fill-rule edits reach their owner, #1197; scores unchanged; Type core: Middle Eastern digit types done, Character panel menu › Digits; PDF export warns when the pixel cap lowers raster-effect resolution; Drawing tools: Live Corners round corners between curved sides, #1144; Type advanced: proportional vertical metrics (`vpal`) done, #966; Shape Builder: #893 fixed, merged regions of crossing lines no longer keep seams; scores unchanged while #937 is open; #1034: document-point character controls after object/group scaling; full readiness recomputed as the standard's additive weighted sum, ~62%; per-audience hours; essentials-user score ~75% added; ready-for-real-work re-examined: 50% → 55%, mainstream 65% added; earlier the same day: full re-measure against Illustrator 2026 30.x; merged the ROADMAP's parity estimate, honest assessment and "Shipped so far" into this file; SVG knockout groups of opaque art export without masks; Image Trace: colour presets no longer shatter noisy flat art and photos trace into smooth tones, measured in `image-trace-quality.md`, score unchanged; Split Into Grid options, #1462; Transform Objects and Reflect across an angle, #1448 and #1460; Pathfinder panel on a group, #1450; unknown parameter keys refused, #1442; grayscale TIFFs with alpha, #1466 in part; scores and hours unchanged; Image Trace places edges to a fraction of a pixel in every mode, measured in `image-trace-quality.md`, score unchanged; 3D and Materials: Extrude & Bevel, Inflate and Rotate added, 3D ~8% → ~25% (estimated), menu stubs 25 → 22; Affinity text frames too short for their first line show it, #1423; Transform dialogs preview, #1412, and the transform tools and commands act on direct-selected anchors, #1408; `.ai` compound path paint, #1410; strokes wider than their shape, #1396; corner drags hide the bounding box, #1229; scores and hours unchanged; PDF and `.ai` import keep overprint, #1388, scores unchanged; `.ai` editing-data follow-ups: gradient stops, type of objects with an appearance, short area-type frames (#1390, #1391, #1392); dialog keyboard focus on every open (#1382, #1395); art flush with an artboard (#1375); font rescan (#1376); symbol instance redraw cache (#1386); scores and hours unchanged; grid tool options, #1377 in part, scores unchanged; `.ai` placed (linked) files read from the editing data, #1264, scores unchanged; gradient shapes import as paths, #1304; white constant alpha masks, #1286 in part; Live Corners widget click selects its corner, #1239; scores and hours unchanged; custom ruler origin, #1212; Artboard click-to-fit verified for objects, groups, visible bounds, undo and cancellation (#1227); scores and hours unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; Localization: Korean catalog added, 54% → 63%, full readiness unchanged at ~62%; desktop print transport adopts craft-print, Windows 7 fallback retained, scores unchanged; Image Trace: an image over 64 megapixels traces scaled down instead of being refused, scores unchanged; Czech render-site localization follow-up; scores and estimates unchanged; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; Path operations: Simplify preview/counts and component selection (#1272); scores unchanged; G2: Film Grain added; Photoshop effects 29/57; 28 filters remain; Platforms: RPM runtime capabilities and glibc floor (#1275); scores unchanged; Raster effects: squared-distance texture redraws (#1257); pixels unchanged; scores and hours unchanged; Drawing: Curvature point display improved (#1236, in part); scores unchanged; Type core: full-name font matching (#1258), scores and hours unchanged; Selection: no-op anchor Align preserves history and saved state (#1180 follow-up); scores unchanged; UI chrome: Eyedropper cursor and precise-setting regressions (#1269); scores unchanged; Colour: Eyedropper reads visible locked and outside-isolation sources (#1251); scores and hours unchanged; Selection: segment click/Delete preserves neighboring edges (#1278); scores unchanged; Colour: unpainted zero-width strokes regain 1 pt when painted (#1253); scores and hours unchanged; graphs: designs and markers, data, Graph Type and label options; UI chrome: native macOS save Format menu, #866, scores unchanged; compound paths inherit the backmost appearance and member fill-rule edits reach their owner, #1197; scores unchanged; Type core: Middle Eastern digit types done, Character panel menu › Digits; PDF export warns when the pixel cap lowers raster-effect resolution; Drawing tools: Live Corners round corners between curved sides, #1144; Type advanced: proportional vertical metrics (`vpal`) done, #966; Shape Builder: #893 fixed, merged regions of crossing lines no longer keep seams; scores unchanged while #937 is open; #1034: document-point character controls after object/group scaling; full readiness recomputed as the standard's additive weighted sum, ~62%; per-audience hours; essentials-user score ~75% added; ready-for-real-work re-examined: 50% → 55%, mainstream 65% added; earlier the same day: full re-measure against Illustrator 2026 30.x; merged the ROADMAP's parity estimate, honest assessment and "Shipped so far" into this file; SVG knockout groups of opaque art export without masks) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-19 · **Change:** minor (Libraries panel: the list scrolls, items and groups reorder by dragging, items drag into and out of groups (`library.moveItem` before/after, `library.moveGroup`); scores unchanged; PDF invisible text opens as type with no fill or stroke and is written back invisible, #2675; status-readout sample widths cached by language, document unit and pixels per point; status messages and readouts use stable responsive allocations sized for localized labels and ordinary coordinates; cached canvas dimensions match the renderer limit before cache-key creation, view construction and worker submission; cached canvas reprojection preserves scale during resize, rotation and DPI changes; interactive Mirror Repeat axis; bounding-box handles take priority over the axis body while the center and end handles retain their move and rotation actions; 3D lighting: one to ten colored lights, smooth colored Inflate, legacy white-light appearance preserved; surface batching and restored seam minimum; scores and estimates unchanged; Blend spine Anchor Point conversions, selection/stacking priority and Shift regression coverage (#2544); scores and hours unchanged; Artboard Options' X and Y with a reference point, #1876; Image Trace: Auto Color keeps rare but distinct colours; Image Trace: large images trace with a warning; anchor limit raised to 8,000,000; Image Trace: Raster::new returns an error for a buffer of the wrong length; M12 maximal-disc graph pruning and junction merging measured; M12 crop EDT and Guo–Hall thinning measured; M12 centre-lines baseline measured; scores and hours unchanged; installed plug-ins stay installed after a restart, #2541; Group Selection picks and moves one path of a compound path, #2441 in part; the Move dialog opens on the last move and its last checkboxes; Image Trace: a higher Paths never gives fewer segments on collapsed loops; Image Trace: the notch rule reads only its own piece; Image Trace: an edge's sub-pixel shift is the same from either side; Image Trace Flat Logo traces line art on white paper instead of a blank result; score and hours unchanged; Image Trace: a simpler contour walk with no precomputed direction table; transparency grid visibility saved in native documents, #1968; MCP cancellation, progress and busy answers during long exports, #2165, #2171, #2172; EPS editing data: legacy mesh corners, vector opacity masks and compound member paint retained; raster page comparison filters minification without relaxing limits; scores and hours unchanged; native family-name fallback from English-only typographic names, #2208; scores unchanged; batch exporting, #1935; G2: Effect › Sketch › Halftone Pattern; Photoshop filters 30/57, 27 remaining (22–38 h); G5: Symbol Options Export Type (Graphic / Movie Clip); Localization: Brazilian Portuguese complete under the tests, #1914, 71% → 74%; G6 image variables: Automation ~60% → ~62% / detail ~65% → ~68%, hours lowered; Warp Options controls verified by focused checks and original-image review; estimates unchanged; G11: Asset Export Art Optimized seam (#983 / #1037) and supersampled thumbs; Export for Screens 85% → 90%; existing-guide Escape cancellation validated 2026-10-14; scores and hours unchanged; open-path anchor Delete keeps surviving runs separate; estimates unchanged; PDF.R1: shared PDF reader with windowed content scanning and explicit incomplete-content errors; scores and estimates unchanged; PDF.W1: guarded shared PDF writer; output behavior, scores and estimates unchanged; Artboard tab-switch follow-up historically validated on `a0671777`; scores and hours unchanged; Artboard clean active-tab close/Delete follow-up validated 2026-10-15; later 2026-10-14 merges: anchor X/Y/W/H, #1824; Snap to Grid over Snap to Pixel, #1816; toolbar and All Tools, #1751; Actions panel selection, #1629 in part; redo during a drag, #1719; preferences written on OK, #1747; type in symbols after reopening, #1738; macOS save sheets, #1855, the Format menu withdrawn; stroke outlines shared, #1667; untrusted files, #1852, #1862–#1873; failed-save dialog, #1685; effects, #1864, Dim Images, #1683; duplicated rows of the 2026-10-05 table noted for the owner; scores unchanged; Image Trace: Shapes rework, stroke caps, enhanced presets, gradients over noise, measured in `image-trace-quality.md`, score unchanged; verified PngSuite downloads, #1729; synthetic control-input budgets, #1709; 2026-10-14 merges: anchor Align and Distribute, #1636; Direct Selection and Delete fixes, #1674, #1666, #1743; Escape while typing, #1686; Pen clicks, #1663; Expand on repeats, #1756; Roughen and Scribble, #1643, #1648; file and input hardening, #1776–#1783, #1786, #1807; MCP limits and envelope, #1727, #1728; Image Trace levelling and Flat Logo on scans, #1647, #1657; scores unchanged; later 2026-10-13 merges: SVG import, #1565, #1596, #1568, #1598, #1601; type on a path from PDF and `.ai`, #1585, #1586; ICC tag alignment, #1584; saves, #1620 in part; Warp Arc, #1578; Zig Zag, #1558; Pathfinder strokes, #1570; OpenType features, #1607; intersect Smart Guides, #1602 in part; a guide's position in the Transform panel, #1597 in part, a convenience with no parity claim; scores unchanged; TIFF is available in Export for Screens and Asset Export with shared format settings; scores and hours unchanged; G1/M11: Artboard deletion and history selection normalization; history regressions and full workspace CI pass; scores and hours unchanged; Character styles retain explicit Auto leading, propagate resets and preserve local leading overrides; scores and hours unchanged; Width Point Edit: Shift-double-click enables adjoining adjustment; plain double-click keeps it off; scores and hours unchanged; Units › Identify Objects By (`identifyObjectsBy`) acts on Layers, Variables and Layer Options, #394, scores unchanged; Appearance of Black › Printing / Exporting (`blackOutput`) acts on RGB raster export and Print as Bitmap, #394, scores unchanged; per-corner radius fields with a link toggle, split from #1594; control transport deadlines, #1726; Color panel: the mode picked last comes back after a restart, #1675; Image Trace: shared edges, memory, thin lines, strokes, corners, and the Shapes, Auto Grouping and Gradients options, measured in `image-trace-quality.md`, score unchanged; 2026-10-13 merges: Polish interface, #1498; compact native saves, #1492; PDF and EPS paste as vector art, #1499; import fixes for `.ai`, PDF, EPS and SVG, #1528, #1530, #1531, #1544, #1551, #1553; guide objects, #1534; Join after Average, #1533; Keyboard Shortcuts lists commands outside the menus, #1539; correction: Average and Join with a corner or smooth choice is missing; scores unchanged; late 2026-10-12 merges: type that does not fit its frame is reported, #1471; Tabs panel drags, #1458; template layers in Outline view, #1486; dialog slider rails, #1483; Direct Selection anchor precedence, #1508; print-size images, #1517; 3D on outlined type, #1481; French catalog, #1511; scores unchanged; Split Into Grid options, #1462; Transform Objects and Reflect across an angle, #1448 and #1460; Pathfinder panel on a group, #1450; unknown parameter keys refused, #1442; grayscale TIFFs with alpha, #1466 in part; Localization: Vietnamese catalog added, 63% → 71%, hours 60–100 → 55–90, full readiness unchanged at ~62%; scores and hours unchanged; Image Trace places edges to a fraction of a pixel in every mode, measured in `image-trace-quality.md`, score unchanged; 3D and Materials: Extrude & Bevel, Inflate and Rotate added, 3D ~8% → ~25% (estimated), menu stubs 25 → 22; Affinity text frames too short for their first line show it, #1423; Transform dialogs preview, #1412, and the transform tools and commands act on direct-selected anchors, #1408; `.ai` compound path paint, #1410; strokes wider than their shape, #1396; corner drags hide the bounding box, #1229; scores and hours unchanged; PDF and `.ai` import keep overprint, #1388, scores unchanged; `.ai` editing-data follow-ups: gradient stops, type of objects with an appearance, short area-type frames (#1390, #1391, #1392); dialog keyboard focus on every open (#1382, #1395); art flush with an artboard (#1375); font rescan (#1376); symbol instance redraw cache (#1386); scores and hours unchanged; grid tool options, #1377 in part, scores unchanged; `.ai` placed (linked) files read from the editing data, #1264, scores unchanged; gradient shapes import as paths, #1304; white constant alpha masks, #1286 in part; Live Corners widget click selects its corner, #1239; scores and hours unchanged; custom ruler origin, #1212; Artboard click-to-fit verified for objects, groups, visible bounds, undo and cancellation (#1227); scores and hours unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; Localization: Korean catalog added, 54% → 63%, full readiness unchanged at ~62%; desktop print transport adopts craft-print, Windows 7 fallback retained, scores unchanged; Image Trace: an image over 64 megapixels traces scaled down instead of being refused, scores unchanged; Czech render-site localization follow-up; scores and estimates unchanged; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; Path operations: Simplify preview/counts and component selection (#1272); scores unchanged; G2: Film Grain added; Photoshop effects 29/57; 28 filters remain; Platforms: RPM runtime capabilities and glibc floor (#1275); scores unchanged; Raster effects: squared-distance texture redraws (#1257); pixels unchanged; scores and hours unchanged; Drawing: Curvature point display improved (#1236, in part); scores unchanged; Type core: full-name font matching (#1258), scores and hours unchanged; Selection: no-op anchor Align preserves history and saved state (#1180 follow-up); scores unchanged; UI chrome: Eyedropper cursor and precise-setting regressions (#1269); scores unchanged; Colour: Eyedropper reads visible locked and outside-isolation sources (#1251); scores and hours unchanged; Selection: segment click/Delete preserves neighboring edges (#1278); scores unchanged; Colour: unpainted zero-width strokes regain 1 pt when painted (#1253); scores and hours unchanged; graphs: designs and markers, data, Graph Type and label options; UI chrome: native macOS save Format menu, #866, scores unchanged; compound paths inherit the backmost appearance and member fill-rule edits reach their owner, #1197; scores unchanged; Type core: Middle Eastern digit types done, Character panel menu › Digits; PDF export warns when the pixel cap lowers raster-effect resolution; Drawing tools: Live Corners round corners between curved sides, #1144; Type advanced: proportional vertical metrics (`vpal`) done, #966; Shape Builder: #893 fixed, merged regions of crossing lines no longer keep seams; scores unchanged while #937 is open; #1034: document-point character controls after object/group scaling; full readiness recomputed as the standard's additive weighted sum, ~62%; per-audience hours; essentials-user score ~75% added; ready-for-real-work re-examined: 50% → 55%, mainstream 65% added; earlier the same day: full re-measure against Illustrator 2026 30.x; merged the ROADMAP's parity estimate, honest assessment and "Shipped so far" into this file; SVG knockout groups of opaque art export without masks; Image Trace: colour presets no longer shatter noisy flat art and photos trace into smooth tones, measured in `image-trace-quality.md`, score unchanged; Split Into Grid options, #1462; Transform Objects and Reflect across an angle, #1448 and #1460; Pathfinder panel on a group, #1450; unknown parameter keys refused, #1442; grayscale TIFFs with alpha, #1466 in part; scores and hours unchanged; Image Trace places edges to a fraction of a pixel in every mode, measured in `image-trace-quality.md`, score unchanged; 3D and Materials: Extrude & Bevel, Inflate and Rotate added, 3D ~8% → ~25% (estimated), menu stubs 25 → 22; Affinity text frames too short for their first line show it, #1423; Transform dialogs preview, #1412, and the transform tools and commands act on direct-selected anchors, #1408; `.ai` compound path paint, #1410; strokes wider than their shape, #1396; corner drags hide the bounding box, #1229; scores and hours unchanged; PDF and `.ai` import keep overprint, #1388, scores unchanged; `.ai` editing-data follow-ups: gradient stops, type of objects with an appearance, short area-type frames (#1390, #1391, #1392); dialog keyboard focus on every open (#1382, #1395); art flush with an artboard (#1375); font rescan (#1376); symbol instance redraw cache (#1386); scores and hours unchanged; grid tool options, #1377 in part, scores unchanged; `.ai` placed (linked) files read from the editing data, #1264, scores unchanged; gradient shapes import as paths, #1304; white constant alpha masks, #1286 in part; Live Corners widget click selects its corner, #1239; scores and hours unchanged; custom ruler origin, #1212; Artboard click-to-fit verified for objects, groups, visible bounds, undo and cancellation (#1227); scores and hours unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; Localization: Korean catalog added, 54% → 63%, full readiness unchanged at ~62%; desktop print transport adopts craft-print, Windows 7 fallback retained, scores unchanged; Image Trace: an image over 64 megapixels traces scaled down instead of being refused, scores unchanged; Czech render-site localization follow-up; scores and estimates unchanged; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; Path operations: Simplify preview/counts and component selection (#1272); scores unchanged; G2: Film Grain added; Photoshop effects 29/57; 28 filters remain; Platforms: RPM runtime capabilities and glibc floor (#1275); scores unchanged; Raster effects: squared-distance texture redraws (#1257); pixels unchanged; scores and hours unchanged; Drawing: Curvature point display improved (#1236, in part); scores unchanged; Type core: full-name font matching (#1258), scores and hours unchanged; Selection: no-op anchor Align preserves history and saved state (#1180 follow-up); scores unchanged; UI chrome: Eyedropper cursor and precise-setting regressions (#1269); scores unchanged; Colour: Eyedropper reads visible locked and outside-isolation sources (#1251); scores and hours unchanged; Selection: segment click/Delete preserves neighboring edges (#1278); scores unchanged; Colour: unpainted zero-width strokes regain 1 pt when painted (#1253); scores and hours unchanged; graphs: designs and markers, data, Graph Type and label options; UI chrome: native macOS save Format menu, #866, scores unchanged; compound paths inherit the backmost appearance and member fill-rule edits reach their owner, #1197; scores unchanged; Type core: Middle Eastern digit types done, Character panel menu › Digits; PDF export warns when the pixel cap lowers raster-effect resolution; Drawing tools: Live Corners round corners between curved sides, #1144; Type advanced: proportional vertical metrics (`vpal`) done, #966; Shape Builder: #893 fixed, merged regions of crossing lines no longer keep seams; scores unchanged while #937 is open; #1034: document-point character controls after object/group scaling; full readiness recomputed as the standard's additive weighted sum, ~62%; per-audience hours; essentials-user score ~75% added; ready-for-real-work re-examined: 50% → 55%, mainstream 65% added; earlier the same day: full re-measure against Illustrator 2026 30.x; merged the ROADMAP's parity estimate, honest assessment and "Shipped so far" into this file; SVG knockout groups of opaque art export without masks; MCP `add_text` exposes the existing `vertical` option for point, area and path type; horizontal remains the default; scores and hours unchanged; Vertical fullwidth question and exclamation marks keep their nominal glyph upright when the font has no vertical alternate; scores and hours unchanged.; Curvature curve geometry, #1236 in part; Width-point finite-arithmetic follow-up validated; scores and hours unchanged; Image Trace adjustment discovery; File interop: legacy `.ai` of version 7 saved with type as text, hidden objects kept and an artboard picker, #868, scores unchanged; Revolve: automatic smooth shading and default-on Cap with continuous rims; PDF.R1: shared PDF reader with windowed content scanning and explicit incomplete-content errors; scores and estimates unchanged; PDF.W1: guarded shared PDF writer; output behavior, scores and estimates unchanged; later 2026-10-14 merges: anchor X/Y/W/H, #1824; Snap to Grid over Snap to Pixel, #1816; toolbar and All Tools, #1751; Actions panel selection, #1629 in part; redo during a drag, #1719; preferences written on OK, #1747; type in symbols after reopening, #1738; macOS save sheets, #1855, the Format menu withdrawn; stroke outlines shared, #1667; untrusted files, #1852, #1862–#1873; failed-save dialog, #1685; effects, #1864, Dim Images, #1683; duplicated rows of the 2026-10-05 table noted for the owner; scores unchanged; Image Trace: Shapes rework, stroke caps, enhanced presets, gradients over noise, measured in `image-trace-quality.md`, score unchanged; verified PngSuite downloads, #1729; synthetic control-input budgets, #1709; 2026-10-14 merges: anchor Align and Distribute, #1636; Direct Selection and Delete fixes, #1674, #1666, #1743; Escape while typing, #1686; Pen clicks, #1663; Expand on repeats, #1756; Roughen and Scribble, #1643, #1648; file and input hardening, #1776–#1783, #1786, #1807; MCP limits and envelope, #1727, #1728; Image Trace levelling and Flat Logo on scans, #1647, #1657; scores unchanged; later 2026-10-13 merges: SVG import, #1565, #1596, #1568, #1598, #1601; type on a path from PDF and `.ai`, #1585, #1586; ICC tag alignment, #1584; saves, #1620 in part; Warp Arc, #1578; Zig Zag, #1558; Pathfinder strokes, #1570; OpenType features, #1607; intersect Smart Guides, #1602 in part; a guide's position in the Transform panel, #1597 in part, a convenience with no parity claim; scores unchanged; TIFF is available in Export for Screens and Asset Export with shared format settings; scores and hours unchanged; G1/M11: Artboard deletion and history selection normalization; history regressions and full workspace CI pass; scores and hours unchanged; Character styles retain explicit Auto leading, propagate resets and preserve local leading overrides; scores and hours unchanged; Width Point Edit: Shift-double-click enables adjoining adjustment; plain double-click keeps it off; scores and hours unchanged; Units › Identify Objects By (`identifyObjectsBy`) acts on Layers, Variables and Layer Options, #394, scores unchanged; Appearance of Black › Printing / Exporting (`blackOutput`) acts on RGB raster export and Print as Bitmap, #394, scores unchanged; per-corner radius fields with a link toggle, split from #1594; control transport deadlines, #1726; Color panel: the mode picked last comes back after a restart, #1675; Image Trace: shared edges, memory, thin lines, strokes, corners, and the Shapes, Auto Grouping and Gradients options, measured in `image-trace-quality.md`, score unchanged; 2026-10-13 merges: Polish interface, #1498; compact native saves, #1492; PDF and EPS paste as vector art, #1499; import fixes for `.ai`, PDF, EPS and SVG, #1528, #1530, #1531, #1544, #1551, #1553; guide objects, #1534; Join after Average, #1533; Keyboard Shortcuts lists commands outside the menus, #1539; correction: Average and Join with a corner or smooth choice is missing; scores unchanged; late 2026-10-12 merges: type that does not fit its frame is reported, #1471; Tabs panel drags, #1458; template layers in Outline view, #1486; dialog slider rails, #1483; Direct Selection anchor precedence, #1508; print-size images, #1517; 3D on outlined type, #1481; French catalog, #1511; scores unchanged; Split Into Grid options, #1462; Transform Objects and Reflect across an angle, #1448 and #1460; Pathfinder panel on a group, #1450; unknown parameter keys refused, #1442; grayscale TIFFs with alpha, #1466 in part; Localization: Vietnamese catalog added, 63% → 71%, hours 60–100 → 55–90, full readiness unchanged at ~62%; scores and hours unchanged; Image Trace places edges to a fraction of a pixel in every mode, measured in `image-trace-quality.md`, score unchanged; 3D and Materials: Extrude & Bevel, Inflate and Rotate added, 3D ~8% → ~25% (estimated), menu stubs 25 → 22; Affinity text frames too short for their first line show it, #1423; Transform dialogs preview, #1412, and the transform tools and commands act on direct-selected anchors, #1408; `.ai` compound path paint, #1410; strokes wider than their shape, #1396; corner drags hide the bounding box, #1229; scores and hours unchanged; PDF and `.ai` import keep overprint, #1388, scores unchanged; `.ai` editing-data follow-ups: gradient stops, type of objects with an appearance, short area-type frames (#1390, #1391, #1392); dialog keyboard focus on every open (#1382, #1395); art flush with an artboard (#1375); font rescan (#1376); symbol instance redraw cache (#1386); scores and hours unchanged; grid tool options, #1377 in part, scores unchanged; `.ai` placed (linked) files read from the editing data, #1264, scores unchanged; gradient shapes import as paths, #1304; white constant alpha masks, #1286 in part; Live Corners widget click selects its corner, #1239; scores and hours unchanged; custom ruler origin, #1212; Artboard click-to-fit verified for objects, groups, visible bounds, undo and cancellation (#1227); scores and hours unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; Localization: Korean catalog added, 54% → 63%, full readiness unchanged at ~62%; desktop print transport adopts craft-print, Windows 7 fallback retained, scores unchanged; Image Trace: an image over 64 megapixels traces scaled down instead of being refused, scores unchanged; Czech render-site localization follow-up; scores and estimates unchanged; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; Path operations: Simplify preview/counts and component selection (#1272); scores unchanged; G2: Film Grain added; Photoshop effects 29/57; 28 filters remain; Platforms: RPM runtime capabilities and glibc floor (#1275); scores unchanged; Raster effects: squared-distance texture redraws (#1257); pixels unchanged; scores and hours unchanged; Drawing: Curvature point display improved (#1236, in part); scores unchanged; Type core: full-name font matching (#1258), scores and hours unchanged; Selection: no-op anchor Align preserves history and saved state (#1180 follow-up); scores unchanged; UI chrome: Eyedropper cursor and precise-setting regressions (#1269); scores unchanged; Colour: Eyedropper reads visible locked and outside-isolation sources (#1251); scores and hours unchanged; Selection: segment click/Delete preserves neighboring edges (#1278); scores unchanged; Colour: unpainted zero-width strokes regain 1 pt when painted (#1253); scores and hours unchanged; graphs: designs and markers, data, Graph Type and label options; UI chrome: native macOS save Format menu, #866, scores unchanged; compound paths inherit the backmost appearance and member fill-rule edits reach their owner, #1197; scores unchanged; Type core: Middle Eastern digit types done, Character panel menu › Digits; PDF export warns when the pixel cap lowers raster-effect resolution; Drawing tools: Live Corners round corners between curved sides, #1144; Type advanced: proportional vertical metrics (`vpal`) done, #966; Shape Builder: #893 fixed, merged regions of crossing lines no longer keep seams; scores unchanged while #937 is open; #1034: document-point character controls after object/group scaling; full readiness recomputed as the standard's additive weighted sum, ~62%; per-audience hours; essentials-user score ~75% added; ready-for-real-work re-examined: 50% → 55%, mainstream 65% added; earlier the same day: full re-measure against Illustrator 2026 30.x; merged the ROADMAP's parity estimate, honest assessment and "Shipped so far" into this file; SVG knockout groups of opaque art export without masks; Image Trace: colour presets no longer shatter noisy flat art and photos trace into smooth tones, measured in `image-trace-quality.md`, score unchanged; Split Into Grid options, #1462; Transform Objects and Reflect across an angle, #1448 and #1460; Pathfinder panel on a group, #1450; unknown parameter keys refused, #1442; grayscale TIFFs with alpha, #1466 in part; scores and hours unchanged; Image Trace places edges to a fraction of a pixel in every mode, measured in `image-trace-quality.md`, score unchanged; 3D and Materials: Extrude & Bevel, Inflate and Rotate added, 3D ~8% → ~25% (estimated), menu stubs 25 → 22; Affinity text frames too short for their first line show it, #1423; Transform dialogs preview, #1412, and the transform tools and commands act on direct-selected anchors, #1408; `.ai` compound path paint, #1410; strokes wider than their shape, #1396; corner drags hide the bounding box, #1229; scores and hours unchanged; PDF and `.ai` import keep overprint, #1388, scores unchanged; `.ai` editing-data follow-ups: gradient stops, type of objects with an appearance, short area-type frames (#1390, #1391, #1392); dialog keyboard focus on every open (#1382, #1395); art flush with an artboard (#1375); font rescan (#1376); symbol instance redraw cache (#1386); scores and hours unchanged; grid tool options, #1377 in part, scores unchanged; `.ai` placed (linked) files read from the editing data, #1264, scores unchanged; gradient shapes import as paths, #1304; white constant alpha masks, #1286 in part; Live Corners widget click selects its corner, #1239; scores and hours unchanged; custom ruler origin, #1212; Artboard click-to-fit verified for objects, groups, visible bounds, undo and cancellation (#1227); scores and hours unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; Localization: Korean catalog added, 54% → 63%, full readiness unchanged at ~62%; desktop print transport adopts craft-print, Windows 7 fallback retained, scores unchanged; Image Trace: an image over 64 megapixels traces scaled down instead of being refused, scores unchanged; Czech render-site localization follow-up; scores and estimates unchanged; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; Path operations: Simplify preview/counts and component selection (#1272); scores unchanged; G2: Film Grain added; Photoshop effects 29/57; 28 filters remain; Platforms: RPM runtime capabilities and glibc floor (#1275); scores unchanged; Raster effects: squared-distance texture redraws (#1257); pixels unchanged; scores and hours unchanged; Drawing: Curvature point display improved (#1236, in part); scores unchanged; Type core: full-name font matching (#1258), scores and hours unchanged; Selection: no-op anchor Align preserves history and saved state (#1180 follow-up); scores unchanged; UI chrome: Eyedropper cursor and precise-setting regressions (#1269); scores unchanged; Colour: Eyedropper reads visible locked and outside-isolation sources (#1251); scores and hours unchanged; Selection: segment click/Delete preserves neighboring edges (#1278); scores unchanged; Colour: unpainted zero-width strokes regain 1 pt when painted (#1253); scores and hours unchanged; graphs: designs and markers, data, Graph Type and label options; UI chrome: native macOS save Format menu, #866, scores unchanged; compound paths inherit the backmost appearance and member fill-rule edits reach their owner, #1197; scores unchanged; Type core: Middle Eastern digit types done, Character panel menu › Digits; PDF export warns when the pixel cap lowers raster-effect resolution; Drawing tools: Live Corners round corners between curved sides, #1144; Type advanced: proportional vertical metrics (`vpal`) done, #966; Shape Builder: #893 fixed, merged regions of crossing lines no longer keep seams; scores unchanged while #937 is open; #1034: document-point character controls after object/group scaling; full readiness recomputed as the standard's additive weighted sum, ~62%; per-audience hours; essentials-user score ~75% added; ready-for-real-work re-examined: 50% → 55%, mainstream 65% added; earlier the same day: full re-measure against Illustrator 2026 30.x; merged the ROADMAP's parity estimate, honest assessment and "Shipped so far" into this file; SVG knockout groups of opaque art export without masks) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026) > **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-19 · **Change:** minor (a drag over art whose effects reach past the view, such as a wide blur, draws them from the still frame's raster, #2558; interactive Mirror Repeat axis; bounding-box handles take priority over the axis body while the center and end handles retain their move and rotation actions; 3D lighting: one to ten colored lights, smooth colored Inflate, legacy white-light appearance preserved; surface batching and restored seam minimum; scores and estimates unchanged; Blend spine Anchor Point conversions, selection/stacking priority and Shift regression coverage (#2544); scores and hours unchanged; Artboard Options' X and Y with a reference point, #1876; Image Trace: Auto Color keeps rare but distinct colours; Image Trace: large images trace with a warning; anchor limit raised to 8,000,000; Image Trace: Raster::new returns an error for a buffer of the wrong length; M12 maximal-disc graph pruning and junction merging measured; M12 crop EDT and Guo–Hall thinning measured; M12 centre-lines baseline measured; scores and hours unchanged; installed plug-ins stay installed after a restart, #2541; Group Selection picks and moves one path of a compound path, #2441 in part; the Move dialog opens on the last move and its last checkboxes; Image Trace: a higher Paths never gives fewer segments on collapsed loops; Image Trace: the notch rule reads only its own piece; Image Trace: an edge's sub-pixel shift is the same from either side; Image Trace Flat Logo traces line art on white paper instead of a blank result; score and hours unchanged; Image Trace: a simpler contour walk with no precomputed direction table; transparency grid visibility saved in native documents, #1968; MCP cancellation, progress and busy answers during long exports, #2165, #2171, #2172; EPS editing data: legacy mesh corners, vector opacity masks and compound member paint retained; raster page comparison filters minification without relaxing limits; scores and hours unchanged; native family-name fallback from English-only typographic names, #2208; scores unchanged; batch exporting, #1935; G2: Effect › Sketch › Halftone Pattern; Photoshop filters 30/57, 27 remaining (22–38 h); G5: Symbol Options Export Type (Graphic / Movie Clip); Localization: Brazilian Portuguese complete under the tests, #1914, 71% → 74%; G6 image variables: Automation ~60% → ~62% / detail ~65% → ~68%, hours lowered; Warp Options controls verified by focused checks and original-image review; estimates unchanged; G11: Asset Export Art Optimized seam (#983 / #1037) and supersampled thumbs; Export for Screens 85% → 90%; existing-guide Escape cancellation validated 2026-10-14; scores and hours unchanged; open-path anchor Delete keeps surviving runs separate; estimates unchanged; PDF.R1: shared PDF reader with windowed content scanning and explicit incomplete-content errors; scores and estimates unchanged; PDF.W1: guarded shared PDF writer; output behavior, scores and estimates unchanged; Artboard tab-switch follow-up historically validated on `a0671777`; scores and hours unchanged; Artboard clean active-tab close/Delete follow-up validated 2026-10-15; later 2026-10-14 merges: anchor X/Y/W/H, #1824; Snap to Grid over Snap to Pixel, #1816; toolbar and All Tools, #1751; Actions panel selection, #1629 in part; redo during a drag, #1719; preferences written on OK, #1747; type in symbols after reopening, #1738; macOS save sheets, #1855, the Format menu withdrawn; stroke outlines shared, #1667; untrusted files, #1852, #1862–#1873; failed-save dialog, #1685; effects, #1864, Dim Images, #1683; duplicated rows of the 2026-10-05 table noted for the owner; scores unchanged; Image Trace: Shapes rework, stroke caps, enhanced presets, gradients over noise, measured in `image-trace-quality.md`, score unchanged; verified PngSuite downloads, #1729; synthetic control-input budgets, #1709; 2026-10-14 merges: anchor Align and Distribute, #1636; Direct Selection and Delete fixes, #1674, #1666, #1743; Escape while typing, #1686; Pen clicks, #1663; Expand on repeats, #1756; Roughen and Scribble, #1643, #1648; file and input hardening, #1776–#1783, #1786, #1807; MCP limits and envelope, #1727, #1728; Image Trace levelling and Flat Logo on scans, #1647, #1657; scores unchanged; later 2026-10-13 merges: SVG import, #1565, #1596, #1568, #1598, #1601; type on a path from PDF and `.ai`, #1585, #1586; ICC tag alignment, #1584; saves, #1620 in part; Warp Arc, #1578; Zig Zag, #1558; Pathfinder strokes, #1570; OpenType features, #1607; intersect Smart Guides, #1602 in part; a guide's position in the Transform panel, #1597 in part, a convenience with no parity claim; scores unchanged; TIFF is available in Export for Screens and Asset Export with shared format settings; scores and hours unchanged; G1/M11: Artboard deletion and history selection normalization; history regressions and full workspace CI pass; scores and hours unchanged; Character styles retain explicit Auto leading, propagate resets and preserve local leading overrides; scores and hours unchanged; Width Point Edit: Shift-double-click enables adjoining adjustment; plain double-click keeps it off; scores and hours unchanged; Units › Identify Objects By (`identifyObjectsBy`) acts on Layers, Variables and Layer Options, #394, scores unchanged; Appearance of Black › Printing / Exporting (`blackOutput`) acts on RGB raster export and Print as Bitmap, #394, scores unchanged; per-corner radius fields with a link toggle, split from #1594; control transport deadlines, #1726; Color panel: the mode picked last comes back after a restart, #1675; Image Trace: shared edges, memory, thin lines, strokes, corners, and the Shapes, Auto Grouping and Gradients options, measured in `image-trace-quality.md`, score unchanged; 2026-10-13 merges: Polish interface, #1498; compact native saves, #1492; PDF and EPS paste as vector art, #1499; import fixes for `.ai`, PDF, EPS and SVG, #1528, #1530, #1531, #1544, #1551, #1553; guide objects, #1534; Join after Average, #1533; Keyboard Shortcuts lists commands outside the menus, #1539; correction: Average and Join with a corner or smooth choice is missing; scores unchanged; late 2026-10-12 merges: type that does not fit its frame is reported, #1471; Tabs panel drags, #1458; template layers in Outline view, #1486; dialog slider rails, #1483; Direct Selection anchor precedence, #1508; print-size images, #1517; 3D on outlined type, #1481; French catalog, #1511; scores unchanged; Split Into Grid options, #1462; Transform Objects and Reflect across an angle, #1448 and #1460; Pathfinder panel on a group, #1450; unknown parameter keys refused, #1442; grayscale TIFFs with alpha, #1466 in part; Localization: Vietnamese catalog added, 63% → 71%, hours 60–100 → 55–90, full readiness unchanged at ~62%; scores and hours unchanged; Image Trace places edges to a fraction of a pixel in every mode, measured in `image-trace-quality.md`, score unchanged; 3D and Materials: Extrude & Bevel, Inflate and Rotate added, 3D ~8% → ~25% (estimated), menu stubs 25 → 22; Affinity text frames too short for their first line show it, #1423; Transform dialogs preview, #1412, and the transform tools and commands act on direct-selected anchors, #1408; `.ai` compound path paint, #1410; strokes wider than their shape, #1396; corner drags hide the bounding box, #1229; scores and hours unchanged; PDF and `.ai` import keep overprint, #1388, scores unchanged; `.ai` editing-data follow-ups: gradient stops, type of objects with an appearance, short area-type frames (#1390, #1391, #1392); dialog keyboard focus on every open (#1382, #1395); art flush with an artboard (#1375); font rescan (#1376); symbol instance redraw cache (#1386); scores and hours unchanged; grid tool options, #1377 in part, scores unchanged; `.ai` placed (linked) files read from the editing data, #1264, scores unchanged; gradient shapes import as paths, #1304; white constant alpha masks, #1286 in part; Live Corners widget click selects its corner, #1239; scores and hours unchanged; custom ruler origin, #1212; Artboard click-to-fit verified for objects, groups, visible bounds, undo and cancellation (#1227); scores and hours unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; Localization: Korean catalog added, 54% → 63%, full readiness unchanged at ~62%; desktop print transport adopts craft-print, Windows 7 fallback retained, scores unchanged; Image Trace: an image over 64 megapixels traces scaled down instead of being refused, scores unchanged; Czech render-site localization follow-up; scores and estimates unchanged; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; Path operations: Simplify preview/counts and component selection (#1272); scores unchanged; G2: Film Grain added; Photoshop effects 29/57; 28 filters remain; Platforms: RPM runtime capabilities and glibc floor (#1275); scores unchanged; Raster effects: squared-distance texture redraws (#1257); pixels unchanged; scores and hours unchanged; Drawing: Curvature point display improved (#1236, in part); scores unchanged; Type core: full-name font matching (#1258), scores and hours unchanged; Selection: no-op anchor Align preserves history and saved state (#1180 follow-up); scores unchanged; UI chrome: Eyedropper cursor and precise-setting regressions (#1269); scores unchanged; Colour: Eyedropper reads visible locked and outside-isolation sources (#1251); scores and hours unchanged; Selection: segment click/Delete preserves neighboring edges (#1278); scores unchanged; Colour: unpainted zero-width strokes regain 1 pt when painted (#1253); scores and hours unchanged; graphs: designs and markers, data, Graph Type and label options; UI chrome: native macOS save Format menu, #866, scores unchanged; compound paths inherit the backmost appearance and member fill-rule edits reach their owner, #1197; scores unchanged; Type core: Middle Eastern digit types done, Character panel menu › Digits; PDF export warns when the pixel cap lowers raster-effect resolution; Drawing tools: Live Corners round corners between curved sides, #1144; Type advanced: proportional vertical metrics (`vpal`) done, #966; Shape Builder: #893 fixed, merged regions of crossing lines no longer keep seams; scores unchanged while #937 is open; #1034: document-point character controls after object/group scaling; full readiness recomputed as the standard's additive weighted sum, ~62%; per-audience hours; essentials-user score ~75% added; ready-for-real-work re-examined: 50% → 55%, mainstream 65% added; earlier the same day: full re-measure against Illustrator 2026 30.x; merged the ROADMAP's parity estimate, honest assessment and "Shipped so far" into this file; SVG knockout groups of opaque art export without masks; Image Trace: colour presets no longer shatter noisy flat art and photos trace into smooth tones, measured in `image-trace-quality.md`, score unchanged; Split Into Grid options, #1462; Transform Objects and Reflect across an angle, #1448 and #1460; Pathfinder panel on a group, #1450; unknown parameter keys refused, #1442; grayscale TIFFs with alpha, #1466 in part; scores and hours unchanged; Image Trace places edges to a fraction of a pixel in every mode, measured in `image-trace-quality.md`, score unchanged; 3D and Materials: Extrude & Bevel, Inflate and Rotate added, 3D ~8% → ~25% (estimated), menu stubs 25 → 22; Affinity text frames too short for their first line show it, #1423; Transform dialogs preview, #1412, and the transform tools and commands act on direct-selected anchors, #1408; `.ai` compound path paint, #1410; strokes wider than their shape, #1396; corner drags hide the bounding box, #1229; scores and hours unchanged; PDF and `.ai` import keep overprint, #1388, scores unchanged; `.ai` editing-data follow-ups: gradient stops, type of objects with an appearance, short area-type frames (#1390, #1391, #1392); dialog keyboard focus on every open (#1382, #1395); art flush with an artboard (#1375); font rescan (#1376); symbol instance redraw cache (#1386); scores and hours unchanged; grid tool options, #1377 in part, scores unchanged; `.ai` placed (linked) files read from the editing data, #1264, scores unchanged; gradient shapes import as paths, #1304; white constant alpha masks, #1286 in part; Live Corners widget click selects its corner, #1239; scores and hours unchanged; custom ruler origin, #1212; Artboard click-to-fit verified for objects, groups, visible bounds, undo and cancellation (#1227); scores and hours unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; Localization: Korean catalog added, 54% → 63%, full readiness unchanged at ~62%; desktop print transport adopts craft-print, Windows 7 fallback retained, scores unchanged; Image Trace: an image over 64 megapixels traces scaled down instead of being refused, scores unchanged; Czech render-site localization follow-up; scores and estimates unchanged; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; Path operations: Simplify preview/counts and component selection (#1272); scores unchanged; G2: Film Grain added; Photoshop effects 29/57; 28 filters remain; Platforms: RPM runtime capabilities and glibc floor (#1275); scores unchanged; Raster effects: squared-distance texture redraws (#1257); pixels unchanged; scores and hours unchanged; Drawing: Curvature point display improved (#1236, in part); scores unchanged; Type core: full-name font matching (#1258), scores and hours unchanged; Selection: no-op anchor Align preserves history and saved state (#1180 follow-up); scores unchanged; UI chrome: Eyedropper cursor and precise-setting regressions (#1269); scores unchanged; Colour: Eyedropper reads visible locked and outside-isolation sources (#1251); scores and hours unchanged; Selection: segment click/Delete preserves neighboring edges (#1278); scores unchanged; Colour: unpainted zero-width strokes regain 1 pt when painted (#1253); scores and hours unchanged; graphs: designs and markers, data, Graph Type and label options; UI chrome: native macOS save Format menu, #866, scores unchanged; compound paths inherit the backmost appearance and member fill-rule edits reach their owner, #1197; scores unchanged; Type core: Middle Eastern digit types done, Character panel menu › Digits; PDF export warns when the pixel cap lowers raster-effect resolution; Drawing tools: Live Corners round corners between curved sides, #1144; Type advanced: proportional vertical metrics (`vpal`) done, #966; Shape Builder: #893 fixed, merged regions of crossing lines no longer keep seams; scores unchanged while #937 is open; #1034: document-point character controls after object/group scaling; full readiness recomputed as the standard's additive weighted sum, ~62%; per-audience hours; essentials-user score ~75% added; ready-for-real-work re-examined: 50% → 55%, mainstream 65% added; earlier the same day: full re-measure against Illustrator 2026 30.x; merged the ROADMAP's parity estimate, honest assessment and "Shipped so far" into this file; SVG knockout groups of opaque art export without masks; MCP `add_text` exposes the existing `vertical` option for point, area and path type; horizontal remains the default; scores and hours unchanged; Vertical fullwidth question and exclamation marks keep their nominal glyph upright when the font has no vertical alternate; scores and hours unchanged.; Curvature curve geometry, #1236 in part; Width-point finite-arithmetic follow-up validated; scores and hours unchanged; Image Trace adjustment discovery; File interop: legacy `.ai` of version 7 saved with type as text, hidden objects kept and an artboard picker, #868, scores unchanged; Revolve: automatic smooth shading and default-on Cap with continuous rims; PDF.R1: shared PDF reader with windowed content scanning and explicit incomplete-content errors; scores and estimates unchanged; PDF.W1: guarded shared PDF writer; output behavior, scores and estimates unchanged; later 2026-10-14 merges: anchor X/Y/W/H, #1824; Snap to Grid over Snap to Pixel, #1816; toolbar and All Tools, #1751; Actions panel selection, #1629 in part; redo during a drag, #1719; preferences written on OK, #1747; type in symbols after reopening, #1738; macOS save sheets, #1855, the Format menu withdrawn; stroke outlines shared, #1667; untrusted files, #1852, #1862–#1873; failed-save dialog, #1685; effects, #1864, Dim Images, #1683; duplicated rows of the 2026-10-05 table noted for the owner; scores unchanged; Image Trace: Shapes rework, stroke caps, enhanced presets, gradients over noise, measured in `image-trace-quality.md`, score unchanged; verified PngSuite downloads, #1729; synthetic control-input budgets, #1709; 2026-10-14 merges: anchor Align and Distribute, #1636; Direct Selection and Delete fixes, #1674, #1666, #1743; Escape while typing, #1686; Pen clicks, #1663; Expand on repeats, #1756; Roughen and Scribble, #1643, #1648; file and input hardening, #1776–#1783, #1786, #1807; MCP limits and envelope, #1727, #1728; Image Trace levelling and Flat Logo on scans, #1647, #1657; scores unchanged; later 2026-10-13 merges: SVG import, #1565, #1596, #1568, #1598, #1601; type on a path from PDF and `.ai`, #1585, #1586; ICC tag alignment, #1584; saves, #1620 in part; Warp Arc, #1578; Zig Zag, #1558; Pathfinder strokes, #1570; OpenType features, #1607; intersect Smart Guides, #1602 in part; a guide's position in the Transform panel, #1597 in part, a convenience with no parity claim; scores unchanged; TIFF is available in Export for Screens and Asset Export with shared format settings; scores and hours unchanged; G1/M11: Artboard deletion and history selection normalization; history regressions and full workspace CI pass; scores and hours unchanged; Character styles retain explicit Auto leading, propagate resets and preserve local leading overrides; scores and hours unchanged; Width Point Edit: Shift-double-click enables adjoining adjustment; plain double-click keeps it off; scores and hours unchanged; Units › Identify Objects By (`identifyObjectsBy`) acts on Layers, Variables and Layer Options, #394, scores unchanged; Appearance of Black › Printing / Exporting (`blackOutput`) acts on RGB raster export and Print as Bitmap, #394, scores unchanged; per-corner radius fields with a link toggle, split from #1594; control transport deadlines, #1726; Color panel: the mode picked last comes back after a restart, #1675; Image Trace: shared edges, memory, thin lines, strokes, corners, and the Shapes, Auto Grouping and Gradients options, measured in `image-trace-quality.md`, score unchanged; 2026-10-13 merges: Polish interface, #1498; compact native saves, #1492; PDF and EPS paste as vector art, #1499; import fixes for `.ai`, PDF, EPS and SVG, #1528, #1530, #1531, #1544, #1551, #1553; guide objects, #1534; Join after Average, #1533; Keyboard Shortcuts lists commands outside the menus, #1539; correction: Average and Join with a corner or smooth choice is missing; scores unchanged; late 2026-10-12 merges: type that does not fit its frame is reported, #1471; Tabs panel drags, #1458; template layers in Outline view, #1486; dialog slider rails, #1483; Direct Selection anchor precedence, #1508; print-size images, #1517; 3D on outlined type, #1481; French catalog, #1511; scores unchanged; Split Into Grid options, #1462; Transform Objects and Reflect across an angle, #1448 and #1460; Pathfinder panel on a group, #1450; unknown parameter keys refused, #1442; grayscale TIFFs with alpha, #1466 in part; Localization: Vietnamese catalog added, 63% → 71%, hours 60–100 → 55–90, full readiness unchanged at ~62%; scores and hours unchanged; Image Trace places edges to a fraction of a pixel in every mode, measured in `image-trace-quality.md`, score unchanged; 3D and Materials: Extrude & Bevel, Inflate and Rotate added, 3D ~8% → ~25% (estimated), menu stubs 25 → 22; Affinity text frames too short for their first line show it, #1423; Transform dialogs preview, #1412, and the transform tools and commands act on direct-selected anchors, #1408; `.ai` compound path paint, #1410; strokes wider than their shape, #1396; corner drags hide the bounding box, #1229; scores and hours unchanged; PDF and `.ai` import keep overprint, #1388, scores unchanged; `.ai` editing-data follow-ups: gradient stops, type of objects with an appearance, short area-type frames (#1390, #1391, #1392); dialog keyboard focus on every open (#1382, #1395); art flush with an artboard (#1375); font rescan (#1376); symbol instance redraw cache (#1386); scores and hours unchanged; grid tool options, #1377 in part, scores unchanged; `.ai` placed (linked) files read from the editing data, #1264, scores unchanged; gradient shapes import as paths, #1304; white constant alpha masks, #1286 in part; Live Corners widget click selects its corner, #1239; scores and hours unchanged; custom ruler origin, #1212; Artboard click-to-fit verified for objects, groups, visible bounds, undo and cancellation (#1227); scores and hours unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; Localization: Korean catalog added, 54% → 63%, full readiness unchanged at ~62%; desktop print transport adopts craft-print, Windows 7 fallback retained, scores unchanged; Image Trace: an image over 64 megapixels traces scaled down instead of being refused, scores unchanged; Czech render-site localization follow-up; scores and estimates unchanged; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; Path operations: Simplify preview/counts and component selection (#1272); scores unchanged; G2: Film Grain added; Photoshop effects 29/57; 28 filters remain; Platforms: RPM runtime capabilities and glibc floor (#1275); scores unchanged; Raster effects: squared-distance texture redraws (#1257); pixels unchanged; scores and hours unchanged; Drawing: Curvature point display improved (#1236, in part); scores unchanged; Type core: full-name font matching (#1258), scores and hours unchanged; Selection: no-op anchor Align preserves history and saved state (#1180 follow-up); scores unchanged; UI chrome: Eyedropper cursor and precise-setting regressions (#1269); scores unchanged; Colour: Eyedropper reads visible locked and outside-isolation sources (#1251); scores and hours unchanged; Selection: segment click/Delete preserves neighboring edges (#1278); scores unchanged; Colour: unpainted zero-width strokes regain 1 pt when painted (#1253); scores and hours unchanged; graphs: designs and markers, data, Graph Type and label options; UI chrome: native macOS save Format menu, #866, scores unchanged; compound paths inherit the backmost appearance and member fill-rule edits reach their owner, #1197; scores unchanged; Type core: Middle Eastern digit types done, Character panel menu › Digits; PDF export warns when the pixel cap lowers raster-effect resolution; Drawing tools: Live Corners round corners between curved sides, #1144; Type advanced: proportional vertical metrics (`vpal`) done, #966; Shape Builder: #893 fixed, merged regions of crossing lines no longer keep seams; scores unchanged while #937 is open; #1034: document-point character controls after object/group scaling; full readiness recomputed as the standard's additive weighted sum, ~62%; per-audience hours; essentials-user score ~75% added; ready-for-real-work re-examined: 50% → 55%, mainstream 65% added; earlier the same day: full re-measure against Illustrator 2026 30.x; merged the ROADMAP's parity estimate, honest assessment and "Shipped so far" into this file; SVG knockout groups of opaque art export without masks; Image Trace: colour presets no longer shatter noisy flat art and photos trace into smooth tones, measured in `image-trace-quality.md`, score unchanged; Split Into Grid options, #1462; Transform Objects and Reflect across an angle, #1448 and #1460; Pathfinder panel on a group, #1450; unknown parameter keys refused, #1442; grayscale TIFFs with alpha, #1466 in part; scores and hours unchanged; Image Trace places edges to a fraction of a pixel in every mode, measured in `image-trace-quality.md`, score unchanged; 3D and Materials: Extrude & Bevel, Inflate and Rotate added, 3D ~8% → ~25% (estimated), menu stubs 25 → 22; Affinity text frames too short for their first line show it, #1423; Transform dialogs preview, #1412, and the transform tools and commands act on direct-selected anchors, #1408; `.ai` compound path paint, #1410; strokes wider than their shape, #1396; corner drags hide the bounding box, #1229; scores and hours unchanged; PDF and `.ai` import keep overprint, #1388, scores unchanged; `.ai` editing-data follow-ups: gradient stops, type of objects with an appearance, short area-type frames (#1390, #1391, #1392); dialog keyboard focus on every open (#1382, #1395); art flush with an artboard (#1375); font rescan (#1376); symbol instance redraw cache (#1386); scores and hours unchanged; grid tool options, #1377 in part, scores unchanged; `.ai` placed (linked) files read from the editing data, #1264, scores unchanged; gradient shapes import as paths, #1304; white constant alpha masks, #1286 in part; Live Corners widget click selects its corner, #1239; scores and hours unchanged; custom ruler origin, #1212; Artboard click-to-fit verified for objects, groups, visible bounds, undo and cancellation (#1227); scores and hours unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; Localization: Korean catalog added, 54% → 63%, full readiness unchanged at ~62%; desktop print transport adopts craft-print, Windows 7 fallback retained, scores unchanged; Image Trace: an image over 64 megapixels traces scaled down instead of being refused, scores unchanged; Czech render-site localization follow-up; scores and estimates unchanged; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; Path operations: Simplify preview/counts and component selection (#1272); scores unchanged; G2: Film Grain added; Photoshop effects 29/57; 28 filters remain; Platforms: RPM runtime capabilities and glibc floor (#1275); scores unchanged; Raster effects: squared-distance texture redraws (#1257); pixels unchanged; scores and hours unchanged; Drawing: Curvature point display improved (#1236, in part); scores unchanged; Type core: full-name font matching (#1258), scores and hours unchanged; Selection: no-op anchor Align preserves history and saved state (#1180 follow-up); scores unchanged; UI chrome: Eyedropper cursor and precise-setting regressions (#1269); scores unchanged; Colour: Eyedropper reads visible locked and outside-isolation sources (#1251); scores and hours unchanged; Selection: segment click/Delete preserves neighboring edges (#1278); scores unchanged; Colour: unpainted zero-width strokes regain 1 pt when painted (#1253); scores and hours unchanged; graphs: designs and markers, data, Graph Type and label options; UI chrome: native macOS save Format menu, #866, scores unchanged; compound paths inherit the backmost appearance and member fill-rule edits reach their owner, #1197; scores unchanged; Type core: Middle Eastern digit types done, Character panel menu › Digits; PDF export warns when the pixel cap lowers raster-effect resolution; Drawing tools: Live Corners round corners between curved sides, #1144; Type advanced: proportional vertical metrics (`vpal`) done, #966; Shape Builder: #893 fixed, merged regions of crossing lines no longer keep seams; scores unchanged while #937 is open; #1034: document-point character controls after object/group scaling; full readiness recomputed as the standard's additive weighted sum, ~62%; per-audience hours; essentials-user score ~75% added; ready-for-real-work re-examined: 50% → 55%, mainstream 65% added; earlier the same day: full re-measure against Illustrator 2026 30.x; merged the ROADMAP's parity estimate, honest assessment and "Shipped so far" into this file; SVG knockout groups of opaque art export without masks) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026) The authoritative parity assessment. [`ROADMAP.md`](../ROADMAP.md) summarizes it; [`gaps.md`](gaps.md) is @@ -348,7 +348,7 @@ about 1–1.5 agent-hours each, so the other rows (estimated on the older scale) | Automation | 3 | 65% | scripting surface, batch (MCP drops malformed app replies without replay; Variables: text/visibility variables bound to objects, an object may carry several, dataset rows applied in one undo step, panel and Window › Variables with dialogs; image/graph kinds and dataset import/export open) | 6–10 | | UI chrome (panels, contextual Properties, workspaces, prefs) | 6 | 80% | macOS native menu dismissal fixed (M14.8); Variables, Links, Asset Export, CSS Properties, Attributes and the canvas context menu are done; remaining: SVG Interactivity, Properties per context, Consolidate All Windows, floating panels outside the app window and docked into any group, context menus beyond the canvas, about 13 Preferences options stored but not yet acted on (#394: Open Documents As Tabs, Real-time Drawing, hyphenation language, devices…) | 13–20 | | UI chrome (panels, contextual Properties, workspaces, prefs) | 6 | 80% | macOS native menu dismissal fixed (M14.8); Variables, Links, Asset Export, CSS Properties, Attributes and the canvas context menu are done; remaining: SVG Interactivity, Properties per context, Consolidate All Windows, floating panels outside the app window and docked into any group, context menus beyond the canvas, about 14 Preferences options stored but not yet acted on (#394: Highlight Alternate Glyphs, Open Documents As Tabs, Real-time Drawing, hyphenation language, devices…) | 13–20 | -| Libraries, Links, Package | 2 | 82% | the Libraries panel is local only (no cloud by design: libraries are files on this machine) and has no shared libraries, linked graphics or gradients and patterns as items yet; its graphics, colours and text styles (#745) and its search, groups and library export/import as `.vclibrary` files (#926) are done; Links and Package are done; placed VectorCraft documents are done (linked by default, vectors in every output, previews, Break Link, Expand, Edit Original, refresh on save) | 2–3 | +| Libraries, Links, Package | 2 | 82% | the Libraries panel is local only (no cloud by design: libraries are files on this machine) and has no shared libraries, linked graphics or gradients and patterns as items yet; its graphics, colours and text styles (#745), its search, groups and library export/import as `.vclibrary` files (#926), and a list that scrolls, with items and groups reordered by dragging and items dragged into and out of groups, are done; Links and Package are done; placed VectorCraft documents are done (linked by default, vectors in every output, previews, Break Link, Expand, Edit Original, refresh on save) | 2–3 | | **Feature parity** | **103** | **~75%** | | **~210–330** | | Interaction-fidelity pass (side by side with Illustrator: every tool modifier, cursor, dialog, Properties context) | | | | 60–90 | | Hardening (big-file corpus, fuzzing, cross-platform and browser QA, accessibility, packaging) | | | Bounded control socket waits (#1726); PngSuite pinned-file integrity and download budgets verified (#1729); RPM runtime capabilities and glibc floor checked (#1275); real-distro installation/start-up QA pending | 50–80 | @@ -841,6 +841,7 @@ Evidence for the shared light Color: geometry tests in `tests_lighting`, effect | Date | Change | Summary | |---|---|---| +| 2026-10-19 | minor | Libraries panel: the list fills the panel and scrolls (also while a drag nears its edges); items drag before or after others of their kind and into or out of groups, and groups drag by their headers (`library.moveItem` `before`/`after`, `library.moveGroup`). Scores and estimates unchanged. | | 2026-10-19 | minor | Cache status-readout sample widths per UI, language, document unit and pixels per point; regressions check reuse across live frames and refresh after display-setting changes. Scores and estimates unchanged. | | 2026-10-19 | minor | Status messages and readouts use stable responsive allocations sized for localized labels and ordinary coordinates. Contributor original-file acceptance confirmed; scores and estimates unchanged. | | 2026-10-19 | minor | Cached canvas dimensions match the renderer limit before cache-key creation, view construction and worker submission. Regression coverage; scores and estimates unchanged. |