From ebc2857549530b29f33ac372fbc018d7264322c7 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Thu, 15 Oct 2026 18:43:30 -0700 Subject: [PATCH] Align: Distribute Objects uses the spacing value with a key object With a key object, the Align panel's six Distribute Objects buttons ignored the Distribute Spacing value. They kept the outermost objects in place, spaced the others evenly between them and could move the key object. object.distribute now takes spacing. While the selection has a key object, the key object stays in place, with the compound path or selected group that contains it, and the objects' left edges, centers or right edges (top edges, centers or bottom edges) end up spacing points apart, outward from the key object's, in their order on that line. With 0, the field's default, every object ends on the key object's line. Two objects are enough, and to and artboard are not used. The panel sends the value with the Distribute Objects buttons while there is a key object, as it does with the Distribute Spacing buttons. Without a key object, or with anchor points selected, object.distribute does not use spacing and works as before. A spacing that is not a number is an error. Docs: docs/mcp.md, docs/target-app-parity.md. --- crates/engine/src/cmd/object.rs | 37 +++++++++++++------ crates/engine/src/tests.rs | 53 ++++++++++++++++++++++++++++ crates/engine/tests/command_sweep.rs | 17 +++++++++ crates/ui-egui/src/panels/align.rs | 53 ++++++++++++++++++++++++---- docs/mcp.md | 14 ++++++-- docs/target-app-parity.md | 1 + 6 files changed, 155 insertions(+), 20 deletions(-) diff --git a/crates/engine/src/cmd/object.rs b/crates/engine/src/cmd/object.rs index 4ce950016..bbd159151 100644 --- a/crates/engine/src/cmd/object.rs +++ b/crates/engine/src/cmd/object.rs @@ -252,7 +252,7 @@ pub fn specs() -> Vec { "Distribute", ["Window", "Align"], None, - "{horizontal?: \"left\"|\"center\"|\"right\", vertical?: \"top\"|\"center\"|\"bottom\", to?: \"selection\"|\"artboard\"|\"key\" (default: the selection; \"key\" distributes as \"selection\" does; not used with anchor points selected), artboard?: index (0-based; with to: \"artboard\", the artboard to distribute across; the app passes the active one; default: as object.align's), bounds?: \"preview\"|\"geometric\" (default: the Use Preview Bounds preference; preview bounds take in strokes), patterns?: bool (Transform Patterns: pattern fills and strokes transform with the art; default: prefs transformPatternTiles)} space the objects' left edges, centers or right edges (top edges, centers or bottom edges) evenly, in their order on that line: the first and last objects stay put, and fewer than three objects don't move; with to: \"artboard\", the first one's left (top) edge goes to the artboard's left (top) edge, the last one's right (bottom) edge goes to the artboard's right (bottom) edge, and two objects are enough. With anchor points selected (Direct Selection), the points are spaced evenly between the first and the last along the axis instead (#1636)", + "{horizontal?: \"left\"|\"center\"|\"right\", vertical?: \"top\"|\"center\"|\"bottom\", spacing?: pt (used with a key object, select.key: the key object stays put and the lines are placed this far apart, outward from its line, in their order on that line; 0 puts them all on its line; not used without a key object or with anchor points selected), to?: \"selection\"|\"artboard\"|\"key\" (default: the selection; \"key\" distributes as \"selection\" does; not used with anchor points selected, or with spacing and a key object), artboard?: index (0-based; with to: \"artboard\", the artboard to distribute across; the app passes the active one; default: as object.align's), bounds?: \"preview\"|\"geometric\" (default: the Use Preview Bounds preference; preview bounds take in strokes), patterns?: bool (Transform Patterns: pattern fills and strokes transform with the art; default: prefs transformPatternTiles)} space the objects' left edges, centers or right edges (top edges, centers or bottom edges) in their order on that line: without spacing and a key object they are spaced evenly, the first and last objects stay put, and fewer than three objects don't move; with spacing and a key object, see spacing (two objects are enough); with to: \"artboard\", the first one's left (top) edge goes to the artboard's left (top) edge, the last one's right (bottom) edge goes to the artboard's right (bottom) edge, and two objects are enough. With anchor points selected (Direct Selection), the points are spaced evenly between the first and the last along the axis instead (#1636)", can_distribute, distribute ), @@ -1232,6 +1232,15 @@ fn direction<'a>(p: &'a Value, cmd: &str) -> Result<(Option<&'a str>, Option<&'a Ok((choice(p, cmd, "horizontal", &["left", "center", "right"])?, choice(p, cmd, "vertical", &["top", "center", "bottom"])?)) } +/// `spacing` of `object.distribute` and `object.distributeSpacing`: a finite number, or `None` +/// when it is missing or null. Any other value is an error. +fn spacing(p: &Value, cmd: &str) -> Result> { + p.get("spacing") + .filter(|v| !v.is_null()) + .map(|v| v.as_f64().filter(|g| g.is_finite()).ok_or_else(|| bad(cmd, "`spacing` must be a number"))) + .transpose() +} + /// `ids` with their bounds (`preview`: visual bounds). fn items_bounds(d: &Document, ids: &[NodeId], preview: bool) -> Vec<(NodeId, Rect)> { ids.iter().filter_map(|id| Some((*id, d.bounds_of(&[*id], preview)?))).collect() @@ -1294,8 +1303,8 @@ fn distribute_to(s: &Session, p: &Value, ids: &[NodeId], preview: bool, cmd: &st } /// Returns the selected root (one of `ids`) that is the key object or contains it, such as the -/// compound path or selected group the key is in. Align to the key object and Distribute Spacing -/// with a spacing keep it in place. +/// compound path or selected group the key is in. Align to the key object, and Distribute and +/// Distribute Spacing with a spacing, keep it in place. fn key_root(s: &Session, ids: &[NodeId]) -> Option { let st = s.doc().ok()?; let ancestry = st.doc.ancestry(st.selection.key?)?; @@ -1439,6 +1448,7 @@ fn distribute(s: &mut Session, p: &Value) -> Result { let ids = selected_roots(s)?; let preview = preview_bounds(s, p, "object.distribute")?; let board = distribute_to(s, p, &ids, preview, "object.distribute")?; + let spacing = spacing(p, "object.distribute")?; // Anchor points selected with Direct Selection are distributed themselves, as Align aligns them. if !s.doc()?.selection.anchors.is_empty() { return match direction(p, "object.distribute")? { @@ -1468,8 +1478,14 @@ fn distribute(s: &mut Session, p: &Value) -> Result { }; items.sort_by(|a, b| key(&a.1, horiz).total_cmp(&key(&b.1, horiz))); let n = items.len(); - // Two objects are enough on an artboard: they go to its edges. - if n < 2 || (n < 3 && board.is_none()) { + // With a spacing, the key object (or the compound path or selected group it is in) stays where + // it is, and the lines are placed `spacing` apart, outward from its line, in their order. + // Without a key object the spacing is not used. `from_key` is the key's index in `items` and + // the spacing. + let from_key = spacing.zip(key_root(s, &ids)).and_then(|(g, k)| Some((items.iter().position(|(id, _)| *id == k)?, g))); + // Two objects are enough on an artboard, where they go to its edges, and when they are spaced + // from a key object. + if n < 2 || (n < 3 && board.is_none() && from_key.is_none()) { return ok(); } let (first_box, last_box) = (&items[0].1, &items[n - 1].1); @@ -1486,7 +1502,10 @@ fn distribute(s: &mut Session, p: &Value) -> Result { .iter() .enumerate() .map(|(i, (id, r))| { - let target = first + (last - first) * i as f64 / (n - 1) as f64; + let target = match from_key { + Some((k, g)) => key(&items[k].1, horiz) + g * (i as f64 - k as f64), + None => first + (last - first) * i as f64 / (n - 1) as f64, + }; let delta = target - key(r, horiz); (*id, if horiz { Vec2::new(delta, 0.0) } else { Vec2::new(0.0, delta) }) }) @@ -1499,11 +1518,7 @@ fn distribute_spacing(s: &mut Session, p: &Value) -> Result { let horiz = choice(p, "object.distributeSpacing", "axis", &["horizontal", "vertical"])? != Some("vertical"); let preview = preview_bounds(s, p, "object.distributeSpacing")?; let board = distribute_to(s, p, &ids, preview, "object.distributeSpacing")?; - let spacing = p - .get("spacing") - .filter(|v| !v.is_null()) - .map(|v| v.as_f64().filter(|g| g.is_finite()).ok_or_else(|| bad("object.distributeSpacing", "`spacing` must be a number"))) - .transpose()?; + let spacing = spacing(p, "object.distributeSpacing")?; if !s.doc()?.selection.anchors.is_empty() { return distribute_anchors(s, "Distribute Spacing", horiz, spacing); } diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index db552a844..cb2ff2147 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -113,6 +113,25 @@ fn a_selected_group_that_contains_the_key_object_stays_in_place() { } } +/// With a spacing, Distribute moves a compound path or selected group that contains the key object +/// as one object and keeps it in place: the other object's left (top) edge goes that far from its +/// left (top) edge. +#[test] +fn distribute_with_a_spacing_keeps_what_contains_the_key_object_in_place() { + for (make, add) in [(&["object.compoundPath.make"][..], false), (&["object.group"], true), (&["object.group", "object.group"], true)] { + let (mut s, key, outer, r) = key_inside(make); + if add { + s.execute("select.add", &json!({"ids": [outer.0]})).unwrap(); + } + assert_eq!(s.doc().unwrap().selection.key, Some(key), "{make:?}"); + s.execute("object.distribute", &json!({"horizontal": "left", "spacing": 5, "bounds": "geometric"})).unwrap(); + assert_eq!(starts(&s, &[outer, r], false), [0.0, 5.0], "{make:?}: 5 pt right of it, it stays"); + s.execute("edit.undo", &json!({})).unwrap(); + s.execute("object.distribute", &json!({"vertical": "top", "spacing": 5, "bounds": "geometric"})).unwrap(); + assert_eq!(starts(&s, &[outer, r], true), [90.0, 85.0], "{make:?}: 5 pt above it, it stays"); + } +} + #[test] fn arrange_order() { let mut s = session(); @@ -452,6 +471,7 @@ fn align_and_distribute_reject_bad_params_without_changing_state() { ("object.distribute", json!({"vertical": "centre"}), v), ("object.distribute", json!({"horizontal": "left", "vertical": "middle"}), v), ("object.distribute", json!({"horizontal": "left", "to": "page"}), to), + ("object.distribute", json!({"horizontal": "left", "spacing": "5"}), "`spacing` must be a number"), ("object.distributeSpacing", json!({"axis": "diagonal"}), "`axis` must be one of horizontal, vertical"), ("object.distributeSpacing", json!({"axis": "vertical", "spacing": "5"}), "`spacing` must be a number"), ("object.distributeSpacing", json!({"axis": "vertical", "to": 0}), to), @@ -599,6 +619,39 @@ fn distribute_spacing_to_artboard_spans_it_with_equal_gaps() { } } +/// With a spacing and a key object, each Distribute button keeps the key object (20, 60 and 40 pt +/// wide or tall, the key in the middle) in place and places the left edges, centers or right edges +/// (top edges, centers or bottom edges) that far apart, outward from the key's, in their order on +/// that line. A spacing of 0 puts them all on the key's line, and two objects are enough. +/// Without a key object the spacing is not used, and with one it is used with Align to Artboard too. +#[test] +fn distribute_with_a_spacing_spaces_the_objects_from_the_key_object() { + for vertical in [false, true] { + let mut s = Session::new(); + s.execute("file.new", &json!({"width": 400, "height": 400})).unwrap(); + let ids = selected_row(&mut s, &[(100.0, 20.0), (150.0, 60.0), (260.0, 40.0)], vertical); + let (axis, lines) = if vertical { ("vertical", ["top", "center", "bottom"]) } else { ("horizontal", ["left", "center", "right"]) }; + s.execute("object.distribute", &json!({axis: lines[0], "spacing": 10})).unwrap(); + assert_eq!(starts(&s, &ids, vertical), [100.0, 180.0, 260.0], "{axis}: no key object, evenly between the outermost"); + s.execute("edit.undo", &json!({})).unwrap(); + s.select(|_, sel| sel.key = Some(ids[1])).unwrap(); + for (line, at) in lines.into_iter().zip([[140.0, 150.0, 160.0], [160.0, 150.0, 170.0], [180.0, 150.0, 180.0]]) { + for to in [json!("key"), json!("artboard"), json!(null)] { + s.execute("object.distribute", &json!({axis: line, "spacing": 10, "to": to})).unwrap(); + assert_eq!(starts(&s, &ids, vertical), at, "{axis} {line} to {to}: 10 pt from the key"); + s.execute("edit.undo", &json!({})).unwrap(); + } + } + s.execute("object.distribute", &json!({axis: lines[0], "spacing": 0, "to": "key"})).unwrap(); + assert_eq!(starts(&s, &ids, vertical), [150.0; 3], "{axis}: on the key's line"); + s.execute("edit.undo", &json!({})).unwrap(); + s.execute("select.set", &json!({"ids": [ids[0].0, ids[2].0]})).unwrap(); + s.select(|_, sel| sel.key = Some(ids[2])).unwrap(); + s.execute("object.distribute", &json!({axis: lines[0], "spacing": 30, "to": "key"})).unwrap(); + assert_eq!(starts(&s, &[ids[0], ids[2]], vertical), [230.0, 260.0], "{axis}: two objects"); + } +} + #[test] fn align_both_centers() { let mut s = session(); diff --git a/crates/engine/tests/command_sweep.rs b/crates/engine/tests/command_sweep.rs index 86ed41ec9..682296e0b 100644 --- a/crates/engine/tests/command_sweep.rs +++ b/crates/engine/tests/command_sweep.rs @@ -121,6 +121,23 @@ fn bug_huge_transform_makes_document_unloadable() { } } +/// Distribute with a huge spacing from each of the three selected objects as the key object leaves +/// a document that saves and opens again. +#[test] +fn distribute_with_a_huge_spacing_from_the_key_object() { + for key in 0..3 { + for p in [json!({"horizontal": "left", "spacing": 1e308}), json!({"vertical": "bottom", "spacing": -1e308})] { + let mut s = Fixture::Multi.session(); + let id = s.doc().unwrap().selection.objects[key]; + s.execute("select.key", &json!({"id": id.0})).unwrap(); + if no_panic(s.execute("object.distribute", &p)).is_ok() { + check_session(&s).unwrap_or_else(|e| panic!("key {key}, {p}: {e}")); + vectorcraft_testkit::invariants::check_native_roundtrip(&s.doc().unwrap().doc).unwrap_or_else(|e| panic!("key {key}, {p}: {e}")); + } + } + } +} + #[test] fn bug_artboard_huge_props_break_svg() { for key in ["x", "y", "width", "height"] { diff --git a/crates/ui-egui/src/panels/align.rs b/crates/ui-egui/src/panels/align.rs index d1d92256f..2902fbad9 100644 --- a/crates/ui-egui/src/panels/align.rs +++ b/crates/ui-egui/src/panels/align.rs @@ -1,5 +1,5 @@ -//! Align panel: Align Objects, Distribute Objects, Distribute Spacing (with a spacing value when -//! aligning to a key object) and Align To (selection / key object / artboard). +//! Align panel: Align Objects, Distribute Objects and Distribute Spacing (both with the spacing +//! value when aligning to a key object) and Align To (selection / key object / artboard). use egui::Ui; use serde::Serialize; @@ -133,6 +133,11 @@ fn align_to(app: &VectorcraftApp) -> AlignTo { pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let n = selection_len(app); let to = align_to(app); + // `spacing` is the Distribute Spacing value. The Distribute Objects and Distribute Spacing + // buttons send it while there is a key object. + let spacing: f64 = pstate::>(ui.ctx(), "align-spacing").unwrap_or(0.0); + let key = to == AlignTo::Key; + let sp = if key { Some(spacing) } else { None }; // Two or more objects, or anchor points, which may all be on one path. let distributes = n >= 2 || has_anchors(app); widgets::subheader(ui, tl!("Align Objects:")); @@ -160,7 +165,11 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { ui.add_space(6.0); } if widgets::icon_button_enabled(ui, icon, tl!(tip), false, distributes, 32.0).clicked() { - app.run("object.distribute", json!({*axis: v, "to": to.param()})).ok(); + let mut p = json!({*axis: v, "to": to.param()}); + if let Some(g) = sp { + p["spacing"] = json!(g); + } + app.run("object.distribute", p).ok(); } } }); @@ -171,11 +180,8 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { ui.horizontal(|ui| { ui.vertical(|ui| { widgets::subheader(ui, tl!("Distribute Spacing:")); - let spacing: f64 = pstate::>(ui.ctx(), "align-spacing").unwrap_or(0.0); - let key = to == AlignTo::Key; ui.horizontal(|ui| { let on = distributes; - let sp = if key { Some(spacing) } else { None }; if widgets::icon_button_enabled(ui, "dc-dist-vspace", tl!("Vertical Distribute Space"), false, on, 28.0).clicked() { app.run("object.distributeSpacing", json!({"axis": "vertical", "spacing": sp, "to": to.param()})).ok(); } @@ -539,6 +545,41 @@ mod tests { assert_eq!(lefts(&app), [x0, x0 + 80.0, x1 - 20.0], "60 pt gaps: {}", app.ui.status); } + /// With a key object, the Align panel's Distribute Objects buttons send the Distribute Spacing + /// value: Horizontal Distribute Left puts the others' left edges on the key's left edge with + /// the default value of 0, and puts them 15 pt apart from it with 15. Without a key object + /// they send no spacing. + #[test] + fn the_distribute_objects_buttons_send_the_spacing_with_a_key_object() { + let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default()); + app.run("file.new", json!({"width": 200, "height": 200})).unwrap(); + let ids: Vec = [(10, 20), (50, 40), (120, 20)] + .into_iter() + .map(|(x, w)| app.run("shape.rectangle", json!({"x": x, "y": 10, "width": w, "height": 10})).unwrap()["id"].clone()) + .collect(); + app.run("select.set", json!({"ids": ids})).unwrap(); + let lefts = |app: &VectorcraftApp| ids.iter().map(|id| left_edge(app, id)).collect::>(); + // The command and its `to` and `spacing` params (the app adds the active artboard). + let sent = |app: &VectorcraftApp| { + let (id, p) = app.session.journal.last().unwrap(); + (id.clone(), p["to"].clone(), p.get("spacing").cloned()) + }; + let ctx = egui::Context::default(); + let (left, _) = distribute_buttons(&mut app, &ctx); + click(&ctx, &mut app, left.center(), show); + assert_eq!(sent(&app), ("object.distribute".to_string(), json!("selection"), None), "no key object"); + assert_eq!(lefts(&app), [10.0, 65.0, 120.0], "{}", app.ui.status); + app.run("edit.undo", json!({})).unwrap(); + app.run("select.key", json!({"id": ids[1]})).unwrap(); + click(&ctx, &mut app, left.center(), show); + assert_eq!(sent(&app), ("object.distribute".to_string(), json!("key"), Some(json!(0.0)))); + assert_eq!(lefts(&app), [50.0; 3], "on the key's left edge: {}", app.ui.status); + app.run("edit.undo", json!({})).unwrap(); + set_pstate(&ctx, "align-spacing", Some(15.0)); + click(&ctx, &mut app, left.center(), show); + assert_eq!(lefts(&app), [35.0, 50.0, 65.0], "15 pt apart: {}", app.ui.status); + } + /// Runs the Object → Align item `label` as a click on it does. fn object_align(app: &mut VectorcraftApp, label: &str) { let entry = crate::menus::menu_entries(app).into_iter().find(|e| e.path == ["Object", "Align"] && e.label == label).expect("menu item"); diff --git a/docs/mcp.md b/docs/mcp.md index 344fef8a6..b30f7d698 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -1807,9 +1807,17 @@ evenly by that line between them, and two objects are enough. `object.distribute left or top edge of the first object, in the order of those edges, on the artboard's left or top edge, and it puts the right or bottom edge of the last object on the artboard's right or bottom edge, with equal gaps between the objects. With `to: "selection"` or `"key"`, or without `to`, `object.distribute` and `object.distributeSpacing` without `spacing` -keep the first and last objects where they are, and `object.distribute` moves nothing with fewer than three objects. -With `spacing`, `object.distributeSpacing` does not use `to`. With anchor points selected (Direct Selection), both -commands space the points among themselves and do not use `to` or `artboard`. +keep the first and last objects where they are, and `object.distribute` without `spacing` moves nothing with fewer than +three objects. With `spacing`, `object.distributeSpacing` does not use `to`, and `object.distribute` does not use it +while the selection has a key object. With anchor points selected (Direct Selection), both commands space the points +among themselves and do not use `to` or `artboard`. + +`object.distribute {spacing}` (the Distribute Objects buttons while the selection has a key object, `select.key`) keeps +the key object where it is, with the compound path or selected group that contains it. It places the objects' lines +(left edges, centers or right edges; top edges, centers or bottom edges) `spacing` points apart, outward from the key +object's line, in their order on that line. With `spacing: 0`, every object ends on the key object's line. Two objects +are enough. Without a key object, or with anchor points selected, `object.distribute` does not use `spacing`. A +`spacing` that is not a number is an error. The desktop app adds its active artboard, the one the Artboards panel highlights, to the `object.align`, `object.distribute`, `object.distributeSpacing` and `select.allOnArtboard` calls it runs without `artboard`, as it does diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 1d29a05bd..df4b8a329 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -509,6 +509,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur Choosing the Artboard tool (Shift+O, its toolbar button, or Edit Artboards in Properties or Document Setup) starts it on the active artboard: the canvas outlines that artboard, and Delete, Cut, Copy, the Transform panel and Artboard Options act on it. In the Artboards panel, a click on the active artboard's options icon, or a double-click on another row's icon, opens Artboard Options for that row's artboard, and the panel menu's Artboard Options… opens it for the active artboard (part of #1876). Both used to choose the Artboard tool instead. Opened while another tool is in use, the dialog moves the art with a new X or Y when the Artboard tool's Move Artwork with Artboard option is on. With Align to Artboard chosen, the Align panel's Distribute Objects and Distribute Spacing buttons use the active artboard (`object.distribute` and `object.distributeSpacing` with `to: "artboard"` and `artboard`). The first object's left or top edge goes to the artboard's left or top edge, and the last object's right or bottom edge goes to the artboard's right or bottom edge. Distribute Objects spaces the other objects evenly by its button's line, and two objects are enough. Distribute Spacing makes the gaps between the objects equal. With anchor points selected, the buttons space the points among themselves and do not use the artboard. When an action plays, its Distribute Objects and Distribute Spacing steps with Align to Artboard use the artboard that is active then. + With a key object, the Align panel's Distribute Objects buttons use the Distribute Spacing value (`object.distribute {spacing}`): the key object, or the compound path or selected group that contains it, stays in place, and the objects' left edges, centers or right edges (top edges, centers or bottom edges) end up that far apart, outward from the key object's, in their order. With the field's default value of 0, a click puts every object on the key object's line (behavior reference: [Ask Mordy: Distributing Objects](http://rwillustrator.blogspot.com/2008/01/ask-mordy-distribute.html)). Choosing the Artboard tool (Shift+O, its toolbar button, Edit Artboards in Properties or Document Setup, or the Artboards panel menu's Artboard Options…) starts it on the active artboard: the canvas outlines that artboard, and Delete, Cut, Copy, the Transform panel and Artboard Options act on it. Object › Artboards › Fit to Artwork Bounds and Fit to Selected Art resize the active artboard, as the Artboards panel menu's Fit to Artwork Bounds does (`artboard.fitToArt {index}`, `artboard.fitToSelection {index}`). They used to resize artboard 1 whichever artboard was active. When an action plays, its Fit to Artwork Bounds and Fit to Selected Art steps that name no artboard resize the artboard that is active then. The canvas draws the active artboard's edge in black, as the public [Select artboards](https://helpx.adobe.com/illustrator/desktop/create-manage-artboards/add-edit-artboards/select-artboards.html) page describes. It draws the other artboards' edges in gray. With several artboards selected by the Artboard tool, the canvas draws the black edge around the active one, which the tool shows with handles and its name. It used to draw artboard 1's edge in black whichever artboard was active.