Re-render shapes when an artboard move grows the canvas (#2389) (#2623)

Shape layers render their pixels clipped to the canvas. Moving an
artboard (layer.artboard.set or layer.translate) re-rendered its shapes
before fit_canvas grew the document, so a shape moved past the old edge
came out missing or cut off until another shape edit refreshed it.

fit_canvas now re-renders every shape whenever it changes the canvas
size, which covers all its callers.

Co-authored-by: Fabio Lussi <198260595+itrycodinganditsbad@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Fabio Lussi 2026-10-10 19:41:47 +02:00 committed by GitHub
parent 0a7b8cab54
commit 14599472af
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 48 additions and 1 deletions

View File

@ -188,7 +188,13 @@ pub(crate) fn fit_canvas(doc: &mut Document) {
w = w.max(i64::from(a.rect.x1));
h = h.max(i64::from(a.rect.y1));
}
doc.size = Size::new(w.clamp(1, 300_000) as u32, h.clamp(1, 300_000) as u32);
let size = Size::new(w.clamp(1, 300_000) as u32, h.clamp(1, 300_000) as u32);
if size != doc.size {
doc.size = size;
// Shape pixels are cut at the canvas: a board moved past the old edge rendered its
// shapes against it, leaving them missing or clipped (#2389).
crate::canvas_geom::refresh(doc, crate::canvas_geom::Refresh::Shapes);
}
}
fn next_artboard_name(doc: &Document) -> String {

View File

@ -385,3 +385,44 @@ fn moving_an_artboard_past_the_canvas_grows_it() {
s.undo();
assert_eq!((doc(&s).size.width, doc(&s).size.height), (100, 80));
}
#[test]
fn moving_an_artboard_past_the_canvas_keeps_its_shapes_whole() {
// #2389: a shape's pixels are cut at the canvas; the board moved before the canvas grew,
// so its shape rendered against the old edge and stayed missing or clipped.
let shape_board = |s: &mut Session| {
s.execute("file.new", json!({"width": 100, "height": 100, "background": "transparent"})).unwrap();
let sh = s.execute("shape.create", json!({"kind": "rect", "rect": [10, 20, 30, 30], "fill": "#ff0000"})).unwrap()["layer"].as_u64().unwrap();
s.execute("layer.new.artboardFromLayers", json!({})).unwrap();
sh
};
for (cmd, p, size, red, not_red) in [
("layer.artboard.set", json!({"x": 120}), (150, 100), [(121, 21), (130, 30), (149, 49)], None),
("layer.translate", json!({"dx": 110, "dy": 0}), (150, 100), [(121, 21), (130, 30), (149, 49)], None),
("layer.artboard.set", json!({"y": 120}), (100, 150), [(11, 121), (20, 130), (39, 149)], None),
// Partly past the edge: the whole shape, not just the part inside the old canvas.
("layer.artboard.set", json!({"x": 90}), (120, 100), [(91, 21), (95, 30), (110, 30)], Some((15, 30))),
] {
let mut s = Session::new();
let sh = shape_board(&mut s);
s.execute(cmd, p.clone()).unwrap();
assert_eq!((doc(&s).size.width, doc(&s).size.height), size, "{cmd} {p}");
for (x, y) in red {
assert!(red_at(&s, x, y), "{cmd} {p}: ({x}, {y})");
}
if let Some((x, y)) = not_red {
assert!(!red_at(&s, x, y), "{cmd} {p}: ({x}, {y}) left behind");
}
let info = s.execute("shape.info", json!({"layer": sh})).unwrap();
assert_eq!(info["bounds"], json!([red[0].0 - 1, red[0].1 - 1, 30, 30]), "{cmd} {p}");
// One undo step back to the original board; redo brings the whole shape back.
s.undo();
assert_eq!((doc(&s).size.width, doc(&s).size.height), (100, 100));
assert!(red_at(&s, 20, 30), "{cmd} {p}: undo");
s.redo();
assert_eq!((doc(&s).size.width, doc(&s).size.height), size);
for (x, y) in red {
assert!(red_at(&s, x, y), "{cmd} {p}: redo ({x}, {y})");
}
}
}