Merge pull request #7585 from dotCooCoo/fix/clipping-type-fill-effects

Export: clipping type with geometry effects clips where the canvas does
This commit is contained in:
BFlat
2026-11-14 21:50:33 +07:00
committed by GitHub
2 changed files with 110 additions and 1 deletions
+59
View File
@@ -201,6 +201,9 @@ fn bake_node(d: &mut Document, n: &Node) -> Option<Node> {
{
return bake_leaf(d, n);
}
if let Some(m) = clip_group_with_type_clip(d, n) {
return Some(m);
}
let mut m = n.clone();
// A clipping path takes the geometry effects in it as the canvas clips by it, those of a
// compound path's paths before the compound path's own ([`crate::clip::with_clip_effects_applied`]);
@@ -226,6 +229,62 @@ fn bake_node(d: &mut Document, n: &Node) -> Option<Node> {
bake_leaf(d, n)
}
/// Clip group or clipping layer `n` whose clipping path is type with geometry effects (its own, its
/// fills' or strokes', or a perspective), baked as the canvas draws it: the type becomes a
/// clipping path of the region the canvas clips to ([`crate::clip_outline`]), its fills (with their
/// effects baked) paint behind the clipped art inside the clip, and its strokes paint over it
/// outside the clip, in a group that `n` becomes with its clip group inside ([`Node::clip_paint`]).
/// The other members are baked as usual. `None` for other objects and when the type has nothing to
/// clip by.
fn clip_group_with_type_clip(d: &mut Document, n: &Node) -> Option<Node> {
let (first, rest) = n.children()?.split_first()?;
if !n.clips()
|| !matches!(first.kind, NodeKind::Text(_))
|| !(has_geometry(&first.appearance.effects) || first.projection().is_some() || has_item_geometry(first))
{
return None;
}
let (bp, rule) = crate::clip_outline(first)?;
let mut clip = Node::path(first.id, PathData::from_bezpath(&bp), Appearance::default());
if let NodeKind::Path { rule: r, clipping, .. } = &mut clip.kind {
(*r, *clipping) = (rule, true);
}
clip.name = first.name.clone();
clip.visible = first.visible;
let paint = first.clip_paint();
// The type's fills and strokes as art: baked with the type's id, as the renderer draws them,
// then given ids of their own.
let mut seen = std::collections::HashSet::from([n.id, first.id]);
let mut art = |d: &mut Document, part: Option<Node>| {
part.map(|p| {
let mut a = bake_node(d, &p).unwrap_or(p);
fresh_ids(d, &mut a, &mut seen);
Arc::new(a)
})
};
let fill = art(d, paint.fill);
let stroke = art(d, paint.stroke);
let members: Vec<Arc<Node>> =
std::iter::once(Arc::new(clip)).chain(fill).chain(rest.iter().map(|c| bake_node(d, c).map(Arc::new).unwrap_or_else(|| c.clone()))).collect();
let mut m = n.clone();
let Some(stroke) = stroke else {
if let Some(ch) = m.children_mut() {
*ch = members;
}
return Some(m);
};
// `n` keeps its id, name and transparency, and holds the clip group and the strokes over it.
let inner = Node::new(d.alloc_id(), NodeKind::Group { children: members, clip: true });
match &mut m.kind {
NodeKind::Group { children, clip } | NodeKind::Layer { children, clip, .. } => {
*children = vec![Arc::new(inner), stroke];
*clip = false;
}
_ => return None,
}
Some(m)
}
/// `m`, art evaluated from an object, with its children baked and then given ids of their own (the
/// pieces copied from the source keep the source's ids until then). The pieces are baked with the
/// ids the renderer draws them with: Random in a Transform effect takes its seed from the id.
+51 -1
View File
@@ -2,7 +2,7 @@ use super::*;
use serde_json::json;
use vectorcraft_color::{Color, Paint};
use vectorcraft_doc::{CharStyle, Effect, NodeId, TextObject};
use vectorcraft_geom::{Point, Rect, shapes};
use vectorcraft_geom::{Point, Rect, Shape, shapes};
/// Type tests need glyph outlines, which only the optional craft-fonts build input provides.
fn skip_without_craft_fonts() -> bool {
@@ -813,6 +813,56 @@ fn type_in_a_clipping_group_with_a_fill_of_its_own_keeps_its_effect() {
assert!(b.y0 > 330.0 && b.y1 < 370.0, "{b:?}");
}
#[test]
fn clipping_type_whose_fill_has_a_geometry_effect_clips_by_its_plain_outlines() {
if skip_without_craft_fonts() {
return;
}
let near = |a: Rect, b: Rect, tol: f64| [a.x0 - b.x0, a.y0 - b.y0, a.x1 - b.x1, a.y1 - b.y1].iter().all(|v| v.abs() < tol);
for stroke in [Paint::None, Paint::solid(Color::rgb(1.0, 0.0, 0.0))] {
// Type that clips a square, with a Transform effect on its own fill.
let mut t = type_node(60);
t.appearance = Appearance::basic(Paint::solid(Color::BLACK), stroke.clone(), 1.0).into();
for item in &mut t.appearance.items {
if let AppearanceItem::Fill(f) = item {
f.effects.push(fx("distort.transform", json!({"moveH": 300})));
}
}
let square = Node::path(NodeId(61), shapes::rectangle(Rect::new(0.0, 0.0, 100.0, 100.0)), Appearance::default_art());
let clip = Node::new(NodeId(62), NodeKind::Group { children: vec![Arc::new(t.clone()), Arc::new(square)], clip: true });
let canvas = crate::clip_outline(&t).map(|(bp, _)| bp.bounding_box()).unwrap();
assert!(canvas.x1 < 100.0, "the canvas clips by the plain outlines: {canvas:?}");
let d = bake_document(&doc_with(clip)).unwrap();
let baked = d.node(NodeId(62)).unwrap();
// With a stroke, the clip group sits in a group with the stroke over it, outside the clip.
let group = match baked.kind {
NodeKind::Group { clip: true, .. } => {
assert!(stroke.is_none(), "{baked:?}");
baked
}
_ => {
let ch = baked.children().unwrap();
assert_eq!(ch.len(), 2, "{baked:?}");
let strokes = ch[1].appearance.items.iter().all(|i| matches!(i, AppearanceItem::Stroke(_)));
assert!(matches!(ch[1].kind, NodeKind::Text(_)) && strokes, "the type with only its stroke: {:?}", ch[1]);
&ch[0]
}
};
let ch = group.children().unwrap();
assert!(matches!(group.kind, NodeKind::Group { clip: true, .. }) && ch.len() == 3, "{group:?}");
// The export clips by the outlines the canvas clips by.
assert!(matches!(ch[0].kind, NodeKind::Path { clipping: true, .. }), "{:?}", ch[0]);
let b = crate::clip_outline(&ch[0]).map(|(bp, _)| bp.bounding_box()).unwrap();
assert!(near(b, canvas, 1e-6), "canvas {canvas:?}, export {b:?}");
// The fills (the characters' and the moved one) paint behind the square, inside the clip.
let fill = ch[1].geometric_bounds().unwrap();
assert!((fill.x0 - canvas.x0).abs() < 1e-6 && fill.x1 > canvas.x1 + 250.0, "{fill:?}");
assert_eq!(ch[2].id, NodeId(61));
let mut ids = std::collections::HashSet::new();
baked.walk(&mut |n| assert!(ids.insert(n.id), "duplicate id {}", n.id));
}
}
#[test]
fn type_with_its_own_transform_effect_copies_stays_type() {
if skip_without_craft_fonts() {