From 8b96db8f0beab1054ed27ff1975366c59ef3d5d6 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Mon, 12 Oct 2026 15:20:48 +0700 Subject: [PATCH] Artboards: art flush with an artboard moves with it despite a rounding error (#1295) A background drawn at the artboard's own position could sit 3e-14 pt outside it and stay behind when the artboard moved, was copied or scaled its art. Art within a millionth of a point of the artboard's edges now counts as inside it. The fix and both tests are from #1295 by Luca Lorenz, without its change to art that only partly overlaps the artboard. Co-authored-by: Luca Lorenz --- crates/doc/src/lib.rs | 23 ++++++++++++++++++++ crates/engine/src/tests_xform.rs | 36 ++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/crates/doc/src/lib.rs b/crates/doc/src/lib.rs index a016606a..10551b75 100644 --- a/crates/doc/src/lib.rs +++ b/crates/doc/src/lib.rs @@ -1100,6 +1100,9 @@ impl Document { /// `locked_and_hidden` (Selection & Anchor Display › Move Locked and Hidden Artwork with /// Artboard). pub fn art_on_artboard(&self, rect: Rect, locked_and_hidden: bool) -> Vec { + // Art drawn flush with the artboard can end up a rounding error outside it (a background + // 3e-14 pt past the top edge, #1295): a millionth of a point is far below anything visible. + let rect = rect.inflate(1e-6, 1e-6); fn collect(n: &Node, rect: Rect, all: bool, out: &mut Vec) { for c in n.children().into_iter().flatten().filter(|c| c.rides_with_artboard(all)) { if c.is_layer() { @@ -1405,6 +1408,26 @@ mod tests { assert_eq!(d.art_on_artboard(wide, true), vec![a, b], "a locked layer's art too"); } + #[test] + fn art_on_artboard_tolerates_roundoff_at_each_edge() { + let (d, a, _) = doc_with_rects(); + let roundoff = 1e-12; + for board in [ + Rect::new(roundoff, 0.0, 10.0, 10.0), + Rect::new(0.0, roundoff, 10.0, 10.0), + Rect::new(0.0, 0.0, 10.0 - roundoff, 10.0), + Rect::new(0.0, 0.0, 10.0, 10.0 - roundoff), + ] { + assert_eq!(d.art_on_artboard(board, false), vec![a], "roundoff at {board:?}"); + } + // A real overhang remains outside. + for board in + [Rect::new(0.001, 0.0, 10.0, 10.0), Rect::new(0.0, 0.001, 10.0, 10.0), Rect::new(0.0, 0.0, 9.999, 10.0), Rect::new(0.0, 0.0, 10.0, 9.999)] + { + assert!(d.art_on_artboard(board, false).is_empty(), "overhang at {board:?}"); + } + } + #[test] fn find_and_paths() { let (d, a, b) = doc_with_rects(); diff --git a/crates/engine/src/tests_xform.rs b/crates/engine/src/tests_xform.rs index 9ad19989..16f72da7 100644 --- a/crates/engine/src/tests_xform.rs +++ b/crates/engine/src/tests_xform.rs @@ -289,6 +289,42 @@ fn artboard_tool_moves_with_art_creates_and_deletes() { assert_eq!(s.doc().unwrap().doc.artboards.len(), 1); } +#[test] +fn artboard_drag_keeps_a_flush_background_with_roundoff_and_multiple_texts() { + let mut s = Session::new(); + s.execute("file.new", &json!({"width": 600, "height": 400})).unwrap(); + let (x, y) = (-35.23064566724116, -107.85371637852609); + s.execute("artboard.setProps", &json!({"index": 0, "x": x, "y": y})).unwrap(); + // The background's top is 2.84e-14 pt above the artboard's top. + let background = rect(&mut s, x, -107.85371637852612, 600.0, 400.0); + let mut ids = vec![background]; + for i in 0..3 { + let text = s.execute("text.create", &json!({"x": x + 100.0, "y": y + 80.0 + f64::from(i) * 60.0, "text": "Test", "size": 12})).unwrap(); + ids.push(NodeId(text["id"].as_u64().unwrap())); + } + s.execute("select.none", &json!({})).unwrap(); + let before: Vec = ids.iter().map(|id| bounds(&s, *id)).collect(); + let board = s.doc().unwrap().doc.artboards[0].rect; + let history = undo_len(&s); + gesture(&mut s, "artboard", &[(x + 300.0, y + 200.0), (x + 400.0, y + 240.0), (x + 400.0, y + 240.0)], Mods::default()); + let moved_board = s.doc().unwrap().doc.artboards[0].rect; + let delta = moved_board.origin() - board.origin(); + assert!(delta.hypot() > 1.0, "the artboard actually moved"); + for (id, was) in ids.iter().zip(&before) { + assert!(close(bounds(&s, *id), *was + delta), "{id:?} must move with the artboard"); + } + assert_eq!(undo_len(&s), history + 1); + s.execute("edit.undo", &json!({})).unwrap(); + assert_eq!(s.doc().unwrap().doc.artboards[0].rect, board); + for (id, was) in ids.iter().zip(&before) { + assert!(close(bounds(&s, *id), *was), "Undo restores {id:?}"); + } + s.execute("edit.redo", &json!({})).unwrap(); + for (id, was) in ids.iter().zip(&before) { + assert!(close(bounds(&s, *id), *was + delta), "Redo moves {id:?}"); + } +} + #[test] fn artboard_resize_via_handle_and_move_command() { let mut s = session();