mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Merge pull request #1921 from sanyamsingh001/codex/r65-artboard-document-switch
G1: Keep the displayed artboard selected after switching documents
This commit is contained in:
@@ -483,7 +483,17 @@ pub fn doc_tabs(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
}
|
||||
} else if let Some(i) = activate {
|
||||
app.ui.home = None;
|
||||
app.session.set_active(i);
|
||||
let changed = app.session.active_index() != Some(i);
|
||||
if app.session.set_active(i)
|
||||
&& changed
|
||||
&& app.session.tool_id() == "artboard"
|
||||
&& let Some(n) = app.session.active().map(|d| d.doc.artboards.len())
|
||||
{
|
||||
// A document switch recreates the tool; restore the displayed board as its target.
|
||||
// Clicking the current tab keeps its existing multiselection and tool options.
|
||||
let selected = crate::panels::artboards::selected(app, n);
|
||||
crate::panels::artboards::select(app, selected);
|
||||
}
|
||||
}
|
||||
// Isolation mode breadcrumb bar.
|
||||
if let Some(st) = app.session.active()
|
||||
@@ -1559,6 +1569,315 @@ mod tests {
|
||||
super::sync_window_title(&mut app, &ctx);
|
||||
assert_eq!(app.last_window_title, "foo.svg \u{2014} VectorCraft");
|
||||
}
|
||||
/// Draw actual tabs, including the same per-frame view synchronization as the app.
|
||||
fn artboard_document_tabs_frame(app: &mut crate::VectorcraftApp, ctx: &egui::Context, events: Vec<egui::Event>) -> Vec<egui::Rect> {
|
||||
app.sync_views();
|
||||
let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1200.0, 700.0));
|
||||
let mut out = ctx.run_ui(egui::RawInput { screen_rect: Some(screen), events, ..Default::default() }, |ui| super::doc_tabs(app, ui));
|
||||
out.textures_delta.clear();
|
||||
let mut titles: Vec<_> = out
|
||||
.shapes
|
||||
.iter()
|
||||
.filter_map(|shape| match &shape.shape {
|
||||
egui::Shape::Text(t) if t.galley.text().contains(" @ ") => Some(t.visual_bounding_rect()),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
titles.sort_by(|a, b| a.left().total_cmp(&b.left()));
|
||||
titles
|
||||
}
|
||||
|
||||
fn artboard_document_click_tab(app: &mut crate::VectorcraftApp, ctx: &egui::Context, index: usize) {
|
||||
let titles = artboard_document_tabs_frame(app, ctx, vec![]);
|
||||
assert_eq!(titles.len(), 2, "both real document tabs must be visible");
|
||||
let at = titles[index].center();
|
||||
assert!(!app.canvas_rect.unwrap().contains(at));
|
||||
let button = |pressed| egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() };
|
||||
for event in [egui::Event::PointerMoved(at), button(true), button(false)] {
|
||||
artboard_document_tabs_frame(app, ctx, vec![event]);
|
||||
}
|
||||
assert_eq!(app.session.active_index(), Some(index), "the actual tab click must activate its document");
|
||||
}
|
||||
|
||||
fn artboard_document_app() -> (crate::VectorcraftApp, egui::Context) {
|
||||
let mut app = crate::VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 200, "height": 100, "artboards": 2})).unwrap();
|
||||
app.run("artboard.setProps", json!({"index": 0, "name": "Keep"})).unwrap();
|
||||
app.run("artboard.setProps", json!({"index": 1, "name": "Shown"})).unwrap();
|
||||
app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 30, "height": 20})).unwrap();
|
||||
app.run("symbol.new", json!({"name": "Tab symbol"})).unwrap();
|
||||
app.run("shape.rectangle", json!({"x": 230, "y": 10, "width": 30, "height": 20})).unwrap();
|
||||
app.run("guide.add", json!({"artboard": 0, "pos": 20})).unwrap();
|
||||
app.run("guide.add", json!({"artboard": 1, "pos": 250, "vertical": true})).unwrap();
|
||||
app.run("file.new", json!({"width": 100, "height": 80})).unwrap();
|
||||
// Neither tab nor Properties clicks enter the canvas; a canvas move could hide the bug.
|
||||
app.canvas_rect = Some(egui::Rect::from_min_max(egui::pos2(0.0, 40.0), egui::pos2(800.0, 260.0)));
|
||||
crate::i18n::set_current(crate::i18n::Lang::EN);
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
artboard_document_click_tab(&mut app, &ctx, 0);
|
||||
(app, ctx)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn artboard_document_tab_roundtrip_properties_delete_uses_shown_board() {
|
||||
let (mut app, ctx) = artboard_document_app();
|
||||
app.select_tool("artboard");
|
||||
app.run("tool.setOption", json!({"key": "moveArt", "value": false})).unwrap();
|
||||
app.run("tool.setOption", json!({"key": "scaleArt", "value": true})).unwrap();
|
||||
artboard_navigation_next(&mut app, &ctx);
|
||||
assert_eq!(app.view().unwrap().artboard, 1);
|
||||
assert_eq!(app.session.tool_options()["selected"], json!([1]), "start with the tool and navigator aligned");
|
||||
app.session.active_mut().unwrap().mark_saved();
|
||||
let before = app.session.active().unwrap().doc.clone();
|
||||
let selection = app.session.active().unwrap().selection.clone();
|
||||
let undo = app.session.active().unwrap().history.undo.len();
|
||||
let other = app.session.documents()[1].doc.clone();
|
||||
let other_history = (app.session.documents()[1].history.undo.len(), app.session.documents()[1].history.redo.len());
|
||||
|
||||
// No direct set_active, tool reselection, or selected-option repair after this round trip.
|
||||
artboard_document_click_tab(&mut app, &ctx, 1);
|
||||
artboard_document_click_tab(&mut app, &ctx, 0);
|
||||
assert_eq!(app.view().unwrap().artboard, 1, "the document keeps its displayed artboard");
|
||||
assert_eq!(app.session.active().unwrap().doc, before, "switching tabs is not a document edit");
|
||||
assert_eq!(app.session.active().unwrap().history.undo.len(), undo);
|
||||
assert!(!app.session.active().unwrap().is_dirty());
|
||||
let selected_before_delete = app.session.tool_options()["selected"].clone();
|
||||
let active_before_delete = app.session.tool_options()["active"].clone();
|
||||
assert_eq!(app.session.tool_options()["moveArt"], json!(false));
|
||||
assert_eq!(app.session.tool_options()["scaleArt"], json!(true));
|
||||
|
||||
artboard_navigation_properties_frame(&mut app, &ctx, vec![]);
|
||||
let texts = artboard_navigation_properties_frame(&mut app, &ctx, vec![]);
|
||||
assert!(texts.iter().any(|(label, _)| label == "Shown"), "Properties must display the second board before deletion");
|
||||
let at = texts.iter().find(|(label, _)| label == "Delete Artboard").map(|(_, r)| r.center()).expect("actual Properties Delete button");
|
||||
assert!(!app.canvas_rect.unwrap().contains(at));
|
||||
let button = |pressed| egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() };
|
||||
let commands = app.run_count;
|
||||
for event in [egui::Event::PointerMoved(at), button(true), button(false)] {
|
||||
artboard_navigation_properties_frame(&mut app, &ctx, vec![event]);
|
||||
}
|
||||
let st = app.session.active().unwrap();
|
||||
assert_eq!(
|
||||
st.doc.artboards.iter().map(|a| a.name.as_str()).collect::<Vec<_>>(),
|
||||
vec!["Keep"],
|
||||
"Properties Delete after a document tab round trip must remove the shown board"
|
||||
);
|
||||
assert_eq!(selected_before_delete, json!([1]));
|
||||
assert_eq!(active_before_delete, json!(1));
|
||||
assert_eq!(st.doc.artboards, vec![before.artboards[0].clone()], "the original first board survives");
|
||||
assert_eq!(st.doc.guides, vec![before.guides[0].clone()], "only the deleted board's guide is removed");
|
||||
assert_eq!(st.doc.layers, before.layers, "artwork and symbol instances stay unchanged");
|
||||
assert_eq!(st.doc.symbols, before.symbols, "symbol definitions stay unchanged");
|
||||
assert_eq!(st.selection, selection);
|
||||
assert_eq!(app.run_count, commands.wrapping_add(1), "one command from the actual Properties control");
|
||||
assert_eq!(st.history.undo.len(), undo + 1);
|
||||
assert_eq!(st.history.undo.last().unwrap().label, "Delete Artboard");
|
||||
assert!(st.history.redo.is_empty());
|
||||
assert!(st.is_dirty());
|
||||
let deleted = st.doc.clone();
|
||||
app.run("edit.undo", json!({})).unwrap();
|
||||
assert_eq!(app.session.active().unwrap().doc, before, "one Undo restores the correct board, guides and art");
|
||||
assert!(!app.session.active().unwrap().is_dirty());
|
||||
app.run("edit.redo", json!({})).unwrap();
|
||||
assert_eq!(app.session.active().unwrap().doc, deleted);
|
||||
assert_eq!(app.session.documents()[1].doc, other);
|
||||
assert_eq!((app.session.documents()[1].history.undo.len(), app.session.documents()[1].history.redo.len()), other_history);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn artboard_document_tab_same_document_preserves_multiselection_and_options() {
|
||||
let (mut app, ctx) = artboard_document_app();
|
||||
app.select_tool("artboard");
|
||||
artboard_navigation_next(&mut app, &ctx);
|
||||
app.run("tool.setOption", json!({"key": "selected", "value": [1, 0]})).unwrap();
|
||||
app.run("tool.setOption", json!({"key": "moveArt", "value": false})).unwrap();
|
||||
app.run("tool.setOption", json!({"key": "scaleArt", "value": true})).unwrap();
|
||||
let options = app.session.tool_options();
|
||||
let before = app.session.active().unwrap().doc.clone();
|
||||
let history = (app.session.active().unwrap().history.undo.len(), app.session.active().unwrap().history.redo.len());
|
||||
artboard_document_click_tab(&mut app, &ctx, 0);
|
||||
for _ in 0..3 {
|
||||
artboard_document_tabs_frame(&mut app, &ctx, vec![]);
|
||||
}
|
||||
assert_eq!(app.session.tool_options(), options, "an already-active tab must not collapse valid multiselection or options");
|
||||
assert_eq!(app.view().unwrap().artboard, 1);
|
||||
assert_eq!(app.session.active().unwrap().doc, before);
|
||||
assert_eq!((app.session.active().unwrap().history.undo.len(), app.session.active().unwrap().history.redo.len()), history);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn artboard_document_tab_roundtrip_preserves_other_tools() {
|
||||
for tool in ["selection", "rectangle"] {
|
||||
let (mut app, ctx) = artboard_document_app();
|
||||
app.select_tool(tool);
|
||||
artboard_navigation_next(&mut app, &ctx);
|
||||
let before = app.session.active().unwrap().doc.clone();
|
||||
let selection = app.session.active().unwrap().selection.clone();
|
||||
let options = app.session.tool_options();
|
||||
let history = (app.session.active().unwrap().history.undo.len(), app.session.active().unwrap().history.redo.len());
|
||||
artboard_document_click_tab(&mut app, &ctx, 1);
|
||||
artboard_document_click_tab(&mut app, &ctx, 0);
|
||||
assert_eq!(app.session.tool_id(), tool);
|
||||
assert_eq!(app.session.tool_options(), options);
|
||||
assert_eq!(app.view().unwrap().artboard, 1);
|
||||
assert_eq!(app.session.active().unwrap().doc, before);
|
||||
assert_eq!(app.session.active().unwrap().selection, selection);
|
||||
assert_eq!((app.session.active().unwrap().history.undo.len(), app.session.active().unwrap().history.redo.len()), history);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn artboard_close_active_tab_properties_delete_uses_shown_board() {
|
||||
let (mut app, ctx) = artboard_document_app();
|
||||
let history = |st: &vectorcraft_engine::DocState| {
|
||||
let entries = |items: &[vectorcraft_engine::HistoryEntry]| {
|
||||
items.iter().map(|e| (e.label.clone(), e.doc.clone(), e.selection.clone())).collect::<Vec<_>>()
|
||||
};
|
||||
(entries(&st.history.undo), entries(&st.history.redo))
|
||||
};
|
||||
let outside_canvas = |app: &crate::VectorcraftApp| {
|
||||
let p = ctx.input(|i| i.pointer.hover_pos()).expect("a real chrome or panel pointer position");
|
||||
assert!(!app.canvas_rect.unwrap().contains(p), "the pointer must stay outside the canvas: {p:?}");
|
||||
};
|
||||
app.select_tool("artboard");
|
||||
app.run("tool.setOption", json!({"key": "moveArt", "value": false})).unwrap();
|
||||
app.run("tool.setOption", json!({"key": "scaleArt", "value": true})).unwrap();
|
||||
artboard_navigation_next(&mut app, &ctx);
|
||||
outside_canvas(&app);
|
||||
assert_eq!(app.view().unwrap().artboard, 1);
|
||||
assert_eq!(app.session.tool_options()["selected"], json!([1]), "the initial navigator action targets Shown");
|
||||
app.session.active_mut().unwrap().mark_saved();
|
||||
let st = app.session.active().unwrap();
|
||||
let (uid, revision) = (st.uid, st.revision);
|
||||
let before = st.doc.clone();
|
||||
let selection = st.selection.clone();
|
||||
let history_before = history(st);
|
||||
let history_limits = (st.history.limit, st.history.byte_budget);
|
||||
assert_eq!(before.artboards.iter().map(|a| a.name.as_str()).collect::<Vec<_>>(), vec!["Keep", "Shown"]);
|
||||
assert_eq!(before.guides.len(), 2);
|
||||
assert!(!selection.objects.is_empty());
|
||||
assert!(history_before.1.is_empty());
|
||||
assert!(!st.is_dirty());
|
||||
let mut expected = before.as_ref().clone();
|
||||
expected.artboards = vec![before.artboards[0].clone()];
|
||||
expected.guides = vec![before.guides[0].clone()];
|
||||
|
||||
// Route from the status bar along the panel edge to the tab strip, never through canvas.
|
||||
for at in [egui::pos2(1000.0, 280.0), egui::pos2(1000.0, 20.0)] {
|
||||
artboard_document_tabs_frame(&mut app, &ctx, vec![egui::Event::PointerMoved(at)]);
|
||||
outside_canvas(&app);
|
||||
}
|
||||
artboard_document_click_tab(&mut app, &ctx, 1);
|
||||
outside_canvas(&app);
|
||||
app.session.active_mut().unwrap().mark_saved();
|
||||
let other_uid = app.session.active().unwrap().uid;
|
||||
assert_ne!(other_uid, uid);
|
||||
assert!(app.session.documents().iter().all(|d| !d.is_dirty()), "both ordinary documents are in saved state");
|
||||
assert!(app.ui.dialog.is_none());
|
||||
eprintln!("ARTBOARD_CLOSE_TARGET_SETUP_READY");
|
||||
|
||||
// The actual B tab x, using the existing close-box test's title-relative hit position.
|
||||
let titles = artboard_document_tabs_frame(&mut app, &ctx, vec![]);
|
||||
outside_canvas(&app);
|
||||
assert_eq!(titles.len(), 2);
|
||||
let title = titles[1];
|
||||
let x = if super::CLOSE_BEFORE_TITLE { title.left() - 16.0 } else { title.right() + 20.0 };
|
||||
let at = egui::pos2(x, title.center().y);
|
||||
assert!(!app.canvas_rect.unwrap().contains(at));
|
||||
let button = |pressed| egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() };
|
||||
for event in [egui::Event::PointerMoved(at), button(true), button(false)] {
|
||||
artboard_document_tabs_frame(&mut app, &ctx, vec![event]);
|
||||
outside_canvas(&app);
|
||||
}
|
||||
assert_eq!(app.session.documents().len(), 1, "B tab x must really close B");
|
||||
assert_eq!(app.session.active_index(), Some(0));
|
||||
assert_eq!(app.session.active().unwrap().uid, uid, "the survivor is the original A, not a replacement document");
|
||||
assert!(app.session.documents().iter().all(|d| d.uid != other_uid));
|
||||
assert!(app.ui.dialog.is_none(), "a clean close must not leave a save/discard modal");
|
||||
for _ in 0..2 {
|
||||
artboard_document_tabs_frame(&mut app, &ctx, vec![]);
|
||||
outside_canvas(&app);
|
||||
}
|
||||
let st = app.session.active().unwrap();
|
||||
assert_eq!(st.doc, before, "closing B must not edit A");
|
||||
assert_eq!(st.selection, selection);
|
||||
assert_eq!(history(st), history_before, "closing B preserves all A Undo/Redo entries");
|
||||
assert_eq!(st.revision, revision);
|
||||
assert_eq!((st.history.limit, st.history.byte_budget), history_limits);
|
||||
assert!(!st.is_dirty());
|
||||
assert_eq!(app.view().unwrap().artboard, 1);
|
||||
eprintln!("ARTBOARD_CLOSE_TARGET_CLOSED_B");
|
||||
|
||||
// Cross the tab strip to the Properties panel before moving down to its Delete control.
|
||||
app.sync_views();
|
||||
artboard_navigation_properties_frame(&mut app, &ctx, vec![egui::Event::PointerMoved(egui::pos2(1000.0, 20.0))]);
|
||||
outside_canvas(&app);
|
||||
app.sync_views();
|
||||
let texts = artboard_navigation_properties_frame(&mut app, &ctx, vec![]);
|
||||
outside_canvas(&app);
|
||||
assert!(texts.iter().any(|(label, _)| label == "Shown"), "actual Properties must display Shown before deletion");
|
||||
assert_eq!(app.view().unwrap().artboard, 1);
|
||||
let tool_before_delete = app.session.tool_options();
|
||||
eprintln!("ARTBOARD_CLOSE_TARGET_SHOWN_DISPLAYED tool={tool_before_delete}");
|
||||
let at = texts.iter().find(|(label, _)| label == "Delete Artboard").map(|(_, r)| r.center()).expect("actual Properties Delete button");
|
||||
assert!(at.x > app.canvas_rect.unwrap().right(), "Delete is in the right panel");
|
||||
let button = |pressed| egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() };
|
||||
let commands = app.run_count;
|
||||
for event in [egui::Event::PointerMoved(at), button(true), button(false)] {
|
||||
app.sync_views();
|
||||
artboard_navigation_properties_frame(&mut app, &ctx, vec![event]);
|
||||
outside_canvas(&app);
|
||||
}
|
||||
let st = app.session.active().unwrap();
|
||||
assert_eq!(app.run_count, commands.wrapping_add(1), "the actual Properties click executes one command");
|
||||
assert_eq!(st.doc.artboards.len(), 1, "the real Delete action removed one board");
|
||||
assert_eq!(st.history.undo.len(), history_before.0.len() + 1);
|
||||
assert_eq!(st.history.undo.last().unwrap().label, "Delete Artboard");
|
||||
eprintln!("ARTBOARD_CLOSE_TARGET_DELETE_CLICKED tool_before={tool_before_delete}");
|
||||
// Do not assert tool alignment before this destructive-action oracle: it must expose the wrong deletion.
|
||||
assert_eq!(
|
||||
st.doc.artboards.iter().map(|a| a.name.as_str()).collect::<Vec<_>>(),
|
||||
vec!["Keep"],
|
||||
"ARTBOARD_CLOSE_TARGET_WRONG_SURVIVOR: Properties showing Shown must delete Shown after closing B"
|
||||
);
|
||||
assert_eq!(st.doc.as_ref(), &expected, "only Shown and its attached guide may be removed from the full document");
|
||||
assert_eq!(st.doc.layers, before.layers, "all artwork and symbol instances stay unchanged");
|
||||
assert_eq!(st.doc.symbols, before.symbols);
|
||||
assert_eq!(st.doc.guides, vec![before.guides[0].clone()]);
|
||||
assert_eq!(st.selection, selection);
|
||||
assert_eq!(st.uid, uid);
|
||||
assert_eq!(st.revision, revision + 1);
|
||||
let history_after = history(st);
|
||||
assert_eq!(&history_after.0[..history_before.0.len()], history_before.0.as_slice());
|
||||
let entry = history_after.0.last().unwrap();
|
||||
assert_eq!(entry.1, before);
|
||||
assert_eq!(entry.2.as_ref(), &selection);
|
||||
assert!(history_after.1.is_empty());
|
||||
assert!(st.is_dirty());
|
||||
let deleted = st.doc.clone();
|
||||
app.run("edit.undo", json!({})).unwrap();
|
||||
let st = app.session.active().unwrap();
|
||||
assert_eq!(st.uid, uid);
|
||||
assert_eq!(st.doc, before, "one Undo restores the full saved A including both boards, guides and artwork");
|
||||
assert_eq!(st.selection, selection);
|
||||
assert_eq!(history(st).0, history_before.0);
|
||||
assert_eq!(st.history.redo.len(), 1);
|
||||
assert_eq!(st.history.redo[0].label, "Delete Artboard");
|
||||
assert_eq!(st.history.redo[0].doc, deleted);
|
||||
assert_eq!(st.history.redo[0].selection.as_ref(), &selection);
|
||||
assert!(!st.is_dirty());
|
||||
app.run("edit.redo", json!({})).unwrap();
|
||||
let st = app.session.active().unwrap();
|
||||
assert_eq!(st.uid, uid);
|
||||
assert_eq!(st.doc.as_ref(), &expected, "one Redo repeats only the correct deletion");
|
||||
assert_eq!(st.selection, selection);
|
||||
assert_eq!(history(st), history_after);
|
||||
assert_eq!((st.history.limit, st.history.byte_budget), history_limits);
|
||||
assert!(st.is_dirty());
|
||||
assert_eq!(app.session.documents().len(), 1, "Undo/Redo do not reopen B");
|
||||
eprintln!("ARTBOARD_CLOSE_TARGET_MODEL_AND_HISTORY_PRESERVED");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -76,6 +76,13 @@ fn close_now(app: &mut VectorcraftApp, i: usize) -> Result<Value, String> {
|
||||
app.views.remove(i);
|
||||
}
|
||||
app.sync_views();
|
||||
if app.session.tool_id() == "artboard"
|
||||
&& let Some(n) = app.session.active().map(|d| d.doc.artboards.len())
|
||||
{
|
||||
// Closing recreates the tool; restore the surviving document's displayed board.
|
||||
let selected = crate::panels::artboards::selected(app, n);
|
||||
crate::panels::artboards::select(app, selected);
|
||||
}
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
|
||||
+4
-2
File diff suppressed because one or more lines are too long
+5
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+4
-2
@@ -1,6 +1,6 @@
|
||||
# UI and interaction parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-14 · **Change:** minor (Curvature curve geometry, #1236 in part; Width-point finite-arithmetic follow-up validated; scores and hours unchanged; Image Trace adjustment discovery; later 2026-10-14 merges: macOS save sheet Format menu back to missing, #1855, #866 reopened; anchor X/Y/W/H in the Transform panel, Properties and Control bar, #1824; Snap to Grid over Snap to Pixel, #1816; toolbar labels, columns and the All Tools drawer, #1751; Actions panel selection, #1629 in part; scores unchanged; 2026-10-14 merges: anchor Align and Distribute, #1636; Delete on closed paths and whole objects, #1674; selection inside isolation, #1666; Escape in a Direct Selection drag and while typing, #1743, #1686; Pen clicks under 3 px, #1663; [ and ] on more tools, #1661 in part; fields commit on click away, #1787; command search keys, #1757; a guide's position in the Transform panel is a convenience with no parity claim, #1659; scores unchanged; intersect Smart Guides for drawn points and dragged anchors, #1602 in part; a ruler guide's position in the Transform panel, #1597 in part; Symbol Options before New Symbol, #1560; tool flyout widths, #1616; Pathfinder Trim, Merge and Crop strokes, #1570; scores unchanged; G1/M11: Artboard deletion and history selection normalization; history regressions and full workspace CI pass; scores and hours unchanged; Width Point Edit: Shift-double-click enables adjoining adjustment; plain double-click keeps it off; scores and hours unchanged; per-corner radius fields with a link toggle, split from #1594; Join merges anchors lying on one another, #1533; correction: Average and Join with a corner or smooth choice is missing, not done; guide objects take the guide colour and style and Lock Guides, #1534; Keyboard Shortcuts lists commands outside the menus, #1539; round corners next to curves and the Smooth tool's reach, #1512; Make Compound Path on a group, #1537; dialog slider rails, #1483; template layers stay in Preview in Outline view, #1486; Direct Selection takes the selected object's anchor where anchors coincide, #1508; Tabs panel stop drags, #1458; Split Into Grid dialog, #1462; Transform Objects and Reflect's angle, #1448 and #1460; Pathfinder panel on a group, #1450; value fields: readable selection, #1465 in part, and a focused field follows its value, #1457; scores unchanged; Transform dialogs have a live Preview and step with the arrow keys, #1412; transform tools, dialogs and Move act on direct-selected anchors, #1408; the bounding box hides during a corner drag, #1229; scores unchanged; dialogs focus their first field on every open, #1367, and a first field that starts greyed out once it is enabled, #1395; scores unchanged; Rectangular Grid and Polar Grid click dialogs take the skew, frame, compound path and Fill Grid options, #1377 in part, score unchanged; Live Corners: a Direct Selection click on a widget selects its corner, #1239; custom ruler origin, #1212; Artboard tool clicks fit the topmost editable object in one undo step (#1227); scores unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; DXF control-point SPLINE closure read, #1299 follow-up; 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; Simplify preview/counts and valid rewritten-path selection (#1272); parity remains partial; Texture-effect drag redraws optimized (#1257); idle Windows interaction still unverified; scores unchanged; Curvature point display improved (#1236, in part); scores unchanged; Direct Selection: no-op anchor Align preserves Undo, Redo and saved state (#1180 follow-up); Eyedropper cursor distinguished from precise crosshair (#1269); scores unchanged; Eyedropper can read visible locked and outside-isolation sources (#1251); scores unchanged; Direct Selection: independent segment picks and Delete (#1278); scores unchanged; Adding paint to an unpainted zero-width stroke restores the 1 pt default (#1253); scores unchanged; macOS save sheets offer a Format menu, #866; Attributes fill-rule controls edit the compound when a member is selected, #1197; a marquee needs to touch a path, not just lie inside its fill, #1103; earlier: Live Corners round corners between curved sides, #1144) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-14 · **Change:** minor (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; Curvature curve geometry, #1236 in part; Width-point finite-arithmetic follow-up validated; scores and hours unchanged; Image Trace adjustment discovery; later 2026-10-14 merges: macOS save sheet Format menu back to missing, #1855, #866 reopened; anchor X/Y/W/H in the Transform panel, Properties and Control bar, #1824; Snap to Grid over Snap to Pixel, #1816; toolbar labels, columns and the All Tools drawer, #1751; Actions panel selection, #1629 in part; scores unchanged; 2026-10-14 merges: anchor Align and Distribute, #1636; Delete on closed paths and whole objects, #1674; selection inside isolation, #1666; Escape in a Direct Selection drag and while typing, #1743, #1686; Pen clicks under 3 px, #1663; [ and ] on more tools, #1661 in part; fields commit on click away, #1787; command search keys, #1757; a guide's position in the Transform panel is a convenience with no parity claim, #1659; scores unchanged; intersect Smart Guides for drawn points and dragged anchors, #1602 in part; a ruler guide's position in the Transform panel, #1597 in part; Symbol Options before New Symbol, #1560; tool flyout widths, #1616; Pathfinder Trim, Merge and Crop strokes, #1570; scores unchanged; G1/M11: Artboard deletion and history selection normalization; history regressions and full workspace CI pass; scores and hours unchanged; Width Point Edit: Shift-double-click enables adjoining adjustment; plain double-click keeps it off; scores and hours unchanged; per-corner radius fields with a link toggle, split from #1594; Join merges anchors lying on one another, #1533; correction: Average and Join with a corner or smooth choice is missing, not done; guide objects take the guide colour and style and Lock Guides, #1534; Keyboard Shortcuts lists commands outside the menus, #1539; round corners next to curves and the Smooth tool's reach, #1512; Make Compound Path on a group, #1537; dialog slider rails, #1483; template layers stay in Preview in Outline view, #1486; Direct Selection takes the selected object's anchor where anchors coincide, #1508; Tabs panel stop drags, #1458; Split Into Grid dialog, #1462; Transform Objects and Reflect's angle, #1448 and #1460; Pathfinder panel on a group, #1450; value fields: readable selection, #1465 in part, and a focused field follows its value, #1457; scores unchanged; Transform dialogs have a live Preview and step with the arrow keys, #1412; transform tools, dialogs and Move act on direct-selected anchors, #1408; the bounding box hides during a corner drag, #1229; scores unchanged; dialogs focus their first field on every open, #1367, and a first field that starts greyed out once it is enabled, #1395; scores unchanged; Rectangular Grid and Polar Grid click dialogs take the skew, frame, compound path and Fill Grid options, #1377 in part, score unchanged; Live Corners: a Direct Selection click on a widget selects its corner, #1239; custom ruler origin, #1212; Artboard tool clicks fit the topmost editable object in one undo step (#1227); scores unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; DXF control-point SPLINE closure read, #1299 follow-up; 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; Simplify preview/counts and valid rewritten-path selection (#1272); parity remains partial; Texture-effect drag redraws optimized (#1257); idle Windows interaction still unverified; scores unchanged; Curvature point display improved (#1236, in part); scores unchanged; Direct Selection: no-op anchor Align preserves Undo, Redo and saved state (#1180 follow-up); Eyedropper cursor distinguished from precise crosshair (#1269); scores unchanged; Eyedropper can read visible locked and outside-isolation sources (#1251); scores unchanged; Direct Selection: independent segment picks and Delete (#1278); scores unchanged; Adding paint to an unpainted zero-width stroke restores the 1 pt default (#1253); scores unchanged; macOS save sheets offer a Format menu, #866; Attributes fill-rule controls edit the compound when a member is selected, #1197; a marquee needs to touch a path, not just lie inside its fill, #1103; earlier: Live Corners round corners between curved sides, #1144) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
|
||||
How VectorCraft's tools, handles, snapping and panels behave next to Illustrator's. A vector illustrator's
|
||||
trust is in the details: where a handle lands, what Shift and Alt do mid-drag, what a smart guide snaps to,
|
||||
@@ -159,7 +159,7 @@ tools, never checked side by side) isn't captured row by row.
|
||||
| Escape while typing ends the edit, keeps the type selected and switches to the Selection tool | done | #1802 (#1686, #1656) |
|
||||
| Shift-click selects several artboards (Artboard tool, Artboards panel); they move, Alt-copy and delete together | done | `artboard.move`/`artboard.delete` `indices` (#1139); panel menu, trash and Properties share selected-set deletion; all-selected retains artboard 1 and a single board cannot be deleted, with pointer-event and Undo/Redo coverage |
|
||||
| Artboard Delete/Backspace delete selected boards while protecting artwork, the last board and active drags | done | Headless shortcut regressions in `tests_distortkeys.rs` cover retaining the original first board when all are selected, single-board protection, artwork, saved state and Undo/Redo; the navigator follows the tool’s surviving board through repeated deletion and history changes. |
|
||||
| Artboard navigator synchronizes the active tool selection before deletion | done | Actual status-bar Next → Delete/Backspace and Next → Properties Delete regressions in `chrome.rs` cover surviving board records, preserved artwork and Undo/Redo; other tools keep object selection and history. |
|
||||
| Artboard navigator synchronizes the active tool selection before deletion | done | Actual status-bar Next → Delete/Backspace and Next → Properties Delete regressions in `chrome.rs` cover surviving board records, preserved artwork and Undo/Redo; other tools keep object selection and history. **Tab-switch follow-up:** document-tab changes align the Artboard tool with the displayed board. Actual A → B → A tab clicks followed by Properties Delete and controls for same-tab multiselection, Move/Scale Artwork options and other tools passed, along with historical full CI on `a0671777`. [Validation evidence](target-app-parity.md#evidence-what-ships-today-by-area). **Clean-tab close:** with the Artboard tool active, closing a clean active tab restores the surviving document's displayed board before Properties Delete. |
|
||||
| Artboard history prunes removed indices while preserving valid multiselection and tool options | tested | Headless pointer/key tests in `panels/properties.rs` cover panel New → Undo → Delete/Backspace/Properties, Duplicate → Undo → Delete, partial pruning and valid multiselection. They check surviving board records, artwork, object selection, Move/Scale Artwork options, saved state and Undo/Redo. Full workspace CI passes. |
|
||||
| Click an object with the Artboard tool to create an artboard fitted to its visible bounds | done | Topmost editable object or group; drag and modified clicks retain their meaning; one undo step (`artboard_click.rs`, #1227) |
|
||||
| Shape tools: Shift constrains, Alt from centre, Space moves while dragging, arrows change sides/points/rows | done | |
|
||||
@@ -227,6 +227,8 @@ re-measure of the panels added since. Slider rails show on Medium Dark (#1808, #
|
||||
| 2026-10-14 | minor | Curvature: the Curvature tool's curve: each stretch of three points follows the circle through them (handles along its tangent, chord/(3·cos²(α/2)) long, shorter in a turn tighter than 30°), and the ends of an open path curve in, so uneven spacing neither overshoots nor loops; the rubber band, dragged, inserted and converted points use the same rule; the Pencil's fitting unchanged (#1236, in part); scores unchanged (estimated) |
|
||||
| 2026-10-14 | minor | Quick Image Trace presets reveal the adjustment panel with Advanced open; Properties reopens it. Sliders keep their preset row until release so the re-trace is committed. UI regressions added; scores and remaining estimates unchanged. |
|
||||
| 2026-10-14 | minor | The macOS save sheet's Format menu goes back to missing: #1855 withdrew it to restore saving (#866 reopened). Rows added for the X/Y/W/H of direct-selected anchors (#1839, fixes #1824), the toolbar and All Tools drawer (#1751) and the Actions panel selection (#1853, part of #1629); the snapping row notes Snap to Grid over Snap to Pixel (#1840). Scores unchanged (estimated) |
|
||||
| 2026-10-15 | minor | Artboard tool: closing a clean active tab restores the displayed board before Properties Delete. Nine targeted checks passed; three original close-route captures reviewed; scores and hours unchanged. |
|
||||
| 2026-10-14 | minor | Artboard tab changes align the tool with the displayed board; same-tab multiselection and Move/Scale Artwork options preserved. Three tab regressions and historical full CI on `a0671777` passed; scores and hours unchanged. |
|
||||
| 2026-10-14 | minor | Width-point finite-arithmetic follow-up: atomicity, history and native round-trip regressions and current full CI passed; scores and hours unchanged. |
|
||||
| 2026-10-14 | minor | Align and Distribute act on the anchor points selected with Direct Selection: the points of one path align to each other, not to the artboard, and Distribute spaces them (#1641, fixes #1636; aligning to a key anchor is still missing); Delete removes every selected anchor of a closed path (it used to remove the wrong ones), removes objects selected whole along with the selected anchors of others, and keeps the remainder selected so a second Delete removes it (#1674); In isolation mode Direct Selection and Group Selection pick only inside the isolated object (#1666); Escape cancels a Direct Selection drag (#1803, #1743); Escape while typing ends the edit, keeps the type selected and switches to the Selection tool (#1802, #1686, #1656); A filled Pencil path keeps a stroke of None (#1804, part of #1679); With the Pen, a press that moves under 3 screen pixels is a click (a corner without handles), and a Precise Cursors command toggles the existing preference and can take a shortcut (#1663); The [ and ] keys reach the Blob Brush, the Eraser and the Symbolism tools in the app (#1734, part of #1661); In the Pathfinder panel a group selected alone counts its nested shapes, so an expanded stroked object keeps the Shape Modes enabled (#1642, #1635); Object › Expand turns radial, grid and mirror repeats into groups of their instances (#1765, #1756); Select › Same matches type on its characters' fill and stroke (#1746, #1737); Fields commit what was typed when you click away or move to another field, as Enter does; Escape still discards it (#1787); Up and Down move through the command search results and Enter runs the highlighted one (#1798, #1757); Slider rails show on Medium Dark (#1808, #1662); On Windows the custom title bar's window stays truly borderless, so the pointer lines up with what is drawn on systems where the hidden caption offset it (#1791, #1758; awaiting the reporter's confirmation); Tool cursors are painted into the window on Wayland (#1652; not run on Linux, #1645 stays open until confirmed); the guide-position row (#1659) is now "not scored": the documentation found doesn't establish it, and it is kept as a convenience pending the owner's decision; scores unchanged |
|
||||
| 2026-10-13 | minor | Pathfinder's Trim, Merge and Crop remove the objects' strokes, and Merge joins same-fill objects whatever their strokes were, as the public Pathfinder page describes (#1572, fixes #1570); Offset Path with an inset larger than the shapes says the offset leaves nothing, instead of asking to select paths (#1591, fixes #1590); The Symbols panel's New Symbol opens Symbol Options to name the symbol first, and Alt-click skips it (#1566, fixes #1560); Tool flyouts are as wide as their longest translated name and shortcut need, so names no longer run into shortcuts in Spanish and other languages (#1650, fixes #1616); Smart Guides: a point drawn with the Pen or a shape tool, and an anchor dragged with Direct Selection, snaps to where paths and ruler guides cross, labelled "intersect" (#1658, part of #1602; a moved selection's own anchors don't); The Transform panel shows a selected ruler guide's X or Y, and typing a value moves it (#1659, part of #1597). This is not established by the documentation we could find: the public help pages on guides, rulers and the Transform, Control and Properties panels (read again on 2026-10-14) describe selecting and dragging unlocked guides and the panels' X and Y for a selection, not a guide's position in a panel, and the one public statement of it is a user's [community answer](https://community.adobe.com/questions-652/adding-a-new-custom-guide-785586), which is not documentation. It is kept as a convenience pending the owner's decision, with no parity claim and nothing counted towards a score; Select Behind (Ctrl/Cmd-click) tests each object once, so a deep stack of overlapping objects no longer slows it; same objects in the same order (#1571); Commands that take `ids` run on the objects given with nothing selected, over MCP, the control channel and the CLI too (#1577, fixes #1575); scores unchanged (estimated) |
|
||||
|
||||
Reference in New Issue
Block a user