Place keeps a clipped group's clip

Place flattened every top-level layer into its objects, so an SVG's clipped top-level group lost
its clip and its clipping path came in as a loose, invisible path; sublayers lost their clips the
same way. A clipping layer is now placed as one clip group, for SVG/DXF and for the art placed from
an artboard (PDF, EPS, placed documents) alike.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
lloyd
2026-10-16 02:01:50 +07:00
committed by bflatastic
co-authored by Claude Opus 5.5
parent d0c302c1a3
commit 8d288337f5
2 changed files with 32 additions and 5 deletions
+15 -5
View File
@@ -222,7 +222,7 @@ fn load(p: &Value, cmd: &str, board: Option<Rect>) -> Result<Loaded> {
// 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<Rect>) -> Result<Loaded> {
/// The placeable art of `layers` that lies on `board`.
fn on_artboard(layers: &[Arc<Node>], board: Rect) -> Vec<Node> {
let on_board = |c: &&Arc<Node>| 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<Node>]) -> impl Iterator<Item = &Arc<Node>> {
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<Item = &'a Arc<Node>>) -> Vec<Node> {
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()),
+17
View File
@@ -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##"<svg xmlns="http://www.w3.org/2000/svg" width="200" height="100" viewBox="0 0 200 100">
<defs><clipPath id="c"><path d="M0 0 L200 0 L200 100 L0 100 Z"/></clipPath></defs>
<g id="Layer_1" clip-path="url(#c)"><path d="M100 -20 L250 -20 L250 40 L100 40 Z" fill="#f00"/></g>
</svg>"##;
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);
}