diff --git a/crates/effects/src/group.rs b/crates/effects/src/group.rs index bb2f605a91..4df0a105ce 100644 --- a/crates/effects/src/group.rs +++ b/crates/effects/src/group.rs @@ -661,8 +661,9 @@ pub fn has_container_appearance(n: &Node) -> bool { /// The regions a group's or layer's own fills and strokes paint, in document space, each with its /// fill rule: every visible member path and compound path, type as its glyph outlines and live /// objects evaluated; a compound shape as its area, with the symbol instances among its members -/// as their symbols' art from `doc`; the members of a clip group or clipping layer without its -/// clipping path. Images, other symbol instances and guides add nothing. A Repeat counts the +/// as their symbols' art from `doc`; a clip group or clipping layer (`n` itself or one among its +/// members) as the region its clipping path clips to ([`crate::clip::clip_outline`]), so members +/// it hides add nothing. Images, other symbol instances and guides add nothing. A Repeat counts the /// Repeats in the art of `doc`'s symbols that its instances show toward its instance budget. pub fn member_shapes(doc: Option<&Document>, n: &Node) -> Vec<(PathData, FillRule)> { fn push(doc: Option<&Document>, n: &Node, out: &mut Vec<(PathData, FillRule)>) { @@ -690,8 +691,14 @@ pub fn member_shapes(doc: Option<&Document>, n: &Node) -> Vec<(PathData, FillRul } } fn members(doc: Option<&Document>, n: &Node, out: &mut Vec<(PathData, FillRule)>) { - let clip = usize::from(n.clips()); - for c in n.children().into_iter().flatten().skip(clip) { + // A clip group shows its members only inside its clipping path: its outline is that + // region, not the clipped members' own shapes, which may reach far outside it (#7289). + if n.clips() { + let clip = n.children().and_then(|ch| ch.first()); + out.extend(clip.and_then(|c| crate::clip::clip_outline(c)).map(|(bp, rule)| (PathData::from_bezpath(&bp), rule))); + return; + } + for c in n.children().into_iter().flatten() { push(doc, c, out); } } diff --git a/crates/effects/src/group/tests.rs b/crates/effects/src/group/tests.rs index c6e2a78c73..8e00718c01 100644 --- a/crates/effects/src/group/tests.rs +++ b/crates/effects/src/group/tests.rs @@ -1757,3 +1757,38 @@ fn a_sublayers_ruler_guide_in_a_hidden_or_locked_group_stays_hidden_or_locked() assert_eq!(guides, [(NodeId(20), !hidden, locked)], "hidden {hidden}, locked {locked}"); } } + +/// A group's own Fill + Offset Path below its contents follows the outline of a clip group among +/// its members, its clipping path, not the clipped art it hides outside that path (#7289). +#[test] +fn a_groups_own_fill_follows_a_clip_groups_clipping_path_not_its_hidden_art() { + let mut clip = square_at_100(); + clip.id = NodeId(2); + if let NodeKind::Path { clipping, .. } = &mut clip.kind { + *clipping = true; + } + // The clipped art reaches 100 pt past the clipping path on either side. + let hidden = Node::path(NodeId(3), shapes::rectangle(Rect::new(0.0, 100.0, 300.0, 200.0)), black()); + let clipped = Node::new(NodeId(5), NodeKind::Group { children: vec![Arc::new(clip), Arc::new(hidden)], clip: true }); + let mut g = Node::group(NodeId(10), vec![Arc::new(clipped.clone())]); + let mut fill = vectorcraft_doc::FillLayer::new(Paint::solid(Color::rgb(1.0, 0.0, 0.0))); + fill.effects.push(offset_10()); + g.appearance.items = vec![AppearanceItem::Fill(fill)]; + g.appearance.set_contents_at(1); + let near = |a: Rect, b: Rect| (a.x0 - b.x0).abs() < 0.5 && (a.y0 - b.y0).abs() < 0.5 && (a.x1 - b.x1).abs() < 0.5 && (a.y1 - b.y1).abs() < 0.5; + let expected = Rect::new(90.0, 90.0, 210.0, 210.0); + + let art = evaluate_container(None, &g).unwrap(); + let ch = art.children().unwrap(); + assert_eq!(ch.len(), 2, "the fill's art below the clip group"); + assert_eq!(ch[0].id, NodeId(10)); + let b = drawn_bounds(&ch[0]).unwrap(); + assert!(near(b, expected), "the offset fill hugs the clipping path: {b:?}"); + + // The clip group itself with that fill: the same outline. + let mut own = clipped; + own.appearance = g.appearance.clone(); + assert_eq!(member_shapes(None, &own).len(), 1); + let b = member_shapes(None, &own)[0].0.bounds().unwrap(); + assert!(near(b, Rect::new(100.0, 100.0, 200.0, 200.0)), "the clipping path's region: {b:?}"); +}