Artboards: copied artboards take a free id and keep their own guides

Duplicate Artboards, the Artboard tool's Alt-drag (artboard.move with
copy) and Paste of a copied artboard gave the new artboard the largest
artboard id plus one, saturating. In a document with an artboard id
of 4,294,967,295 (u32::MAX), which only a hand-made file has, the copy
got that same id. Artboard guides record the id of their artboard.
Deleting either of the two artboards with that id then deleted the
guides of both, and guide.list reported the copy's guides on the
other one.

push_artboard_copy now takes the id from Document::next_artboard_id,
as artboard.new and Convert to Artboards do. Since the #1718 fix, that
function returns the smallest free id when the largest is u32::MAX.
In a document whose artboard ids are all below u32::MAX, a copy gets
the same id as before.

Docs: docs/target-app-parity.md.
This commit is contained in:
dotCooCoo
2026-10-16 02:21:46 +07:00
committed by bflatastic
parent 83759c0e94
commit ac4d69032c
3 changed files with 44 additions and 1 deletions
+1 -1
View File
@@ -166,7 +166,7 @@ pub(crate) fn copy_artboard(d: &mut vectorcraft_doc::Document, i: usize, dv: Vec
/// (`<name> copy 2`…) when that is taken → its index.
pub(crate) fn push_artboard_copy(d: &mut vectorcraft_doc::Document, src: &vectorcraft_doc::Artboard, rect: Rect) -> usize {
let mut a = src.clone();
a.id = d.artboards.iter().map(|a| a.id).max().unwrap_or(0).saturating_add(1);
a.id = d.next_artboard_id();
let taken = |name: &str| d.artboards.iter().any(|a| a.name == name);
a.name = [src.name.clone(), format!("{} copy", src.name)]
.into_iter()
+42
View File
@@ -197,6 +197,48 @@ fn artboard_tool_cut_removes_the_artboard_and_its_art() {
assert_eq!((d.artboards.len(), d.node(r).is_some()), (2, true), "the cut is one undo step");
}
/// A copy of an artboard whose id is the largest there is (only a hand-made file has one, #1718)
/// takes an id that no other artboard has, as a new artboard does, whether Duplicate Artboards, the
/// Artboard tool's Alt-drag (`artboard.move` with `copy`) or Paste makes it. Deleting either
/// artboard then leaves the other's guides.
#[test]
fn copied_artboards_take_a_free_id_and_keep_their_own_guides() {
let mut got = vec![];
for (cmd, p) in [
("artboard.duplicate", json!({"index": 0})),
("artboard.move", json!({"index": 0, "dx": 0, "dy": 900, "copy": true})),
("edit.paste", json!({})),
] {
let mut s = Session::new();
let mut d = vectorcraft_doc::Document::new(800.0, 600.0);
d.artboards[0].id = u32::MAX;
s.add_document(d, None);
s.execute("guide.add", &json!({"vertical": true, "pos": 100, "artboard": 0})).unwrap();
if cmd == "edit.paste" {
s.execute("artboard.copy", &json!({"index": 0})).unwrap();
}
s.execute(cmd, &p).unwrap();
// Each pass deletes one artboard (the copy, then the original), adds the artboard ids of
// the remaining guides to `kept`, and undoes the delete.
let mut kept = vec![];
for i in [1, 0] {
s.execute("artboard.delete", &json!({"index": i})).unwrap();
kept.push(s.doc().unwrap().doc.guides.iter().map(|g| g.artboard).collect::<Vec<_>>());
s.execute("edit.undo", &json!({})).unwrap();
}
got.push((cmd, s.doc().unwrap().doc.artboards.iter().map(|a| a.id).collect::<Vec<_>>(), kept));
}
let max = Some(u32::MAX);
assert_eq!(
got,
[
("artboard.duplicate", vec![u32::MAX, 1], vec![vec![max], vec![Some(1)]]),
("artboard.move", vec![u32::MAX, 1], vec![vec![max], vec![Some(1)]]),
("edit.paste", vec![u32::MAX, 1], vec![vec![max], vec![]]),
]
);
}
#[test]
fn swatch_groups_duplicate_sort() {
let mut s = session();