mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-14 20:36:02 +00:00
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:
@@ -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.
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user