mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-14 21:44:57 +00:00
Export: clipping type with geometry effects clips where the canvas does
Type used as a clip group's clipping path, with a geometry effect on the object, on one of its fills or strokes, or a perspective, was baked into its outlined pieces with the effects applied, so SVG, PDF and the other exports clipped by all of them while the canvas clips by the plain glyph outlines. The baked clipping path was also a group, so the type's fills and strokes were not painted. bake_node now bakes such a clip group as the canvas draws it: the clipping path is a path of the region effects::clip_outline gives, the type's fills (effects baked) paint behind the clipped art inside the clip, and its strokes paint over it outside the clip, in a group that keeps the clip group's id and transparency.
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();
|
||||
let baked: Vec<Arc<Node>> = ch.iter().map(|c| bake_node(d, c).map(Arc::new).unwrap_or_else(|| c.clone())).collect();
|
||||
if let Some(ch) = m.children_mut() {
|
||||
@@ -211,6 +214,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_is_outlined_plain() {
|
||||
assert!(b.y0 > 30.0 && b.y1 < 70.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