Merge pull request #1631 from sanyamsingh001/codex/r61-artboard-delete
G1: keep Artboard deletion consistent across controls
This commit is contained in:
commit
32950368b3
|
|
@ -459,10 +459,12 @@ fn artboard_tool_moves_with_art_creates_and_deletes() {
|
|||
assert!(s.tool_claims_key(ToolKey::Delete, ViewInfo::default()));
|
||||
s.tool_key(ToolKey::Delete, Mods::default(), ViewInfo::default()).unwrap();
|
||||
assert_eq!(s.doc().unwrap().doc.artboards.len(), 1);
|
||||
// The last artboard can't be deleted, so Delete stays the shortcut's.
|
||||
assert!(!s.tool_claims_key(ToolKey::Delete, ViewInfo::default()));
|
||||
s.tool_key(ToolKey::Delete, Mods::default(), ViewInfo::default()).unwrap();
|
||||
assert_eq!(s.doc().unwrap().doc.artboards.len(), 1);
|
||||
// The last artboard can't be deleted, but neither key may fall through to Clear.
|
||||
for key in [ToolKey::Delete, ToolKey::Backspace] {
|
||||
assert!(s.tool_claims_key(key, ViewInfo::default()));
|
||||
s.tool_key(key, Mods::default(), ViewInfo::default()).unwrap();
|
||||
assert_eq!(s.doc().unwrap().doc.artboards.len(), 1);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -372,23 +372,23 @@ impl Tool for ArtboardTool {
|
|||
|
||||
/// Escape owns the request to return to Selection (or cancel an active drag), so the UI
|
||||
/// applies the tool's request instead of falling through to its general Escape behaviour.
|
||||
/// Delete/Backspace remove the active artboard ahead of the Clear shortcut, which would only
|
||||
/// delete selected art; with one artboard left (it can't be deleted) they stay the shortcut's.
|
||||
fn claims_key(&self, cx: &ToolContext, key: ToolKey) -> bool {
|
||||
matches!(key, ToolKey::Escape)
|
||||
|| (matches!(key, ToolKey::Delete | ToolKey::Backspace)
|
||||
&& self.drag.is_none()
|
||||
&& cx.doc.artboards.len() > self.selected(cx).len()
|
||||
&& self.active < cx.doc.artboards.len())
|
||||
/// Delete/Backspace belong to the artboards even when deletion is protected or a drag is
|
||||
/// active: falling through to Clear would delete selected artwork instead.
|
||||
fn claims_key(&self, _cx: &ToolContext, key: ToolKey) -> bool {
|
||||
matches!(key, ToolKey::Escape | ToolKey::Delete | ToolKey::Backspace)
|
||||
}
|
||||
|
||||
fn key(&mut self, cx: &ToolContext, key: ToolKey, _mods: Mods) -> Vec<Action> {
|
||||
match key {
|
||||
ToolKey::Delete | ToolKey::Backspace => {
|
||||
if !self.claims_key(cx, key) {
|
||||
let mut sel = self.selected(cx);
|
||||
if self.drag.is_some() || cx.doc.artboards.len() <= 1 || self.active >= cx.doc.artboards.len() {
|
||||
return vec![];
|
||||
}
|
||||
let sel = self.selected(cx);
|
||||
// With every artboard selected, keep the first one in the current order.
|
||||
if sel.len() == cx.doc.artboards.len() {
|
||||
sel.retain(|i| *i != 0);
|
||||
}
|
||||
self.active = sel.first().copied().unwrap_or(0).saturating_sub(1);
|
||||
self.others.clear();
|
||||
let params = match sel.as_slice() {
|
||||
|
|
@ -648,10 +648,15 @@ mod tests {
|
|||
let v = preview_params(&t.pointer(&c, &ev(PointerKind::Drag, 470.0, 450.0)));
|
||||
assert_eq!((v["indices"].clone(), v["dx"].as_f64()), (json!([0, 1]), Some(20.0)));
|
||||
assert_eq!(t.pointer(&c, &ev(PointerKind::Up, 470.0, 450.0)), vec![Action::Commit]);
|
||||
// Delete removes both; with all three selected it can't, so the key isn't taken.
|
||||
// Delete removes both; with all three selected the keys retain the original first board.
|
||||
assert_eq!(t.key(&c, ToolKey::Delete, Mods::default()), vec![Action::Exec("artboard.delete".into(), json!({"indices": [0, 1]}))]);
|
||||
for key in [ToolKey::Delete, ToolKey::Backspace] {
|
||||
t.set_option("selected", &json!([2, 0, 1]));
|
||||
assert!(t.claims_key(&c, key));
|
||||
assert_eq!(t.key(&c, key, Mods::default()), vec![Action::Exec("artboard.delete".into(), json!({"indices": [1, 2]}))]);
|
||||
assert_eq!(t.options()["selected"], json!([0]));
|
||||
}
|
||||
t.set_option("selected", &json!([0, 1, 2]));
|
||||
assert!(!t.claims_key(&c, ToolKey::Delete));
|
||||
// Shift-click takes one out; a plain click on an unselected one selects just it.
|
||||
click(&mut t, 100.0, 700.0, shift);
|
||||
assert_eq!(t.options()["selected"], json!([0, 1]));
|
||||
|
|
|
|||
|
|
@ -976,6 +976,168 @@ mod tests {
|
|||
assert!(app.run("view.goToArtboard", json!({"index": "sideways"})).is_err());
|
||||
}
|
||||
|
||||
fn artboard_navigation_app(tool: &str) -> (crate::VectorcraftApp, vectorcraft_doc::NodeId) {
|
||||
let mut app = crate::VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 200, "height": 100, "artboards": 3})).unwrap();
|
||||
let id = app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 30, "height": 20})).unwrap()["id"].as_u64().unwrap();
|
||||
app.run("select.set", json!({"ids": [id]})).unwrap();
|
||||
app.canvas_rect = Some(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(460.0, 260.0)));
|
||||
app.select_tool(tool);
|
||||
crate::i18n::set_current(crate::i18n::Lang::EN);
|
||||
(app, vectorcraft_doc::NodeId(id))
|
||||
}
|
||||
|
||||
/// Click the actual status-bar Next control, without sending a canvas pointer event.
|
||||
fn artboard_navigation_next(app: &mut crate::VectorcraftApp, ctx: &egui::Context) {
|
||||
let buttons = navigator(app, ctx, vec![]);
|
||||
assert_eq!(buttons.len(), 4);
|
||||
let at = buttons[2].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)] {
|
||||
navigator(app, ctx, vec![event]);
|
||||
}
|
||||
}
|
||||
|
||||
fn artboard_navigation_properties_frame(
|
||||
app: &mut crate::VectorcraftApp,
|
||||
ctx: &egui::Context,
|
||||
events: Vec<egui::Event>,
|
||||
) -> Vec<(String, egui::Rect)> {
|
||||
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| {
|
||||
egui::Panel::right("navigation-properties").exact_size(300.0).show(ui, |ui| crate::panels::properties::show(app, ui));
|
||||
});
|
||||
out.textures_delta.clear();
|
||||
out.shapes
|
||||
.iter()
|
||||
.filter_map(|s| match &s.shape {
|
||||
egui::Shape::Text(t) => Some((t.galley.text().to_string(), egui::Rect::from_min_size(t.pos, t.galley.size()))),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// None clicks Properties' Delete Artboard; Some routes the real key through shortcuts.
|
||||
fn artboard_navigation_delete(key: Option<egui::Key>) {
|
||||
for multiple in [false, true] {
|
||||
let (mut app, id) = artboard_navigation_app("artboard");
|
||||
let selected = if multiple { json!([0, 2]) } else { json!([0]) };
|
||||
app.session.set_tool_option("selected", &selected);
|
||||
assert_eq!(app.view().unwrap().artboard, 0);
|
||||
assert_eq!(app.session.tool_options()["active"], json!(0));
|
||||
assert_eq!(app.session.tool_options()["selected"], selected);
|
||||
app.session.active_mut().unwrap().mark_saved();
|
||||
let st = app.session.active().unwrap();
|
||||
let before = st.doc.clone();
|
||||
let selection = st.selection.clone();
|
||||
let undo = st.history.undo.len();
|
||||
let ctx = egui::Context::default();
|
||||
|
||||
artboard_navigation_next(&mut app, &ctx);
|
||||
|
||||
assert_eq!(app.view().unwrap().artboard, 1, "Next shows board 2");
|
||||
assert_eq!(app.session.tool_options()["active"], json!(1), "Next must change the destructive target before deletion");
|
||||
assert_eq!(app.session.tool_options()["selected"], json!([1]), "navigation replaces the old artboard multiselection");
|
||||
assert_eq!(app.view().unwrap().center, before.artboards[1].rect.center());
|
||||
let st = app.session.active().unwrap();
|
||||
assert_eq!(st.doc, before, "navigation only changes view and tool state");
|
||||
assert_eq!(st.selection, selection, "ordinary artwork stays selected");
|
||||
assert_eq!(st.history.undo.len(), undo);
|
||||
assert!(st.history.redo.is_empty());
|
||||
assert!(!st.is_dirty());
|
||||
|
||||
if let Some(key) = key {
|
||||
let ev = egui::Event::Key { key, physical_key: None, pressed: true, repeat: false, modifiers: Default::default() };
|
||||
let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1200.0, 300.0));
|
||||
let mut out = ctx.run_ui(egui::RawInput { screen_rect: Some(screen), events: vec![ev], ..Default::default() }, |ui| {
|
||||
crate::shortcuts::handle(&mut app, ui.ctx());
|
||||
super::status_bar(&mut app, ui);
|
||||
});
|
||||
out.textures_delta.clear();
|
||||
} else {
|
||||
artboard_navigation_properties_frame(&mut app, &ctx, vec![]);
|
||||
let texts = artboard_navigation_properties_frame(&mut app, &ctx, vec![]);
|
||||
assert!(texts.iter().any(|(label, _)| label == &before.artboards[1].name), "Properties shows the navigated board");
|
||||
let at = texts.iter().find(|(label, _)| label == "Delete Artboard").map(|(_, rect)| rect.center()).expect("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() };
|
||||
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,
|
||||
vec![before.artboards[0].clone(), before.artboards[2].clone()],
|
||||
"first and third original board records survive"
|
||||
);
|
||||
assert_eq!(st.doc.node(id), before.node(id), "selected artwork survives unchanged");
|
||||
assert_eq!(st.selection, selection);
|
||||
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();
|
||||
let st = app.session.active().unwrap();
|
||||
assert_eq!(st.doc, before, "one Undo restores every board and the artwork");
|
||||
assert_eq!(st.selection, selection);
|
||||
assert_eq!((st.history.undo.len(), st.history.redo.len()), (undo, 1));
|
||||
assert!(!st.is_dirty());
|
||||
app.run("edit.redo", json!({})).unwrap();
|
||||
assert_eq!(app.session.active().unwrap().doc, deleted);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn artboard_navigation_next_then_delete_uses_shown_board() {
|
||||
artboard_navigation_delete(Some(egui::Key::Delete));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn artboard_navigation_next_then_backspace_uses_shown_board() {
|
||||
artboard_navigation_delete(Some(egui::Key::Backspace));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn artboard_navigation_next_then_properties_delete_uses_shown_board() {
|
||||
artboard_navigation_delete(None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn artboard_navigation_preserves_other_tools_selection_and_history() {
|
||||
for tool in ["selection", "rectangle"] {
|
||||
let (mut app, _) = artboard_navigation_app(tool);
|
||||
app.run("object.move", json!({"dx": 7, "dy": 9})).unwrap();
|
||||
let redo = app.session.active().unwrap().doc.clone();
|
||||
app.run("edit.undo", json!({})).unwrap();
|
||||
app.session.active_mut().unwrap().mark_saved();
|
||||
let st = app.session.active().unwrap();
|
||||
let before = st.doc.clone();
|
||||
let selection = st.selection.clone();
|
||||
let history = (st.history.undo.len(), st.history.redo.len());
|
||||
assert_eq!(history.1, 1);
|
||||
let options = app.session.tool_options();
|
||||
|
||||
artboard_navigation_next(&mut app, &egui::Context::default());
|
||||
|
||||
assert_eq!(app.view().unwrap().artboard, 1);
|
||||
assert_eq!(app.view().unwrap().center, before.artboards[1].rect.center());
|
||||
assert_eq!(app.session.tool_id(), tool);
|
||||
assert_eq!(app.session.tool_options(), options);
|
||||
let st = app.session.active().unwrap();
|
||||
assert_eq!(st.doc, before);
|
||||
assert_eq!(st.selection, selection);
|
||||
assert_eq!((st.history.undo.len(), st.history.redo.len()), history);
|
||||
assert!(!st.is_dirty());
|
||||
app.run("edit.redo", json!({})).unwrap();
|
||||
assert_eq!(app.session.active().unwrap().doc, redo, "navigation preserves the original Redo branch");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_tab_title_names_the_opacity_mask_while_it_is_edited() {
|
||||
let mut s = Session::new();
|
||||
|
|
|
|||
|
|
@ -562,6 +562,28 @@ impl VectorcraftApp {
|
|||
let docs = self.session.documents();
|
||||
self.views.truncate(docs.len());
|
||||
self.views.extend(docs.iter().skip(self.views.len()).map(View::of));
|
||||
for (view, doc) in self.views.iter_mut().zip(docs) {
|
||||
view.artboard = view.artboard.min(doc.doc.artboards.len().saturating_sub(1));
|
||||
}
|
||||
// Undo can remove the Artboard tool's active or additionally selected boards. Repair only
|
||||
// invalid indices: valid multiselection, drag state and tool options must survive history.
|
||||
if self.session.tool_id() != "artboard" || self.session.tool_busy() {
|
||||
return;
|
||||
}
|
||||
let Some(n) = self.session.active().map(|d| d.doc.artboards.len()).filter(|n| *n > 0) else { return };
|
||||
let Some(view) = self.session.active_index().and_then(|i| self.views.get_mut(i)) else { return };
|
||||
let options = self.session.tool_options();
|
||||
let Some(selected) = options["selected"].as_array() else { return };
|
||||
let mut valid: Vec<usize> = selected.iter().filter_map(Value::as_u64).filter_map(|i| usize::try_from(i).ok()).filter(|i| *i < n).collect();
|
||||
if valid.len() != selected.len() {
|
||||
if valid.is_empty() {
|
||||
valid.push(view.artboard);
|
||||
}
|
||||
if let Some(active) = valid.first() {
|
||||
view.artboard = *active;
|
||||
}
|
||||
self.session.set_tool_option("selected", &serde_json::json!(valid));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn view(&self) -> Option<&View> {
|
||||
|
|
|
|||
|
|
@ -1474,7 +1474,7 @@ pub fn run_ui_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<R
|
|||
}
|
||||
|
||||
/// `view.goToArtboard`: make artboard `index` (a number, or first / previous / next / last from
|
||||
/// the current one) the navigator's, and fit it in the window.
|
||||
/// the current one) active, synchronizing the Artboard tool when in use, and fit it in the window.
|
||||
fn go_to_artboard(app: &mut VectorcraftApp, p: &Value) -> Result<Value, String> {
|
||||
let n = app.session.active().map(|d| d.doc.artboards.len()).ok_or("no document open")?;
|
||||
let last = n.checked_sub(1).ok_or("the document has no artboards")?;
|
||||
|
|
@ -1491,9 +1491,7 @@ fn go_to_artboard(app: &mut VectorcraftApp, p: &Value) -> Result<Value, String>
|
|||
None => return Err("missing index".into()),
|
||||
}
|
||||
.min(last);
|
||||
if let Some(v) = app.view_mut() {
|
||||
v.artboard = to;
|
||||
}
|
||||
crate::panels::artboards::select(app, to);
|
||||
crate::canvas::fit(app, "view.fitArtboard");
|
||||
Ok(json!({ "index": to }))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,6 +60,19 @@ pub(crate) fn picked(app: &VectorcraftApp, n: usize) -> Vec<usize> {
|
|||
all
|
||||
}
|
||||
|
||||
/// Delete the picked artboards and select a remaining one.
|
||||
pub(crate) fn delete_selected(app: &mut VectorcraftApp) {
|
||||
let n = app.session.active().map_or(0, |d| d.doc.artboards.len());
|
||||
let mut all = picked(app, n);
|
||||
if n > 1 && all.len() == n {
|
||||
// Deleting all artboards keeps artboard 1 (the public Delete artboards guide).
|
||||
all.retain(|i| *i != 0);
|
||||
}
|
||||
if n > all.len() && app.run("artboard.delete", json!({ "indices": all })).is_ok() {
|
||||
select(app, all.first().copied().unwrap_or(0).saturating_sub(1));
|
||||
}
|
||||
}
|
||||
|
||||
/// Shift- or Ctrl/Cmd-click on artboard `i`'s row while the Artboard tool is in use: into its
|
||||
/// selection or out of it (the active one stays the navigator's).
|
||||
fn toggle(app: &mut VectorcraftApp, i: usize, n: usize) {
|
||||
|
|
@ -203,10 +216,8 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|||
} else if new.clicked() && app.run("artboard.new", json!({})).is_ok() {
|
||||
select(app, n);
|
||||
}
|
||||
if widgets::icon_button_enabled(ui, "trash-2", tl!("Delete Artboard"), false, n > all.len(), 24.0).clicked()
|
||||
&& app.run("artboard.delete", json!({ "indices": all })).is_ok()
|
||||
{
|
||||
select(app, all.first().copied().unwrap_or(0).saturating_sub(1));
|
||||
if widgets::icon_button_enabled(ui, "trash-2", tl!("Delete Artboard"), false, n > 1, 24.0).clicked() {
|
||||
delete_selected(app);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -226,7 +237,7 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|||
duplicate(app, sel);
|
||||
}
|
||||
if menu_item(ui, tl!("Delete Artboards"), n > 1, false) {
|
||||
app.run("artboard.delete", json!({"index": sel})).ok();
|
||||
delete_selected(app);
|
||||
}
|
||||
if menu_item(ui, tl!("Rename"), n > 0, false) {
|
||||
set_pstate(ui.ctx(), "ab-edit", Some(sel));
|
||||
|
|
|
|||
|
|
@ -238,7 +238,7 @@ fn artboard_sections(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|||
super::artboards::select(app, n);
|
||||
}
|
||||
if ui.add_enabled_ui(n > 1, |ui| widgets::flat_button(ui, tl!("Delete Artboard"), w)).inner.clicked() {
|
||||
app.run("artboard.delete", json!({"index": i})).ok();
|
||||
super::artboards::delete_selected(app);
|
||||
}
|
||||
});
|
||||
divider(ui);
|
||||
|
|
@ -813,6 +813,368 @@ mod tests {
|
|||
assert_eq!(app.session.tool_id(), "selection");
|
||||
}
|
||||
|
||||
fn artboard_deletion_app(count: usize) -> VectorcraftApp {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 200, "height": 100, "artboards": count})).unwrap();
|
||||
for i in 0..count {
|
||||
app.run("shape.rectangle", json!({"x": i * 250 + 10, "y": 10, "width": 30, "height": 20})).unwrap();
|
||||
}
|
||||
app.select_tool("artboard");
|
||||
super::super::artboards::select(&mut app, 0);
|
||||
app
|
||||
}
|
||||
|
||||
fn artboard_deletion_click(
|
||||
app: &mut VectorcraftApp,
|
||||
ctx: &egui::Context,
|
||||
draw: fn(&mut VectorcraftApp, &mut Ui),
|
||||
label: Option<&str>,
|
||||
modifiers: egui::Modifiers,
|
||||
) {
|
||||
let at = if let Some(label) = label {
|
||||
panel_frame(app, ctx, vec![], draw);
|
||||
let texts = panel_frame(app, ctx, vec![], draw);
|
||||
texts.iter().find(|(t, _)| t.trim() == label).map(|(_, r)| r.center()).unwrap_or_else(|| panic!("no {label:?} in {texts:?}"))
|
||||
} else {
|
||||
// The trash is the last 24-point footer button. Use the drawn panel bounds, as the Asset Export bin test uses its footer.
|
||||
let mut bounds = Rect::NOTHING;
|
||||
for _ in 0..2 {
|
||||
let raw = egui::RawInput { screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(300.0, 700.0))), ..Default::default() };
|
||||
let mut out = ctx.run_ui(raw, |ui| {
|
||||
draw(app, ui);
|
||||
bounds = ui.min_rect();
|
||||
});
|
||||
out.textures_delta.clear();
|
||||
}
|
||||
assert!(bounds.width() >= 24.0 && bounds.height() >= 24.0);
|
||||
bounds.right_bottom() - vec2(12.0, 12.0)
|
||||
};
|
||||
let button = |pressed| Event::PointerButton { pos: at, button: PointerButton::Primary, pressed, modifiers };
|
||||
panel_frame(app, ctx, vec![Event::ModifiersChanged(modifiers), Event::PointerMoved(at), button(true)], draw);
|
||||
panel_frame(app, ctx, vec![button(false)], draw);
|
||||
panel_frame(app, ctx, vec![Event::ModifiersChanged(egui::Modifiers::NONE)], draw);
|
||||
}
|
||||
|
||||
fn select_artboard_rows(app: &mut VectorcraftApp, ctx: &egui::Context, indices: &[usize]) {
|
||||
for (at, i) in indices.iter().enumerate() {
|
||||
let modifiers = if at == 0 { egui::Modifiers::NONE } else { egui::Modifiers::SHIFT };
|
||||
artboard_deletion_click(app, ctx, super::super::artboards::show, Some(&format!("Artboard {}", i + 1)), modifiers);
|
||||
}
|
||||
let count = app.session.doc().unwrap().doc.artboards.len();
|
||||
assert_eq!(super::super::artboards::picked(app, count), indices, "the rows are selected before deletion");
|
||||
}
|
||||
|
||||
fn assert_artboard_deletion(draw: fn(&mut VectorcraftApp, &mut Ui), label: Option<&str>) {
|
||||
for indices in [vec![0, 2], vec![2], vec![3], vec![2, 3], vec![0, 1, 2, 3]] {
|
||||
let mut app = artboard_deletion_app(4);
|
||||
let ctx = egui::Context::default();
|
||||
select_artboard_rows(&mut app, &ctx, &indices);
|
||||
app.session.doc_mut().unwrap().mark_saved();
|
||||
let before = app.session.doc().unwrap().doc.clone();
|
||||
let undo = app.session.doc().unwrap().history.undo.len();
|
||||
let all_selected = indices == [0, 1, 2, 3];
|
||||
let expected: Vec<_> = if all_selected {
|
||||
assert_eq!(app.view().unwrap().artboard, 3, "the active last board must not replace the first-board survivor oracle");
|
||||
vec![&before.artboards[0]]
|
||||
} else {
|
||||
before.artboards.iter().enumerate().filter(|(i, _)| !indices.contains(i)).map(|(_, a)| a).collect()
|
||||
};
|
||||
let expected = serde_json::to_value(expected).unwrap();
|
||||
let artwork = serde_json::to_value(&before.layers).unwrap();
|
||||
|
||||
let commands = app.run_count;
|
||||
artboard_deletion_click(&mut app, &ctx, draw, label, egui::Modifiers::NONE);
|
||||
let st = app.session.doc().unwrap();
|
||||
assert_eq!(
|
||||
serde_json::to_value(&st.doc.artboards).unwrap(),
|
||||
expected,
|
||||
"selected deletion keeps the expected IDs; all-selected keeps original board 1"
|
||||
);
|
||||
assert_eq!(app.run_count, commands.wrapping_add(1), "one command from the actual deletion control");
|
||||
assert_eq!(serde_json::to_value(&st.doc.layers).unwrap(), artwork, "artwork IDs, geometry and appearance stay intact");
|
||||
assert_eq!(st.history.undo.len(), undo + 1, "one deletion is one undo step");
|
||||
assert!(st.is_dirty());
|
||||
let active = app.view().unwrap().artboard;
|
||||
if all_selected {
|
||||
assert_eq!(active, 0);
|
||||
}
|
||||
assert!(active < st.doc.artboards.len(), "the active artboard remains in range");
|
||||
assert_eq!(app.session.tool_options()["active"], json!(active));
|
||||
assert_eq!(super::super::artboards::picked(&app, st.doc.artboards.len()), [active]);
|
||||
assert_eq!(app.session.tool_options()["selected"], json!([active]), "raw tool selection contains only the remaining active board");
|
||||
|
||||
app.run("edit.undo", json!({})).unwrap();
|
||||
let st = app.session.doc().unwrap();
|
||||
assert_eq!(serde_json::to_value(&st.doc.artboards).unwrap(), serde_json::to_value(&before.artboards).unwrap());
|
||||
assert_eq!(serde_json::to_value(&st.doc.layers).unwrap(), artwork);
|
||||
assert_eq!(st.history.undo.len(), undo);
|
||||
assert!(!st.is_dirty(), "one undo returns to the saved document");
|
||||
app.run("edit.redo", json!({})).unwrap();
|
||||
let st = app.session.doc().unwrap();
|
||||
assert_eq!(serde_json::to_value(&st.doc.artboards).unwrap(), expected, "redo removes the same artboard IDs");
|
||||
assert_eq!(serde_json::to_value(&st.doc.layers).unwrap(), artwork);
|
||||
assert_eq!(st.history.undo.len(), undo + 1);
|
||||
assert!(st.is_dirty());
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_artboard_deletion_guard(draw: fn(&mut VectorcraftApp, &mut Ui), label: Option<&str>) {
|
||||
let mut app = artboard_deletion_app(1);
|
||||
// Leave a redo entry to ensure a disabled action preserves both history branches.
|
||||
app.run("artboard.setProps", json!({"index": 0, "name": "Temporary"})).unwrap();
|
||||
app.run("edit.undo", json!({})).unwrap();
|
||||
let ctx = egui::Context::default();
|
||||
select_artboard_rows(&mut app, &ctx, &[0]);
|
||||
app.session.doc_mut().unwrap().mark_saved();
|
||||
let before = app.session.doc().unwrap().doc.clone();
|
||||
let st = app.session.doc().unwrap();
|
||||
let history = (st.history.undo.len(), st.history.redo.len());
|
||||
assert!(history.1 > 0);
|
||||
let active = app.view().unwrap().artboard;
|
||||
let options = app.session.tool_options();
|
||||
|
||||
let commands = app.run_count;
|
||||
artboard_deletion_click(&mut app, &ctx, draw, label, egui::Modifiers::NONE);
|
||||
assert_eq!(app.run_count, commands, "the disabled last-board control dispatches no command");
|
||||
let st = app.session.doc().unwrap();
|
||||
assert_eq!(serde_json::to_value(&st.doc.artboards).unwrap(), serde_json::to_value(&before.artboards).unwrap());
|
||||
assert_eq!(serde_json::to_value(&st.doc.layers).unwrap(), serde_json::to_value(&before.layers).unwrap());
|
||||
assert_eq!((st.history.undo.len(), st.history.redo.len()), history);
|
||||
assert!(!st.is_dirty(), "deleting the last artboard is a no-op");
|
||||
assert_eq!(app.view().unwrap().artboard, active);
|
||||
assert_eq!(app.session.tool_options(), options);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn artboard_menu_deletes_selected_artboards_in_one_undo_step() {
|
||||
assert_artboard_deletion(super::super::artboards::menu, Some("Delete Artboards"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn properties_deletes_selected_artboards_in_one_undo_step() {
|
||||
assert_artboard_deletion(show, Some("Delete Artboard"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn artboard_menu_preserves_last_artboard() {
|
||||
assert_artboard_deletion_guard(super::super::artboards::menu, Some("Delete Artboards"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn properties_preserves_last_artboard() {
|
||||
assert_artboard_deletion_guard(show, Some("Delete Artboard"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn artboard_trash_deletes_selected_artboards_in_one_undo_step() {
|
||||
assert_artboard_deletion(super::super::artboards::show, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn artboard_trash_preserves_last_artboard() {
|
||||
assert_artboard_deletion_guard(super::super::artboards::show, None);
|
||||
}
|
||||
|
||||
/// Click the actual panel footer's New button (24 points, with a 4-point gap before Trash).
|
||||
fn artboard_history_new(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
let mut bounds = Rect::NOTHING;
|
||||
for _ in 0..2 {
|
||||
let raw = egui::RawInput { screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(300.0, 700.0))), ..Default::default() };
|
||||
let mut out = ctx.run_ui(raw, |ui| {
|
||||
super::super::artboards::show(app, ui);
|
||||
bounds = ui.min_rect();
|
||||
});
|
||||
out.textures_delta.clear();
|
||||
}
|
||||
let count = app.session.doc().unwrap().doc.artboards.len();
|
||||
let commands = app.run_count;
|
||||
let at = bounds.right_bottom() - vec2(40.0, 12.0);
|
||||
let button = |pressed| Event::PointerButton { pos: at, button: PointerButton::Primary, pressed, modifiers: egui::Modifiers::NONE };
|
||||
panel_frame(app, ctx, vec![Event::PointerMoved(at), button(true)], super::super::artboards::show);
|
||||
panel_frame(app, ctx, vec![button(false)], super::super::artboards::show);
|
||||
assert_eq!(app.run_count, commands.wrapping_add(1), "the actual New button dispatches once");
|
||||
assert_eq!(app.session.doc().unwrap().doc.artboards.len(), count + 1);
|
||||
assert_eq!(app.session.tool_options()["active"], json!(count));
|
||||
assert_eq!(app.view().unwrap().artboard, count);
|
||||
}
|
||||
|
||||
/// Keep the panel and its natural focus in the same context throughout Undo and deletion.
|
||||
fn artboard_history_key(app: &mut VectorcraftApp, ctx: &egui::Context, key: egui::Key, modifiers: egui::Modifiers) {
|
||||
for pressed in [true, false] {
|
||||
let events = vec![Event::ModifiersChanged(modifiers), Event::Key { key, physical_key: None, pressed, repeat: false, modifiers }];
|
||||
let raw = egui::RawInput { screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(300.0, 700.0))), events, ..Default::default() };
|
||||
let mut out = ctx.run_ui(raw, |ui| {
|
||||
crate::shortcuts::handle(app, ui.ctx());
|
||||
super::super::artboards::show(app, ui);
|
||||
});
|
||||
out.textures_delta.clear();
|
||||
}
|
||||
}
|
||||
|
||||
fn artboard_history_delete_after_undo(key: Option<egui::Key>, duplicate: bool) {
|
||||
let mut app = artboard_deletion_app(2);
|
||||
app.session.set_tool_option("moveArt", &json!(false));
|
||||
app.session.set_tool_option("scaleArt", &json!(true));
|
||||
app.session.doc_mut().unwrap().mark_saved();
|
||||
let st = app.session.doc().unwrap();
|
||||
let before = st.doc.clone();
|
||||
let selection = st.selection.clone();
|
||||
assert!(!selection.is_empty(), "selected artwork catches a Clear fallthrough");
|
||||
let undo = st.history.undo.len();
|
||||
let ctx = egui::Context::default();
|
||||
if duplicate {
|
||||
let commands = app.run_count;
|
||||
artboard_deletion_click(&mut app, &ctx, super::super::artboards::menu, Some("Duplicate Artboards"), egui::Modifiers::NONE);
|
||||
assert_eq!(app.run_count, commands.wrapping_add(1));
|
||||
assert_eq!(app.session.doc().unwrap().doc.artboards.len(), 3);
|
||||
assert_eq!(app.session.tool_options()["active"], json!(2));
|
||||
assert_eq!(app.view().unwrap().artboard, 2);
|
||||
} else {
|
||||
artboard_history_new(&mut app, &ctx);
|
||||
}
|
||||
assert_eq!(app.session.doc().unwrap().history.undo.len(), undo + 1);
|
||||
artboard_history_key(&mut app, &ctx, egui::Key::Z, egui::Modifiers::COMMAND);
|
||||
let st = app.session.doc().unwrap();
|
||||
assert_eq!(st.doc, before, "the real Undo shortcut restores the original document");
|
||||
assert_eq!(st.selection, selection);
|
||||
assert_eq!((st.history.undo.len(), st.history.redo.len()), (undo, 1));
|
||||
assert!(!st.is_dirty());
|
||||
// Capture the state before another panel or pointer can repair it. Still exercise deletion
|
||||
// on RED: keyboard deletion used to do nothing; Properties already clamped its own target.
|
||||
let after_undo = (app.view().unwrap().artboard, app.session.tool_options());
|
||||
if let Some(key) = key {
|
||||
artboard_history_key(&mut app, &ctx, key, egui::Modifiers::NONE);
|
||||
} else {
|
||||
let texts = panel_frame(&mut app, &ctx, vec![], show);
|
||||
assert!(texts.iter().any(|(label, _)| label == &before.artboards[1].name));
|
||||
artboard_deletion_click(&mut app, &ctx, show, Some("Delete Artboard"), egui::Modifiers::NONE);
|
||||
}
|
||||
let st = app.session.doc().unwrap();
|
||||
assert_eq!(st.doc.artboards, vec![before.artboards[0].clone()], "deletion after Undo removes the shown second board");
|
||||
assert_eq!(st.doc.layers, before.layers, "ordinary artwork survives without geometry or style changes");
|
||||
assert_eq!(st.selection, selection, "deletion never falls through to Clear");
|
||||
assert_eq!(after_undo.0, 1, "Undo normalizes the navigator before another gesture");
|
||||
assert_eq!(after_undo.1, json!({"active": 1, "selected": [1], "moveArt": false, "scaleArt": true}));
|
||||
assert_eq!((st.history.undo.len(), st.history.redo.len()), (undo + 1, 0));
|
||||
assert_eq!(st.history.undo.last().unwrap().label, "Delete Artboard");
|
||||
assert!(st.is_dirty());
|
||||
assert_eq!(app.view().unwrap().artboard, 0);
|
||||
assert_eq!(app.session.tool_options(), json!({"active": 0, "selected": [0], "moveArt": false, "scaleArt": true}));
|
||||
let deleted = st.doc.clone();
|
||||
|
||||
artboard_history_key(&mut app, &ctx, egui::Key::Z, egui::Modifiers::COMMAND);
|
||||
let st = app.session.doc().unwrap();
|
||||
assert_eq!(st.doc, before);
|
||||
assert_eq!(st.selection, selection);
|
||||
assert_eq!((st.history.undo.len(), st.history.redo.len()), (undo, 1));
|
||||
assert!(!st.is_dirty());
|
||||
// Valid current selection stays put; history does not restore an earlier artboard selection.
|
||||
assert_eq!(app.session.tool_options()["selected"], json!([0]));
|
||||
artboard_history_key(&mut app, &ctx, egui::Key::Z, egui::Modifiers::COMMAND | egui::Modifiers::SHIFT);
|
||||
let st = app.session.doc().unwrap();
|
||||
assert_eq!(st.doc, deleted);
|
||||
assert_eq!(st.selection, selection);
|
||||
assert_eq!((st.history.undo.len(), st.history.redo.len()), (undo + 1, 0));
|
||||
assert!(st.is_dirty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn artboard_history_new_undo_delete_uses_remaining_board() {
|
||||
artboard_history_delete_after_undo(Some(egui::Key::Delete), false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn artboard_history_new_undo_backspace_uses_remaining_board() {
|
||||
artboard_history_delete_after_undo(Some(egui::Key::Backspace), false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn artboard_history_new_undo_properties_agrees_with_tool() {
|
||||
artboard_history_delete_after_undo(None, false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn artboard_history_duplicate_undo_delete_uses_remaining_board() {
|
||||
artboard_history_delete_after_undo(Some(egui::Key::Delete), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn artboard_history_undo_prunes_only_removed_indices() {
|
||||
for order in [[0, 1, 2], [0, 2, 1]] {
|
||||
let mut app = artboard_deletion_app(2);
|
||||
app.session.set_tool_option("moveArt", &json!(false));
|
||||
app.session.set_tool_option("scaleArt", &json!(true));
|
||||
app.session.doc_mut().unwrap().mark_saved();
|
||||
let st = app.session.doc().unwrap();
|
||||
let before = st.doc.clone();
|
||||
let selection = st.selection.clone();
|
||||
let undo = st.history.undo.len();
|
||||
let ctx = egui::Context::default();
|
||||
artboard_history_new(&mut app, &ctx);
|
||||
let created = app.session.doc().unwrap().doc.clone();
|
||||
for (at, i) in order.into_iter().enumerate() {
|
||||
let modifiers = if at == 0 { egui::Modifiers::NONE } else { egui::Modifiers::SHIFT };
|
||||
artboard_deletion_click(&mut app, &ctx, super::super::artboards::show, Some(&format!("Artboard {}", i + 1)), modifiers);
|
||||
}
|
||||
let options = app.session.tool_options();
|
||||
let expected = if order[2] == 2 { json!([0, 1]) } else { json!([1, 0]) };
|
||||
assert_eq!(options["selected"], if order[2] == 2 { json!([2, 0, 1]) } else { json!([1, 0, 2]) });
|
||||
artboard_history_key(&mut app, &ctx, egui::Key::Z, egui::Modifiers::COMMAND);
|
||||
let mut normalized = options;
|
||||
normalized["active"] = expected[0].clone();
|
||||
normalized["selected"] = expected;
|
||||
assert_eq!(app.session.tool_options(), normalized, "only the removed index is pruned, preserving surviving order and options");
|
||||
assert_eq!(json!(app.view().unwrap().artboard), normalized["active"]);
|
||||
let st = app.session.doc().unwrap();
|
||||
assert_eq!(st.doc, before);
|
||||
assert_eq!(st.selection, selection);
|
||||
assert_eq!((st.history.undo.len(), st.history.redo.len()), (undo, 1));
|
||||
assert!(!st.is_dirty());
|
||||
artboard_history_key(&mut app, &ctx, egui::Key::Z, egui::Modifiers::COMMAND | egui::Modifiers::SHIFT);
|
||||
assert_eq!(app.session.doc().unwrap().doc, created);
|
||||
assert_eq!(app.session.doc().unwrap().selection, selection);
|
||||
assert_eq!(app.session.tool_options(), normalized, "Redo does not replace an already valid multiselection");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn artboard_history_valid_multiselection_survives_undo_redo() {
|
||||
let mut app = artboard_deletion_app(3);
|
||||
app.session.set_tool_option("moveArt", &json!(false));
|
||||
app.session.set_tool_option("scaleArt", &json!(true));
|
||||
let ctx = egui::Context::default();
|
||||
select_artboard_rows(&mut app, &ctx, &[0, 1]);
|
||||
let options = app.session.tool_options();
|
||||
assert_eq!(options["selected"], json!([1, 0]));
|
||||
let view = app.view().unwrap().artboard;
|
||||
app.session.doc_mut().unwrap().mark_saved();
|
||||
let st = app.session.doc().unwrap();
|
||||
let before = st.doc.clone();
|
||||
let selection = st.selection.clone();
|
||||
let undo = st.history.undo.len();
|
||||
app.run("object.move", json!({"dx": 7, "dy": 9})).unwrap();
|
||||
let moved = app.session.doc().unwrap().doc.clone();
|
||||
assert_ne!(moved, before);
|
||||
artboard_history_key(&mut app, &ctx, egui::Key::Z, egui::Modifiers::COMMAND);
|
||||
app.sync_views();
|
||||
let st = app.session.doc().unwrap();
|
||||
assert_eq!(st.doc, before);
|
||||
assert_eq!(st.selection, selection);
|
||||
assert_eq!((st.history.undo.len(), st.history.redo.len()), (undo, 1));
|
||||
assert!(!st.is_dirty());
|
||||
assert_eq!(app.session.tool_options(), options);
|
||||
assert_eq!(app.view().unwrap().artboard, view);
|
||||
artboard_history_key(&mut app, &ctx, egui::Key::Z, egui::Modifiers::COMMAND | egui::Modifiers::SHIFT);
|
||||
app.sync_views();
|
||||
let st = app.session.doc().unwrap();
|
||||
assert_eq!(st.doc, moved);
|
||||
assert_eq!(st.selection, selection);
|
||||
assert_eq!((st.history.undo.len(), st.history.redo.len()), (undo + 1, 0));
|
||||
assert!(st.is_dirty());
|
||||
assert_eq!(app.session.tool_options(), options);
|
||||
assert_eq!(app.view().unwrap().artboard, view);
|
||||
}
|
||||
|
||||
/// #681: Rearrange All opens Rearrange All Artboards in one click, from the Document section
|
||||
/// (only with several artboards) and from Edit Artboards' Quick Actions.
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -365,6 +365,16 @@ pub fn handle(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
|||
] {
|
||||
if ctx.input(|i| i.key_pressed(k)) && app.session.tool_claims_key(tk, view) && ctx.input_mut(|i| i.consume_key(m, k)) {
|
||||
let r = app.session.tool_key(tk, crate::canvas::mods(m, false), view);
|
||||
// Keyboard actions can change the Artboard tool's active board too. Keep the
|
||||
// navigator aligned with its valid survivor, as pointer dispatch does.
|
||||
if r.is_ok()
|
||||
&& app.session.tool_id() == "artboard"
|
||||
&& let Some(i) = app.session.tool_options()["active"].as_u64().and_then(|i| usize::try_from(i).ok())
|
||||
&& app.session.active().is_some_and(|d| i < d.doc.artboards.len())
|
||||
&& let Some(v) = app.view_mut()
|
||||
{
|
||||
v.artboard = i;
|
||||
}
|
||||
crate::canvas::apply_requests(app, r);
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
//! Delete and Backspace with the Width and Puppet Warp tools, through the shortcut routing: the
|
||||
//! selected width point or pin goes, and with nothing of the tool's selected the keys clear the
|
||||
//! selected art as usual.
|
||||
//! Delete and Backspace through shortcut routing: Artboard editing protects selected artwork,
|
||||
//! including when no artboard can be deleted. Width points and Puppet Warp pins take the keys
|
||||
//! while selected, then leave them to Clear.
|
||||
|
||||
use egui::{Key, Modifiers};
|
||||
use serde_json::{Value, json};
|
||||
|
|
@ -103,3 +103,211 @@ fn delete_and_backspace_remove_the_selected_pin_then_clear() {
|
|||
assert!(!exists(&app, id), "{key:?} with no pin selected clears the selection");
|
||||
}
|
||||
}
|
||||
|
||||
fn artboard_keys_app(boards: usize) -> (VectorcraftApp, u64) {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 400, "height": 400, "artboards": boards})).unwrap();
|
||||
let id = app.run("shape.rectangle", json!({"x": 20, "y": 20, "width": 40, "height": 30})).unwrap()["id"].as_u64().unwrap();
|
||||
app.run("select.set", json!({"ids": [id]})).unwrap();
|
||||
app.select_tool("artboard");
|
||||
(app, id)
|
||||
}
|
||||
|
||||
/// A protected deletion must preserve both history branches and the saved snapshot.
|
||||
fn protected_artboard_key(key: Key) {
|
||||
let (mut app, id) = artboard_keys_app(1);
|
||||
app.run("object.move", json!({"dx": 7, "dy": 9})).unwrap();
|
||||
let redo_doc = app.session.doc().unwrap().doc.clone();
|
||||
app.run("edit.undo", json!({})).unwrap();
|
||||
app.session.active_mut().unwrap().mark_saved();
|
||||
let st = app.session.doc().unwrap();
|
||||
let before = st.doc.clone();
|
||||
let selection = st.selection.clone();
|
||||
let history = (st.history.undo.len(), st.history.redo.len());
|
||||
assert!(history.0 > 0 && history.1 == 1);
|
||||
let options = app.session.tool_options();
|
||||
let active = app.view().unwrap().artboard;
|
||||
|
||||
press(&mut app, key);
|
||||
|
||||
assert!(exists(&app, id), "{key:?} with the last artboard must keep selected artwork");
|
||||
let st = app.session.doc().unwrap();
|
||||
assert_eq!(*st.doc, *before, "protected artboards and artwork stay unchanged");
|
||||
assert_eq!(st.selection, selection);
|
||||
assert_eq!((st.history.undo.len(), st.history.redo.len()), history);
|
||||
assert!(!st.is_dirty());
|
||||
assert_eq!(app.session.tool_options(), options);
|
||||
assert_eq!(app.view().unwrap().artboard, active);
|
||||
app.run("edit.redo", json!({})).unwrap();
|
||||
assert_eq!(*app.session.doc().unwrap().doc, *redo_doc, "the original Redo still works");
|
||||
app.run("edit.undo", json!({})).unwrap();
|
||||
assert_eq!(*app.session.doc().unwrap().doc, *before);
|
||||
assert!(!app.session.doc().unwrap().is_dirty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn artboard_keys_delete_keeps_first_of_all_selected_boards() {
|
||||
delete_artboards_with_key(Key::Delete, true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn artboard_keys_backspace_keeps_first_of_all_selected_boards() {
|
||||
delete_artboards_with_key(Key::Backspace, true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn artboard_keys_delete_protects_last_board_and_art() {
|
||||
protected_artboard_key(Key::Delete);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn artboard_keys_backspace_protects_last_board_and_art() {
|
||||
protected_artboard_key(Key::Backspace);
|
||||
}
|
||||
|
||||
fn delete_artboards_with_key(key: Key, select_all: bool) {
|
||||
let (mut app, id) = artboard_keys_app(3);
|
||||
// Reorder so the first board is neither the original lowest-ID board nor the active last one.
|
||||
app.run("artboard.reorder", json!({"index": 2, "to": 0})).unwrap();
|
||||
let selected = if select_all { json!([2, 1, 0]) } else { json!([2, 0]) };
|
||||
crate::panels::artboards::select(&mut app, 2);
|
||||
app.session.set_tool_option("selected", &selected);
|
||||
app.session.active_mut().unwrap().mark_saved();
|
||||
let st = app.session.doc().unwrap();
|
||||
let before = st.doc.clone();
|
||||
let selection = st.selection.clone();
|
||||
let survivor = if select_all { 0 } else { 1 };
|
||||
let undo = st.history.undo.len();
|
||||
|
||||
press(&mut app, key);
|
||||
|
||||
let st = app.session.doc().unwrap();
|
||||
assert_eq!(st.doc.artboards, vec![before.artboards[survivor].clone()], "the correct original board record survives");
|
||||
assert_eq!(st.selection, selection);
|
||||
assert_eq!(st.doc.node(vectorcraft_doc::NodeId(id)), before.node(vectorcraft_doc::NodeId(id)), "artwork is unchanged");
|
||||
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 after = st.doc.clone();
|
||||
assert_eq!(app.session.tool_options()["selected"], json!([0]));
|
||||
assert_eq!(app.session.tool_options()["active"], json!(0));
|
||||
assert_eq!(app.view().unwrap().artboard, 0);
|
||||
app.run("edit.undo", json!({})).unwrap();
|
||||
assert_eq!(*app.session.doc().unwrap().doc, *before);
|
||||
assert!(!app.session.doc().unwrap().is_dirty());
|
||||
app.run("edit.redo", json!({})).unwrap();
|
||||
assert_eq!(*app.session.doc().unwrap().doc, *after);
|
||||
assert_eq!(app.session.doc().unwrap().history.undo.len(), undo + 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn artboard_keys_delete_removes_partial_selection_in_one_step() {
|
||||
delete_artboards_with_key(Key::Delete, false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn artboard_keys_backspace_removes_partial_selection_in_one_step() {
|
||||
delete_artboards_with_key(Key::Backspace, false);
|
||||
}
|
||||
|
||||
/// Keyboard-only history sequences must not need a canvas event to align the navigator and tool.
|
||||
fn artboard_keys_history_sequence(redo_before_delete: bool) {
|
||||
for key in [Key::Delete, Key::Backspace] {
|
||||
for select_all in [false, true] {
|
||||
let (mut app, id) = artboard_keys_app(3);
|
||||
app.run("artboard.reorder", json!({"index": 2, "to": 0})).unwrap();
|
||||
// Match the panel's selection: both the navigator and tool start on the last board.
|
||||
crate::panels::artboards::select(&mut app, 2);
|
||||
app.session.set_tool_option("selected", &if select_all { json!([2, 1, 0]) } else { json!([2, 0]) });
|
||||
assert_eq!(app.view().unwrap().artboard, 2);
|
||||
assert_eq!(app.session.tool_options()["active"], json!(2));
|
||||
app.session.active_mut().unwrap().mark_saved();
|
||||
let st = app.session.doc().unwrap();
|
||||
let before = st.doc.clone();
|
||||
let selection = st.selection.clone();
|
||||
let undo = st.history.undo.len();
|
||||
let survivor = if select_all { 0 } else { 1 };
|
||||
let check = |app: &VectorcraftApp, indices: &[usize], undo: usize, redo: usize, dirty: bool| {
|
||||
let st = app.session.doc().unwrap();
|
||||
let expected: Vec<_> = indices.iter().map(|&i| before.artboards[i].clone()).collect();
|
||||
assert_eq!(st.doc.artboards, expected, "{key:?}: survivor IDs, order and full records");
|
||||
assert_eq!(st.doc.node(vectorcraft_doc::NodeId(id)), before.node(vectorcraft_doc::NodeId(id)), "selected artwork stays unchanged");
|
||||
assert_eq!(st.selection, selection);
|
||||
assert_eq!((st.history.undo.len(), st.history.redo.len()), (undo, redo));
|
||||
assert_eq!(st.is_dirty(), dirty);
|
||||
assert_eq!(app.view().unwrap().artboard, 0, "navigator must follow the surviving active board without a canvas event");
|
||||
assert_eq!(app.session.tool_options()["active"], json!(0));
|
||||
assert_eq!(app.session.tool_options()["selected"], json!([0]));
|
||||
};
|
||||
|
||||
press(&mut app, key);
|
||||
check(&app, &[survivor], undo + 1, 0, true);
|
||||
assert_eq!(app.session.doc().unwrap().history.undo.last().unwrap().label, "Delete Artboard");
|
||||
let deleted = app.session.doc().unwrap().doc.clone();
|
||||
app.run("edit.undo", json!({})).unwrap();
|
||||
check(&app, &[0, 1, 2], undo, 1, false);
|
||||
assert_eq!(*app.session.doc().unwrap().doc, *before);
|
||||
|
||||
if redo_before_delete {
|
||||
app.run("edit.redo", json!({})).unwrap();
|
||||
check(&app, &[survivor], undo + 1, 0, true);
|
||||
assert_eq!(*app.session.doc().unwrap().doc, *deleted);
|
||||
press(&mut app, key);
|
||||
check(&app, &[survivor], undo + 1, 0, true);
|
||||
assert_eq!(*app.session.doc().unwrap().doc, *deleted, "deleting the final board is a protected no-op");
|
||||
} else {
|
||||
// Undo restores the boards, not the old multi-selection: the next key deletes
|
||||
// the board both the navigator and tool now show, replacing the Redo branch.
|
||||
press(&mut app, key);
|
||||
check(&app, &[1, 2], undo + 1, 0, true);
|
||||
assert_eq!(app.session.doc().unwrap().history.undo.last().unwrap().label, "Delete Artboard");
|
||||
let repeated = app.session.doc().unwrap().doc.clone();
|
||||
app.run("edit.undo", json!({})).unwrap();
|
||||
check(&app, &[0, 1, 2], undo, 1, false);
|
||||
assert_eq!(*app.session.doc().unwrap().doc, *before);
|
||||
app.run("edit.redo", json!({})).unwrap();
|
||||
check(&app, &[1, 2], undo + 1, 0, true);
|
||||
assert_eq!(*app.session.doc().unwrap().doc, *repeated);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn artboard_keys_keep_navigator_in_sync_through_undo_redo_delete() {
|
||||
artboard_keys_history_sequence(true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn artboard_keys_keep_navigator_in_sync_when_deleting_after_undo() {
|
||||
artboard_keys_history_sequence(false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn artboard_keys_preserve_drag_preview_and_art() {
|
||||
for key in [Key::Delete, Key::Backspace] {
|
||||
let (mut app, id) = artboard_keys_app(3);
|
||||
app.session.active_mut().unwrap().mark_saved();
|
||||
let before = app.session.doc().unwrap().doc.clone();
|
||||
let undo = app.session.doc().unwrap().history.undo.len();
|
||||
let view = app.view_info();
|
||||
for (kind, x, y) in [(PointerKind::Down, 200.0, 200.0), (PointerKind::Drag, 230.0, 225.0)] {
|
||||
app.session.pointer(&PointerEvent::new(kind, x, y), view).unwrap();
|
||||
}
|
||||
let preview = app.session.doc().unwrap().doc.clone();
|
||||
assert_ne!(*preview, *before);
|
||||
assert!(app.session.tool_busy());
|
||||
|
||||
press(&mut app, key);
|
||||
|
||||
assert!(exists(&app, id), "{key:?} during a drag must keep selected artwork");
|
||||
assert_eq!(*app.session.doc().unwrap().doc, *preview);
|
||||
assert!(app.session.tool_busy());
|
||||
assert_eq!(app.session.doc().unwrap().history.undo.len(), undo);
|
||||
press(&mut app, Key::Escape);
|
||||
assert_eq!(*app.session.doc().unwrap().doc, *before);
|
||||
assert!(!app.session.tool_busy());
|
||||
assert!(!app.session.doc().unwrap().is_dirty());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -1,6 +1,6 @@
|
|||
# Roadmap: milestones and what's next
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (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; per-corner radius fields with a link toggle, split from #1594; control transport deadlines, #1726; M12 row: Image Trace's presets and options as they are now; Polish interface, #1498; compact native saves, #1492; PDF and EPS paste as vector art, #1499; import fixes, #1528, #1530, #1531, #1544, #1551, #1553; guide objects, #1534; Join after Average, #1533; Keyboard Shortcuts, #1539; estimates unchanged; Direct Selection anchor precedence, #1508; dialog slider rails, #1483; template layers in Outline view, #1486; type that does not fit its frame is reported, #1471; 3D on outlined type, #1481; print-size images, #1517; French catalog, #1511; estimates unchanged; Split Into Grid options, #1462; Transform Objects and Reflect's angle, #1448 and #1460; Pathfinder on a group, #1450; unknown parameter keys refused, #1442; grayscale TIFFs with alpha, #1466 in part; estimates unchanged; M9: Extrude & Bevel, Inflate and Rotate; `.ai` compound path paint, #1410, overprint on import, #1388, text-only PDF parts, #1387; Transform dialogs preview, #1412, and transforms of direct-selected anchors, #1408; strokes wider than their shape, #1396; estimates unchanged; `.ai` editing-data follow-ups, #1390, #1391, #1392; dialog keyboard focus, #1367 and #1395; symbol instance redraw cache, #1386; font rescan, #1376; estimates unchanged; gradient shapes import as paths, #1304; white constant alpha masks, #1286 in part; Live Corners widget click, #1239; custom ruler origin, #1212; M11: Artboard object clicks create a fitted artboard (#1227); estimate unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; DXF control-point SPLINE closure read, #1299 follow-up; desktop print transport adopts craft-print, Windows 7 fallback retained, 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; M6: Simplify preview/counts and valid topology selection (#1272); estimates unchanged; G2: Film Grain added; Photoshop effects 29/57; 28 filters remain; M14: RPM runtime metadata (#1275); estimates unchanged; M5: squared-distance texture redraws (#1257); clean-machine/platform drag measurements still pending; estimates unchanged; M2: Curvature point display improved (#1236, in part); estimates unchanged; M7: full-name font matching (#1258); Windows font-service QA pending; estimates unchanged; M1: no-op anchor Align keeps Undo, Redo and saved state (#1180 follow-up); estimates unchanged; M14: dedicated Eyedropper cursor (#1269); estimates unchanged; M3: Eyedropper reads visible locked and outside-isolation sources (#1251); estimates unchanged; M2: Direct Selection segment picks and Delete (#1278); estimates unchanged; M3: adding paint to an unpainted zero-width stroke restores 1 pt (#1253); estimates unchanged; M12: graph designs, graph data, Graph Type and label options; M14: native macOS save Format menu, #866; M6: compound appearance and member fill-rule fixes, #1197; estimates unchanged; M9: PDF raster-effect cap warnings; G11: #893 fixed, #937 open; M7: accurate character controls after scaling, #1034; estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (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; per-corner radius fields with a link toggle, split from #1594; control transport deadlines, #1726; M12 row: Image Trace's presets and options as they are now; Polish interface, #1498; compact native saves, #1492; PDF and EPS paste as vector art, #1499; import fixes, #1528, #1530, #1531, #1544, #1551, #1553; guide objects, #1534; Join after Average, #1533; Keyboard Shortcuts, #1539; estimates unchanged; Direct Selection anchor precedence, #1508; dialog slider rails, #1483; template layers in Outline view, #1486; type that does not fit its frame is reported, #1471; 3D on outlined type, #1481; print-size images, #1517; French catalog, #1511; estimates unchanged; Split Into Grid options, #1462; Transform Objects and Reflect's angle, #1448 and #1460; Pathfinder on a group, #1450; unknown parameter keys refused, #1442; grayscale TIFFs with alpha, #1466 in part; estimates unchanged; M9: Extrude & Bevel, Inflate and Rotate; `.ai` compound path paint, #1410, overprint on import, #1388, text-only PDF parts, #1387; Transform dialogs preview, #1412, and transforms of direct-selected anchors, #1408; strokes wider than their shape, #1396; estimates unchanged; `.ai` editing-data follow-ups, #1390, #1391, #1392; dialog keyboard focus, #1367 and #1395; symbol instance redraw cache, #1386; font rescan, #1376; estimates unchanged; gradient shapes import as paths, #1304; white constant alpha masks, #1286 in part; Live Corners widget click, #1239; custom ruler origin, #1212; M11: Artboard object clicks create a fitted artboard (#1227); estimate unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; DXF control-point SPLINE closure read, #1299 follow-up; desktop print transport adopts craft-print, Windows 7 fallback retained, 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; M6: Simplify preview/counts and valid topology selection (#1272); estimates unchanged; G2: Film Grain added; Photoshop effects 29/57; 28 filters remain; M14: RPM runtime metadata (#1275); estimates unchanged; M5: squared-distance texture redraws (#1257); clean-machine/platform drag measurements still pending; estimates unchanged; M2: Curvature point display improved (#1236, in part); estimates unchanged; M7: full-name font matching (#1258); Windows font-service QA pending; estimates unchanged; M1: no-op anchor Align keeps Undo, Redo and saved state (#1180 follow-up); estimates unchanged; M14: dedicated Eyedropper cursor (#1269); estimates unchanged; M3: Eyedropper reads visible locked and outside-isolation sources (#1251); estimates unchanged; M2: Direct Selection segment picks and Delete (#1278); estimates unchanged; M3: adding paint to an unpainted zero-width stroke restores 1 pt (#1253); estimates unchanged; M12: graph designs, graph data, Graph Type and label options; M14: native macOS save Format menu, #866; M6: compound appearance and member fill-rule fixes, #1197; estimates unchanged; M9: PDF raster-effect cap warnings; G11: #893 fixed, #937 open; M7: accurate character controls after scaling, #1034; estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
|
||||
Forward-looking plan. Where VectorCraft stands is in [`ROADMAP.md`](../ROADMAP.md) and
|
||||
[target-app-parity.md](target-app-parity.md); the ranked work list is [gaps.md](gaps.md). Hours are Opus 5.5
|
||||
|
|
@ -60,7 +60,7 @@ Checked 2026-10-10 from the code, tests and open issues; no workflow fails, so V
|
|||
| M8 | Transform & distort (Puppet Warp, Liquify tools, Envelopes, Blends, Perspective Grid) | ✅ done (M8.1–M8.20 on 2026-10-06, fidelity pass): Liquify Tool Options with one shared brush, pen pressure, hold-to-apply, a safe scope and incremental strokes; tool options that last; Puppet Warp rotation, multi-select, Control bar and a rest-shape session that follows Undo; Width tool on compound paths; Envelope dialogs with preview, Reset with Warp/Mesh, full Envelope Options, type/images/symbols/appearance/gradients/patterns distorted on canvas and in every export, warps that keep their frame, Edit Contents, mesh handles and Control bar; Blend Options, anchor-targeted Blend tool, editable spine, keys picked on the canvas, faithful interpolation and knockout; Perspective Grid with Define Grid, presets, lock/snap/rulers, widgets, the 1–4 plane switch, movable planes, Perspective Selection scale/copy/perpendicular/Transform Again, and type and symbols truly projected with Edit Text. Left: Anti-Alias and Preserve Shape output, warp-envelope point editing, brush interpolation in blends, Free Transform/Scale/Reflect along a rotated box | 2–4 |
|
||||
| M9 | Live effects (+ 3D & Materials) | 🟡 2D effects done incl. Effect → Pathfinder, Color Adjustments and Document Raster Effects Settings; PDF export warns when the pixel cap lowers raster-effect resolution; Artistic › Film Grain done (29/57 Photoshop filters); initial Revolve, Extrude & Bevel, Inflate and Rotate; SVG Filters, remaining raster effects and 3D materials, lights and shadows pending | 81–130 |
|
||||
| M10 | Brushes, symbols, patterns, Repeat | 🟡 pattern swatches (5 tile types, Pattern Options, editing mode, SVG `<pattern>`/PDF export) and live Repeat (radial/grid/mirror) done; brushes/symbols in progress | 23–37 |
|
||||
| M11 | Artboards & views (click-to-fit objects with the Artboard tool (#1227), artboard escape routing and document setup modal handoff done; artboard panel/tool with Move Artwork for X/Y fields and Artboard Options, Trim View, middle-button pan, print tiling and the status bar's artboard navigator done; multiple windows, presentation polish) | 🟡 | 15–25 |
|
||||
| M11 | Artboards & views (selected-set deletion shared by Artboards menu, trash and Properties, retaining artboard 1 when all are selected; Artboard deletion keys keep the first of an all-selected set and protect artwork; history prunes invalid indices while preserving valid multiselection (history tests and full workspace CI pass); click-to-fit objects with the Artboard tool (#1227), artboard escape routing and document setup modal handoff done; artboard panel/tool with Move Artwork for X/Y fields and Artboard Options, Trim View, middle-button pan, print tiling and the status bar's artboard navigator with synchronized Artboard tool selection done; multiple windows, presentation polish) | 🟡 | 15–25 |
|
||||
| M12 | Advanced color & art (CMYK/ICC, separations, Gradient Mesh, Live Paint, Image Trace, Graphs) | 🟡 Gradient Mesh, Live Paint, Image Trace (15 presets; Ignore Color, Shapes, Auto Grouping and Gradients options; measured in `image-trace-quality.md`; the Flat Logo mode places edges to a fraction of a pixel, restores sharp corners, snaps lines and circles, and refuses gradients), Recolor Artwork, colour management (ICC, soft proofing, separations preview), Graphs (all 9 tools, Graph Data/Type, group-selected series colours persist through regenerate/save/open) done; Graph Design and Marker designs, Transpose and Switch x/y, `|` label line breaks, the pie options, drop shadows, the legend across the top, overlapping widths, First Row/Column in Front, filled lines, kept label formatting and the Graph Data worksheet done; graph Column designs pending | 6–10 |
|
||||
| M13 | Automation (Actions ✅ record/playback, persisted; variables, scripting, batch) | 🟡 | 18–27 |
|
||||
| M14 | 1.0 polish (preferences, shortcut editor, workspaces, accessibility, packaging for all OSes) | 🟡 Numeric-field pointer selection and focus regression-tested (#1326, in part; short movements still select all); RPM runtime capabilities and glibc floor checked (#1275; real-distro install/start-up pending), Eyedropper cursor with precise-setting checks (#1269), Preferences, shortcut editor (chords with Shift and a punctuation key fire as typed), workspaces, a custom title bar on Windows/Linux with a Home button, content-sized dialogs, menus that scroll when longer than the window and macOS native menu dismissal done (M14.8), and native macOS save sheets with a Format menu (#866); an experimental, unsupported Windows 7 x64 portable OpenGL build (not verified by maintainers); accessibility and broader Windows/Linux packaging/QA pending | 18–28 |
|
||||
|
|
@ -82,6 +82,7 @@ After placed documents (File › Place of a `.vectorcraft` file, linked), M4.14
|
|||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-13 | minor | G1/M11: Artboard panel deletion, protected deletion keys and navigator/tool synchronization before and after deletion; history normalization preserves valid multiselection; history regressions and full workspace CI pass; scores and hours unchanged |
|
||||
| 2026-10-13 | minor | M7: mixed paragraph alignment indication implemented; headless regressions, full integration CI and visual review pass; other mixed paragraph fields, scores and hours unchanged |
|
||||
| 2026-10-13 | minor | Interaction fidelity: Alt swaps a new ruler guide's orientation during creation; preview, snapping, release, cancellation and Undo/Redo have event regressions. Scores and hours unchanged (estimated). |
|
||||
| 2026-10-13 | minor | G1: create width points through Width Point Edit with optional adjoining adjustment; command/pointer/history regressions and full CI passed; dialog layout reviewed; scores and hours unchanged |
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -1,6 +1,6 @@
|
|||
# UI and interaction parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (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-13 · **Change:** minor (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,
|
||||
|
|
@ -148,7 +148,10 @@ tools, never checked side by side) isn't captured row by row.
|
|||
| Alt-drag duplicates (objects, artboards with their art) | done | |
|
||||
| Document Setup → Edit Artboards applies valid settings, closes the dialog and activates the Artboard tool | done | Pointer-click tests cover successful handoff, invalid settings, OK, Cancel and Undo in `dialogs/document_setup.rs`. |
|
||||
| Escape leaves Artboard editing for Selection; during a drag it first cancels the gesture | done | Headless egui key events cover idle Escape, active-drag cancellation and a subsequent Escape in `tests_distortkeys.rs`. |
|
||||
| Shift-click selects several artboards (Artboard tool, Artboards panel); they move, Alt-copy and delete together | done | `artboard.move`/`artboard.delete` `indices` (#1139) |
|
||||
| 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 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. |
|
||||
| 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 |
|
||||
| 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 | |
|
||||
| Tool double-click opens its options | done | |
|
||||
|
|
@ -203,6 +206,7 @@ re-measure of the panels added since.
|
|||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-13 | minor | G1/M11: Artboard panel deletion, protected deletion keys and navigator/tool synchronization before and after deletion; history normalization preserves valid multiselection; history regressions and full workspace CI pass; scores and hours unchanged |
|
||||
| 2026-10-13 | minor | Alt during a drag from either ruler swaps the new guide's orientation ([public shortcut reference](https://helpx.adobe.com/illustrator/using/default-keyboard-shortcuts.html)); tool and egui event tests cover preview, snapping, release coordinates, cancellation and one-step Undo/Redo; scores unchanged (estimated). |
|
||||
| 2026-10-13 | minor | G1: create width points through Width Point Edit with optional adjoining adjustment; command/pointer/history regressions and full CI passed; dialog layout reviewed; scores and hours unchanged |
|
||||
| 2026-10-13 | minor | Corner Radius: a link toggle behind the field in the Properties and Transform panels; off, a live rectangle shows a radius field for each corner (Top Left, Top Right, Bottom Right, Bottom Left), each setting its own corner (`singleCornerRadius` preference); split from #1594; score unchanged (estimated) |
|
||||
|
|
|
|||
Loading…
Reference in New Issue