SVG export: a chosen artboard's file leaves out the art of other artboards inside a shared group

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
EricKuck
2026-10-16 02:21:47 +07:00
committed by bflatastic
co-authored by Claude Opus 5.5
parent e22b6a5d45
commit 0a4e9ea8c8
2 changed files with 34 additions and 7 deletions
+17 -7
View File
@@ -94,13 +94,14 @@ fn selected_part(n: &Node, keep: &HashSet<NodeId>) -> Option<Node> {
}
/// `doc` with only the art over `rect` (an artboard): objects whose visual bounds miss it are left
/// out, at any depth of layers and sublayers, and objects that reach it stay whole (the file's
/// bounds clip them), as DXF's crop leaves them. Symbol instances are measured by their art, and
/// the box of one whose art holds brushed strokes grows by as far as their brush art can paint
/// ([`SymbolBoxes::with_brush_art`]). An object that holds a symbol instance also stays when what
/// the canvas paints for it reaches `rect` ([`vectorcraft_render::painted_bounds`]: the effects in
/// the symbols' art and its own effects). A clipping layer keeps every object, its clipping path
/// among them. An artboard's file then holds its own art, not a whole document of artboards.
/// out, at any depth of layers, sublayers and groups that don't clip, and objects that reach it
/// stay whole (the file's bounds clip them), as DXF's crop leaves them. Symbol instances are
/// measured by their art, and the box of one whose art holds brushed strokes grows by as far as
/// their brush art can paint ([`SymbolBoxes::with_brush_art`]). An object that holds a symbol
/// instance also stays when what the canvas paints for it reaches `rect`
/// ([`vectorcraft_render::painted_bounds`]: the effects in the symbols' art and its own effects). A
/// clipping layer keeps every object, its clipping path among them. An artboard's file then holds
/// its own art, not a whole document of artboards.
pub(super) fn art_over(doc: &Document, rect: vectorcraft_geom::Rect) -> Document {
fn kept(n: &Arc<Node>, doc: &Document, symbols: &SymbolBoxes, rect: vectorcraft_geom::Rect) -> Option<Arc<Node>> {
if matches!(n.kind, NodeKind::Layer { clip: false, .. }) {
@@ -109,6 +110,15 @@ pub(super) fn art_over(doc: &Document, rect: vectorcraft_geom::Rect) -> Document
*children = children.iter().filter_map(|c| kept(c, doc, symbols, rect)).collect();
return Some(Arc::new(layer));
}
// A group that doesn't clip keeps only its members over the artboard, so art gathered in one
// group across several artboards splits by artboard. Not with effects of its own: a shadow
// can reach in from a member outside.
if matches!(n.kind, NodeKind::Group { clip: false, .. }) && n.appearance.effects.is_empty() {
let mut group = Node::clone(n);
let children = group.children_mut()?;
*children = children.iter().filter_map(|c| kept(c, doc, symbols, rect)).collect();
return (!children.is_empty()).then(|| Arc::new(group));
}
let over = |b: vectorcraft_geom::Rect| b.x0 <= rect.x1 && rect.x0 <= b.x1 && b.y0 <= rect.y1 && rect.y0 <= b.y1;
let mut holds_instance = false;
n.walk(&mut |c| holds_instance |= matches!(c.kind, NodeKind::SymbolInstance { .. }));
@@ -74,3 +74,20 @@ fn a_chosen_artboard_s_svg_holds_only_its_own_art() {
let whole = text(&s.execute("document.serialize", &json!({"format": "svg"})).unwrap());
assert_eq!(shapes(&whole).0, 2, "nothing named: the whole document's art");
}
/// Art grouped across artboards (one variant per artboard in one group) splits by artboard too.
#[test]
fn a_chosen_artboard_s_svg_leaves_out_its_neighbours_art_in_a_shared_group() {
let mut s = Session::new();
s.execute("file.new", &json!({"width": 100, "height": 100, "artboards": 3})).unwrap();
let x = |s: &Session, b: usize| s.doc().unwrap().doc.artboards[b].rect.x0;
let (first, third) = (x(&s, 0), x(&s, 2));
s.execute("shape.rectangle", &json!({"x": first + 10.0, "y": 10, "width": 30, "height": 30})).unwrap();
s.execute("shape.ellipse", &json!({"x": third + 10.0, "y": 10, "width": 30, "height": 30})).unwrap();
s.execute("select.all", &json!({})).unwrap();
s.execute("object.group", &json!({})).unwrap();
let text = |v: &serde_json::Value| v["text"].as_str().unwrap().to_string();
let third_only = text(&s.execute("document.serialize", &json!({"format": "svg", "useArtboards": true, "range": "3"})).unwrap());
assert_eq!(third_only.matches("<path").count() + third_only.matches("<rect").count(), 1, "{third_only}");
assert!(third_only.contains(" C") || third_only.contains("<ellipse"), "{third_only}");
}