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 <[email protected]>
This commit is contained in:
bflatasticandLuca Lorenz committed 2026-10-12 16:43:41 +07:00
1 parent 8716fc8fbc
commit 8b96db8f0b
2 files changed
+59

No files matched your search

+23
View File
@@ -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<NodeId> {
// 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<NodeId>) {
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();
+36
View File
@@ -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<Rect> = 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();