diff --git a/crates/engine/src/cmd/place/mod.rs b/crates/engine/src/cmd/place/mod.rs index 6719dbedf..4490f57cd 100644 --- a/crates/engine/src/cmd/place/mod.rs +++ b/crates/engine/src/cmd/place/mod.rs @@ -222,7 +222,7 @@ fn load(p: &Value, cmd: &str, board: Option) -> Result { // default, as Illustrator places one, it is its art's bounds. let framed = matches!(format.id, "svg" | "svgz") && crop && p.get("crop").is_some(); let frame = doc.artboards.first().map(|a| a.rect).filter(|_| framed); - (art_of(layers.iter().filter(|l| placeable(l)).flat_map(|l| l.children().into_iter().flatten())), frame) + (art_of(layer_art(&layers)), frame) } else { // The page placed is a PDF import's only one. let index = if pdf { 0 } else { page - 1 }; @@ -240,7 +240,16 @@ fn load(p: &Value, cmd: &str, board: Option) -> Result { /// The placeable art of `layers` that lies on `board`. fn on_artboard(layers: &[Arc], board: Rect) -> Vec { let on_board = |c: &&Arc| c.visual_bounds().is_some_and(|b| b.intersect(board).area() > 0.0 || board.contains(b.origin())); - art_of(layers.iter().filter(|l| placeable(l)).flat_map(|l| l.children().into_iter().flatten()).filter(on_board)) + art_of(layer_art(layers).filter(on_board)) +} + +/// The objects of the placeable `layers`, a clipping layer as itself: its objects alone would lose +/// their clip, and its clipping path would come in as a loose path. +fn layer_art(layers: &[Arc]) -> impl Iterator> { + layers.iter().filter(|l| placeable(l)).flat_map(|l| match &l.kind { + NodeKind::Layer { clip: true, .. } => std::slice::from_ref(l), + _ => l.children().map_or(&[][..], Vec::as_slice), + }) } /// A clipping path of rectangle `r`, with id `id`. @@ -257,14 +266,15 @@ fn placeable(l: &Node) -> bool { l.visible && !matches!(l.kind, NodeKind::Layer { template: true, .. }) } -/// The placeable art among `nodes`: guides left out, visible sublayers as groups. +/// The placeable art among `nodes`: guides left out, visible sublayers as groups (clip groups for +/// clipping layers). fn art_of<'a>(nodes: impl Iterator>) -> Vec { nodes .filter_map(|c| match &c.kind { NodeKind::Path { guide: true, .. } => None, - NodeKind::Layer { children, .. } => placeable(c).then(|| { + NodeKind::Layer { children, clip, .. } => placeable(c).then(|| { let mut g = (**c).clone(); - g.kind = NodeKind::Group { children: art_of(children.iter()).into_iter().map(Arc::new).collect(), clip: false }; + g.kind = NodeKind::Group { children: art_of(children.iter()).into_iter().map(Arc::new).collect(), clip: *clip }; g }), _ => Some((**c).clone()), diff --git a/crates/engine/src/tests_place.rs b/crates/engine/src/tests_place.rs index 35cd5f787..16dcf574e 100644 --- a/crates/engine/src/tests_place.rs +++ b/crates/engine/src/tests_place.rs @@ -375,3 +375,20 @@ fn an_svg_placed_with_crop_is_framed_by_its_viewbox() { assert!(near(path_bounds(&s, &r["ids"][0]), Rect::new(120.0, 100.0, 280.0, 300.0)), "{r}"); } } + +/// An SVG whose top-level group is clipped is placed as one clip group: the clip is kept, and its +/// clipping path is not left behind as a loose path. +#[test] +fn a_clipped_top_level_group_stays_clipped() { + let svg = br##" + + +"##; + let mut s = session(); + let r = place(&mut s, "c.svg", svg, json!({"link": false})); + assert_eq!((r["width"].as_f64().unwrap(), r["height"].as_f64().unwrap()), (200.0, 100.0), "{r}"); + let placed = node(&s, &r["ids"][0]); + let NodeKind::Group { children, clip: false } = &placed.kind else { panic!("{:?}", placed.kind) }; + let [inner] = children.as_slice() else { panic!("one object: {children:?}") }; + assert!(matches!(&inner.kind, NodeKind::Group { children, clip: true } if children.len() == 2), "{:?}", inner.kind); +}