mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-15 21:57:04 +00:00
Merge pull request #7755 from dotCooCoo/fix/clip-type-paint-export
Clipping: painted type keeps its fill and stroke in a group with geometry effects
This commit is contained in:
@@ -27,7 +27,9 @@ use vectorcraft_geom::{Affine, FillRule, PathData, Rect, Vec2};
|
||||
|
||||
use crate::distort::{transform_move, transform_offsets};
|
||||
use crate::group::{has_own_paint, paints};
|
||||
use crate::reshape::{as_art, clipping_text_outline, dressed, geometry_effects, glyph_outlines, outline_nested_text, reshape_leaves};
|
||||
use crate::reshape::{
|
||||
as_art, clipping_text_outline, dressed, geometry_effects, glyph_outlines, outline_nested_text, paint_type_clip, reshape_leaves,
|
||||
};
|
||||
use crate::{
|
||||
GeomContext, adjust_in_document, apply_geometry_carrying, crop_marks_art, evaluate_container, has_adjustment, has_crop_marks, has_geometry,
|
||||
has_instance_row_raster, has_item_geometry, has_pathfinder, is_pathfinder, merged_params, needs_outline, reshape,
|
||||
@@ -689,6 +691,9 @@ fn moved_type_group(n: &Node) -> Option<Node> {
|
||||
/// them. `None` when no type stays type.
|
||||
fn container_keeping_type(d: &Document, n: &Node) -> Option<Node> {
|
||||
fn keep(d: &Document, n: &mut Node, moves: &[serde_json::Value], clipping: bool, kept: &mut bool) {
|
||||
if !clipping {
|
||||
paint_type_clip(Some(d), n);
|
||||
}
|
||||
let clips = n.clips();
|
||||
let Some(ch) = n.children_mut() else { return };
|
||||
for (i, c) in ch.iter_mut().enumerate() {
|
||||
|
||||
@@ -746,9 +746,13 @@ pub(crate) fn clipping_text_outline(t: &Node) -> Option<Node> {
|
||||
|
||||
/// Replace type anywhere inside `n` by its outlines, as art that keeps the type's look
|
||||
/// ([`as_art`]). Type in a clip group's clipping path (all of `n` when `clipping` is set) becomes
|
||||
/// its outlines with its visibility and effects ([`clipping_text_outline`]). The swatches and
|
||||
/// patterns that live-effect plug-ins painting their pieces name are looked up in `doc`.
|
||||
/// its outlines with its visibility and effects ([`clipping_text_outline`]), and its fills and
|
||||
/// strokes become art of their own ([`paint_type_clip`]). The swatches and patterns that
|
||||
/// live-effect plug-ins painting their pieces name are looked up in `doc`.
|
||||
pub(crate) fn outline_nested_text(doc: Option<&Document>, n: &mut Node, clipping: bool) {
|
||||
if !clipping {
|
||||
paint_type_clip(doc, n);
|
||||
}
|
||||
let clips = n.clips();
|
||||
let Some(ch) = n.children_mut() else { return };
|
||||
for (i, c) in ch.iter_mut().enumerate() {
|
||||
@@ -766,6 +770,36 @@ pub(crate) fn outline_nested_text(doc: Option<&Document>, n: &mut Node, clipping
|
||||
}
|
||||
}
|
||||
|
||||
/// When `n` is a clip group or clipping layer whose clipping path is type that paints
|
||||
/// ([`Node::clip_paint`]), make it the art the canvas draws for it: the type's outlines clip
|
||||
/// ([`clipping_text_outline`]), its fills as art ([`as_art`]) paint behind the clipped art inside
|
||||
/// the clip, and its strokes as art paint over it, outside the clip, in a group that `n` becomes
|
||||
/// (keeping its id, name and transparency) with the clip group inside. The other members stay as
|
||||
/// they are. Hidden type paints nothing.
|
||||
pub(crate) fn paint_type_clip(doc: Option<&Document>, n: &mut Node) {
|
||||
let Some((first, rest)) = n.children().and_then(|ch| ch.split_first()) else { return };
|
||||
if !n.clips() || !matches!(first.kind, NodeKind::Text(_)) || !first.visible {
|
||||
return;
|
||||
}
|
||||
let paint = first.clip_paint();
|
||||
if paint.fill.is_none() && paint.stroke.is_none() {
|
||||
return;
|
||||
}
|
||||
let Some(clip) = clipping_text_outline(first) else { return };
|
||||
let art = |part: Option<Node>| part.and_then(|p| as_art(doc, &p, None)).map(Arc::new);
|
||||
let members: Vec<Arc<Node>> = std::iter::once(Arc::new(clip)).chain(art(paint.fill)).chain(rest.iter().cloned()).collect();
|
||||
let stroke = art(paint.stroke);
|
||||
let id = n.id;
|
||||
match (&mut n.kind, stroke) {
|
||||
(NodeKind::Group { children, .. } | NodeKind::Layer { children, .. }, None) => *children = members,
|
||||
(NodeKind::Group { children, clip } | NodeKind::Layer { children, clip, .. }, Some(stroke)) => {
|
||||
*children = vec![Arc::new(Node::new(id, NodeKind::Group { children: members, clip: true })), stroke];
|
||||
*clip = false;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// `n` (see [`needs_outline`]) with its object-level geometry effects and its perspective applied:
|
||||
/// its [`outline_art`] reshaped as one piece, or for an image the image clipped by its reshaped
|
||||
/// frame. Type's own fills and strokes keep their effects on its whole outline. An image's own
|
||||
|
||||
@@ -84,6 +84,63 @@ fn release_and_ungroup_keep_the_clipping_paths_paint() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Pixels of `doc` rendered on white that are mostly blue and mostly green.
|
||||
fn blue_and_green(doc: &Document) -> (usize, usize) {
|
||||
let opts = RenderOptions { background: Some([255, 255, 255, 255]), ..Default::default() };
|
||||
let r = Renderer::new().render(doc, 200, 200, Affine::IDENTITY, &opts);
|
||||
let (mut blue, mut green) = (0, 0);
|
||||
for y in 0..200 {
|
||||
for x in 0..200 {
|
||||
let p = r.pixel(x, y);
|
||||
blue += usize::from(p[2] > 200 && p[0] < 60 && p[1] < 60);
|
||||
green += usize::from(p[1] > 200 && p[0] < 60 && p[2] < 60);
|
||||
}
|
||||
}
|
||||
(blue, green)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn painted_type_clipping_inside_a_group_with_geometry_effects_keeps_its_paint() {
|
||||
// The canvas and the SVG and PDF output paint the type's fill and stroke as they did before
|
||||
// the effect. Type that stays type beside the clip group takes the second Transform case
|
||||
// through baking that keeps type.
|
||||
let cases =
|
||||
[("distort.transform", json!({"moveH": 5}), false), ("distort.transform", json!({"moveH": 5}), true), ("distort.roughen", json!({}), false)];
|
||||
for (effect, params, other_type) in cases {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 200, "height": 200})).unwrap();
|
||||
rect(&mut s, 0.0, 30.0, "#ff0000");
|
||||
let t = NodeId(s.execute("text.create", &json!({"x": 10, "y": 120, "text": "Clip", "size": 72})).unwrap()["id"].as_u64().unwrap());
|
||||
s.execute("select.all", &json!({})).unwrap();
|
||||
let clip = NodeId(s.execute("object.clippingMask.make", &json!({})).unwrap()["id"].as_u64().unwrap());
|
||||
s.execute("paint.setFill", &json!({"color": "#0000ff", "ids": [t.0]})).unwrap();
|
||||
s.execute("paint.setStroke", &json!({"color": "#00ff00", "ids": [t.0]})).unwrap();
|
||||
s.execute("stroke.set", &json!({"weight": 2, "ids": [t.0]})).unwrap();
|
||||
let before = blue_and_green(&s.doc().unwrap().doc);
|
||||
let mut ids = vec![clip.0];
|
||||
if other_type {
|
||||
ids.push(s.execute("text.create", &json!({"x": 10, "y": 190, "text": "x", "size": 10})).unwrap()["id"].as_u64().unwrap());
|
||||
}
|
||||
let group = NodeId(s.execute("object.group", &json!({"ids": ids})).unwrap()["id"].as_u64().unwrap());
|
||||
s.execute("effect.apply", &json!({"effect": effect, "params": params, "ids": [group.0], "item": null})).unwrap();
|
||||
let doc = s.doc().unwrap().doc.clone();
|
||||
let (blue, green) = blue_and_green(&doc);
|
||||
assert!(
|
||||
blue * 2 > before.0 && green * 2 > before.1,
|
||||
"{effect} {other_type}: the canvas paints the type's fill and stroke: {blue} {green}, {before:?} before"
|
||||
);
|
||||
let svg = vectorcraft_svg::export(&doc, &Default::default());
|
||||
let pdf = vectorcraft_pdf::export(&doc, &Default::default()).unwrap();
|
||||
for (what, out) in [("SVG", vectorcraft_svg::import(&svg).unwrap()), ("PDF", vectorcraft_pdf::import(&pdf).unwrap())] {
|
||||
let (b, g) = blue_and_green(&out);
|
||||
assert!(
|
||||
b.abs_diff(blue) * 10 < blue && g.abs_diff(green) * 5 < green,
|
||||
"{effect} {other_type}, {what}: {b} {g} against the canvas's {blue} {green}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn type_as_a_clipping_path_paints_its_characters() {
|
||||
let mut s = Session::new();
|
||||
|
||||
Reference in New Issue
Block a user