mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-14 19:26:56 -04:00
M3.89: a clipping path's fill paints behind the clipped art, its stroke over it
Paint given to a clipping path (paint commands with its id) now shows: its fill paints inside the clip behind the clipped art and its stroke over the art, unclipped, on the canvas and in SVG and PDF. Node::clip_paint splits the clipping path into its fills and its strokes (type: its characters' too) for all three outputs; the renderer caches the parts per clipping path. Clip group visual bounds take in the stroke. Make Clipping Mask still leaves the clipping path unpainted, as the reference app does; Ungroup no longer wipes a painted clipping path's appearance (Release already kept it). Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
799b507564
commit
3b2f46137a
9 files changed
+245
-16
No files matched your search
+55
-1
@@ -478,7 +478,13 @@ impl Node {
|
||||
pub fn visual_bounds(&self) -> Option<Rect> {
|
||||
match &self.kind {
|
||||
NodeKind::Group { children, clip: true } | NodeKind::Layer { children, clip: true, .. } => {
|
||||
children.first().and_then(|c| c.geometric_bounds())
|
||||
// A clipping path that paints (see `clip_paint`) strokes over the clip edge.
|
||||
let clip = children.first()?;
|
||||
if matches!(clip.kind, NodeKind::Path { .. } | NodeKind::Compound { .. } | NodeKind::Text(_)) {
|
||||
clip.visual_bounds()
|
||||
} else {
|
||||
clip.geometric_bounds()
|
||||
}
|
||||
}
|
||||
NodeKind::Layer { children, .. } | NodeKind::Group { children, .. } => {
|
||||
// The container's own strokes paint around its members.
|
||||
@@ -705,6 +711,54 @@ impl Node {
|
||||
_ => Some((unite(&shapes), FillRule::NonZero)),
|
||||
}
|
||||
}
|
||||
|
||||
/// What this object paints as the clipping path of a clip group, shared by the renderer and
|
||||
/// the SVG and PDF writers: its fills, painted behind the clipped art, and its strokes, painted
|
||||
/// over it (unclipped). Each part is this object keeping only those appearance items (type:
|
||||
/// also only its characters' fills or strokes); `None` where it paints nothing, as right after
|
||||
/// Make Clipping Mask. Only paths, compound paths and type paint as clipping paths.
|
||||
pub fn clip_paint(&self) -> ClipPaint {
|
||||
use crate::appearance::AppearanceItem;
|
||||
let runs = match &self.kind {
|
||||
NodeKind::Path { guide: false, .. } | NodeKind::Compound { .. } => None,
|
||||
NodeKind::Text(t) => Some(&t.runs),
|
||||
_ => return ClipPaint::default(),
|
||||
};
|
||||
let paints = |fill: bool| {
|
||||
let item = |i: &AppearanceItem| match i {
|
||||
AppearanceItem::Fill(f) => fill && f.visible && !f.paint.is_none(),
|
||||
AppearanceItem::Stroke(s) => !fill && s.visible && !s.paint.is_none() && s.width > 0.0,
|
||||
};
|
||||
let run = |r: &crate::text::TextRun| if fill { !r.style.fill.is_none() } else { !r.style.stroke.is_none() && r.style.stroke_width > 0.0 };
|
||||
self.appearance.items.iter().any(item) || runs.is_some_and(|rs| rs.iter().any(run))
|
||||
};
|
||||
let part = |fill: bool| {
|
||||
paints(fill).then(|| {
|
||||
let mut n = self.clone();
|
||||
for i in (0..n.appearance.items.len()).rev() {
|
||||
if matches!(n.appearance.items[i], AppearanceItem::Fill(_)) != fill {
|
||||
n.appearance.remove_item(i);
|
||||
}
|
||||
}
|
||||
if let NodeKind::Text(t) = &mut n.kind {
|
||||
for r in &mut t.runs {
|
||||
*(if fill { &mut r.style.stroke } else { &mut r.style.fill }) = vectorcraft_color::Paint::None;
|
||||
}
|
||||
}
|
||||
n
|
||||
})
|
||||
};
|
||||
ClipPaint { fill: part(true), stroke: part(false) }
|
||||
}
|
||||
}
|
||||
|
||||
/// What a clipping path paints ([`Node::clip_paint`]).
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct ClipPaint {
|
||||
/// The clipping path with only its fills: painted behind the clipped art (inside the clip).
|
||||
pub fill: Option<Node>,
|
||||
/// The clipping path with only its strokes: painted over the clipped art, not clipped.
|
||||
pub stroke: Option<Node>,
|
||||
}
|
||||
|
||||
/// Knockout Group state of a container (the Transparency panel's three-state checkbox). In a
|
||||
|
||||
@@ -93,7 +93,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Make",
|
||||
["Object", "Clipping Mask"],
|
||||
Some("Cmd+7"),
|
||||
"{} the topmost selected object (a path, compound path or text, which loses its paint) clips the others: compound holes, even-odd fills and glyph outlines clip as drawn → {id} of the clip group",
|
||||
"{} the topmost selected object (a path, compound path or text, which loses its paint) clips the others: compound holes, even-odd fills and glyph outlines clip as drawn. Paint given to the clipping path later (paint commands with its id) shows: its fill behind the clipped art, its stroke over it → {id} of the clip group",
|
||||
has_multi,
|
||||
clip_make
|
||||
),
|
||||
@@ -102,7 +102,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Release",
|
||||
["Object", "Clipping Mask"],
|
||||
Some("Cmd+Alt+7"),
|
||||
"{} the selected clip groups become plain groups; their clipping path (path, compound path or text) stays, unpainted",
|
||||
"{} the selected clip groups become plain groups; their clipping path (path, compound path or text) stays, with the paint it has (none unless painted after Make)",
|
||||
has_selection,
|
||||
clip_release
|
||||
),
|
||||
@@ -383,11 +383,12 @@ fn ungroup(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
d.remove(*id)?;
|
||||
for (k, c) in children.into_iter().enumerate() {
|
||||
let mut c = (*c).clone();
|
||||
if clip && k == 0 {
|
||||
if let NodeKind::Path { clipping, .. } = &mut c.kind {
|
||||
*clipping = false;
|
||||
}
|
||||
c.appearance = Appearance::default();
|
||||
// The clipping path keeps its paint (none, unless painted after Make).
|
||||
if clip
|
||||
&& k == 0
|
||||
&& let NodeKind::Path { clipping, .. } = &mut c.kind
|
||||
{
|
||||
*clipping = false;
|
||||
}
|
||||
c.opacity *= opacity;
|
||||
new_sel.push(c.id);
|
||||
@@ -552,7 +553,7 @@ pub(super) fn make_clipping_path(d: &mut Document, top: NodeId) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stop clip group or clipped layer `id` clipping; its clipping path stays, unpainted.
|
||||
/// Stop clip group or clipped layer `id` clipping; its clipping path stays, with its paint.
|
||||
pub(super) fn release_clip(d: &mut Document, id: NodeId) {
|
||||
let Some(n) = d.node_mut(id).filter(|n| n.clips()) else { return };
|
||||
n.set_clips(false);
|
||||
|
||||
@@ -795,6 +795,8 @@ mod tests_charstroke;
|
||||
#[cfg(test)]
|
||||
mod tests_clip;
|
||||
#[cfg(test)]
|
||||
mod tests_clippaint;
|
||||
#[cfg(test)]
|
||||
mod tests_cmdsplit;
|
||||
#[cfg(test)]
|
||||
mod tests_colorguide;
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
//! Fill and stroke on a clipping path (M3.89): Make Clipping Mask leaves the clipping path
|
||||
//! unpainted, paint given to it later shows (its fill behind the clipped art, its stroke over it
|
||||
//! and unclipped) on the canvas, in SVG and in PDF, and Release and Ungroup keep it.
|
||||
|
||||
use serde_json::json;
|
||||
use vectorcraft_geom::Affine;
|
||||
use vectorcraft_render::{RenderOptions, Renderer};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn rect(s: &mut Session, x: f64, w: f64, fill: &str) -> NodeId {
|
||||
let id = NodeId(s.execute("shape.rectangle", &json!({"x": x, "y": 50, "width": w, "height": 100})).unwrap()["id"].as_u64().unwrap());
|
||||
s.execute("paint.setFill", &json!({"color": fill, "ids": [id.0]})).unwrap();
|
||||
id
|
||||
}
|
||||
|
||||
/// Red art (40..100) clipped by a rectangle (50..150) painted blue with a 10 pt green stroke.
|
||||
fn painted_clip() -> (Session, NodeId, NodeId) {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 200, "height": 200})).unwrap();
|
||||
rect(&mut s, 40.0, 60.0, "#ff0000");
|
||||
let clip = rect(&mut s, 50.0, 100.0, "#ffffff");
|
||||
s.execute("select.all", &json!({})).unwrap();
|
||||
let group = NodeId(s.execute("object.clippingMask.make", &json!({})).unwrap()["id"].as_u64().unwrap());
|
||||
let n = s.doc().unwrap().doc.node(clip).unwrap().clone();
|
||||
assert!(n.clip_paint().fill.is_none() && n.clip_paint().stroke.is_none(), "Make leaves the clipping path unpainted");
|
||||
s.execute("paint.setFill", &json!({"color": "#0000ff", "ids": [clip.0]})).unwrap();
|
||||
s.execute("paint.setStroke", &json!({"color": "#00ff00", "ids": [clip.0]})).unwrap();
|
||||
s.execute("stroke.set", &json!({"weight": 10, "ids": [clip.0]})).unwrap();
|
||||
(s, group, clip)
|
||||
}
|
||||
|
||||
/// RGB at (x, 100) of `doc` rendered on white.
|
||||
fn rgb(doc: &Document, x: u32) -> [u8; 3] {
|
||||
let opts = RenderOptions { background: Some([255, 255, 255, 255]), ..Default::default() };
|
||||
let p = Renderer::new().render(doc, 200, 200, Affine::IDENTITY, &opts).pixel(x, 100);
|
||||
[p[0], p[1], p[2]]
|
||||
}
|
||||
|
||||
/// Behind the clipped art, its fill; over it (also outside the clip), its stroke.
|
||||
fn assert_painted(doc: &Document, what: &str) {
|
||||
assert_eq!(rgb(doc, 70), [255, 0, 0], "{what}: the clipped art over the fill");
|
||||
assert_eq!(rgb(doc, 120), [0, 0, 255], "{what}: the fill inside the clip");
|
||||
assert_eq!(rgb(doc, 45), [0, 255, 0], "{what}: the stroke's outer half, unclipped (the art below it is clipped away)");
|
||||
assert_eq!(rgb(doc, 52), [0, 255, 0], "{what}: the stroke over the clipped art");
|
||||
assert_eq!(rgb(doc, 147), [0, 255, 0], "{what}: the stroke over the fill");
|
||||
assert_eq!(rgb(doc, 30), [255, 255, 255], "{what}: outside the clip and its stroke");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_clipping_paths_fill_and_stroke_show_in_render_svg_and_pdf() {
|
||||
let (s, group, clip) = painted_clip();
|
||||
let doc = s.doc().unwrap().doc.clone();
|
||||
assert_painted(&doc, "canvas");
|
||||
// The clip group's bounds take in the stroke.
|
||||
let b = doc.node(group).unwrap().visual_bounds().unwrap();
|
||||
assert!((b.x0 - 45.0).abs() < 1e-6 && (b.x1 - 155.0).abs() < 1e-6, "{b:?}");
|
||||
// SVG: the fill inside the clip, the stroke after it, one element keeping the clipping path's id.
|
||||
let svg = vectorcraft_svg::export(&doc, &Default::default());
|
||||
assert_painted(&vectorcraft_svg::import(&svg).unwrap(), "SVG");
|
||||
// PDF.
|
||||
let pdf = vectorcraft_pdf::export(&doc, &Default::default()).unwrap();
|
||||
assert_painted(&vectorcraft_pdf::import(&pdf).unwrap(), "PDF");
|
||||
// Unpainted clipping paths write nothing extra.
|
||||
let mut bare = s;
|
||||
bare.execute("paint.setFill", &json!({"none": true, "ids": [clip.0]})).unwrap();
|
||||
bare.execute("paint.setStroke", &json!({"none": true, "ids": [clip.0]})).unwrap();
|
||||
let doc = bare.doc().unwrap().doc.clone();
|
||||
assert_eq!(rgb(&doc, 120), [255, 255, 255]);
|
||||
assert_eq!(rgb(&doc, 45), [255, 255, 255]);
|
||||
let svg = vectorcraft_svg::export(&doc, &Default::default());
|
||||
assert_eq!(svg.matches("<path").count(), 2, "the clip region and the red art: {svg}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release_and_ungroup_keep_the_clipping_paths_paint() {
|
||||
for cmd in ["object.clippingMask.release", "object.ungroup"] {
|
||||
let (mut s, group, clip) = painted_clip();
|
||||
s.execute("select.set", &json!({"ids": [group.0]})).unwrap();
|
||||
s.execute(cmd, &json!({})).unwrap();
|
||||
let n = s.doc().unwrap().doc.node(clip).unwrap().clone();
|
||||
assert_eq!(n.appearance.fill_paint(), vectorcraft_color::Paint::solid(vectorcraft_color::Color::from_hex("#0000ff").unwrap()), "{cmd}");
|
||||
assert_eq!(n.appearance.stroke().map(|st| st.width), Some(10.0), "{cmd}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn type_as_a_clipping_path_paints_its_characters() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 200, "height": 200})).unwrap();
|
||||
rect(&mut s, 0.0, 200.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();
|
||||
s.execute("object.clippingMask.make", &json!({})).unwrap();
|
||||
let paint = |s: &Session| s.doc().unwrap().doc.node(t).unwrap().clip_paint();
|
||||
assert!(paint(&s).fill.is_none() && paint(&s).stroke.is_none());
|
||||
s.execute("paint.setFill", &json!({"color": "#0000ff", "ids": [t.0]})).unwrap();
|
||||
let p = paint(&s);
|
||||
assert!(p.stroke.is_none());
|
||||
let NodeKind::Text(fill) = &p.fill.expect("the characters' fill paints").kind else { panic!("type") };
|
||||
assert!(fill.runs.iter().all(|r| r.style.fill.color().is_some() && r.style.stroke.is_none()));
|
||||
}
|
||||
@@ -527,13 +527,21 @@ impl Exporter<'_> {
|
||||
match &n.kind {
|
||||
NodeKind::Layer { children, clip: false, .. } | NodeKind::Group { children, clip: false } => self.children(s, children, page),
|
||||
NodeKind::Group { children, clip: true } | NodeKind::Layer { children, clip: true, .. } => {
|
||||
// The region every output clips to; nothing to clip by hides the clipped art.
|
||||
// The region every output clips to; nothing to clip by hides the clipped art. The
|
||||
// clipping path's fill paints behind the clipped art and its stroke over it, unclipped.
|
||||
if let Some((clip, rest)) = children.split_first()
|
||||
&& let Some((p, r)) = vectorcraft_effects::clip_outline(clip).and_then(|(bp, r)| to_path(&bp).map(|p| (p, r)))
|
||||
{
|
||||
let paint = clip.clip_paint();
|
||||
s.push_clip_path(&p, &rule(r));
|
||||
if let Some(fill) = &paint.fill {
|
||||
self.node(s, fill, page, false);
|
||||
}
|
||||
self.children(s, rest, page);
|
||||
s.pop();
|
||||
if let Some(stroke) = &paint.stroke {
|
||||
self.node(s, stroke, page, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
NodeKind::Path { path, rule, guide, .. } => {
|
||||
|
||||
@@ -212,6 +212,8 @@ struct GeomEntry {
|
||||
|
||||
/// A clipping path (kept alive so its address can't be reused) and the region it clips to.
|
||||
type ClipEntry = (Arc<Node>, Option<Arc<(BezPath, FillRule)>>);
|
||||
/// A clipping path (kept alive) and what it paints: its fills and its strokes ([`Node::clip_paint`]).
|
||||
type ClipPaintEntry = (Arc<Node>, Option<Arc<Node>>, Option<Arc<Node>>);
|
||||
|
||||
/// Reusable renderer (keeps the render context, decoded images and glyph caches between frames).
|
||||
pub struct Renderer {
|
||||
@@ -260,6 +262,8 @@ pub struct Renderer {
|
||||
/// Clip paths pushed on the current context (path, rule, transform): starting the context
|
||||
/// again from a picture of it pushes them again (see [`Self::group`]).
|
||||
clip_paths: Vec<(BezPath, FillRule, Affine)>,
|
||||
/// What clipping paths paint, per clipping path (see [`Self::clip_paint_of`]).
|
||||
clip_paints: PtrMap<usize, ClipPaintEntry>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
@@ -314,6 +318,7 @@ impl Renderer {
|
||||
backdrop: None,
|
||||
blends: PtrMap::default(),
|
||||
clip_paths: vec![],
|
||||
clip_paints: PtrMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -503,6 +508,24 @@ impl Renderer {
|
||||
region
|
||||
}
|
||||
|
||||
/// Cached fills and strokes of clipping path `a` ([`Node::clip_paint`]): the parts keep their
|
||||
/// allocations between frames, so their own geometry stays cached.
|
||||
fn clip_paint_of(&mut self, a: &Arc<Node>) -> (Option<Arc<Node>>, Option<Arc<Node>>) {
|
||||
let key = Arc::as_ptr(a) as usize;
|
||||
if let Some((node, fill, stroke)) = self.clip_paints.get(&key)
|
||||
&& Arc::ptr_eq(node, a)
|
||||
{
|
||||
return (fill.clone(), stroke.clone());
|
||||
}
|
||||
let paint = a.clip_paint();
|
||||
let (fill, stroke) = (paint.fill.map(Arc::new), paint.stroke.map(Arc::new));
|
||||
if self.clip_paints.len() > 1024 {
|
||||
self.clip_paints.clear();
|
||||
}
|
||||
self.clip_paints.insert(key, (a.clone(), fill.clone(), stroke.clone()));
|
||||
(fill, stroke)
|
||||
}
|
||||
|
||||
/// Cached BezPath of a path node.
|
||||
fn path_of(&mut self, a: &Arc<Node>) -> Option<Arc<BezPath>> {
|
||||
let key = Arc::as_ptr(a) as usize;
|
||||
@@ -672,14 +695,24 @@ impl Renderer {
|
||||
}
|
||||
NodeKind::Group { children, clip: true } | NodeKind::Layer { children, clip: true, .. } => {
|
||||
// Nothing to clip by hides the clipped art (as in the SVG and PDF output). A clip
|
||||
// group doesn't isolate: blending inside it reaches the art below.
|
||||
// group doesn't isolate: blending inside it reaches the art below. The clipping
|
||||
// path's fill paints behind the clipped art and its stroke over it, unclipped.
|
||||
if let Some((clip, rest)) = children.split_first()
|
||||
&& let Some(region) = self.clip_of(clip)
|
||||
{
|
||||
let (fill, stroke) = self.clip_paint_of(clip);
|
||||
let blends = self.blends_through(n);
|
||||
let bounds = if blends { Some(region.0.bounding_box()) } else { None };
|
||||
let comp = Composite { clip: Some((®ion.0, region.1)), blends, bounds, ..Default::default() };
|
||||
self.group(ctx, f, comp, &mut |r, c, fr| r.draw_children(c, fr, rest, knockout));
|
||||
self.group(ctx, f, comp, &mut |r, c, fr| {
|
||||
if let Some(fill) = &fill {
|
||||
r.draw_arc(c, fr, fill);
|
||||
}
|
||||
r.draw_children(c, fr, rest, knockout)
|
||||
});
|
||||
if let Some(stroke) = &stroke {
|
||||
self.draw_arc(ctx, f, stroke);
|
||||
}
|
||||
}
|
||||
}
|
||||
NodeKind::Path { path, rule, guide, .. } => {
|
||||
|
||||
@@ -15,6 +15,11 @@ fn rect(d: &mut Document, r: Rect, c: [f32; 3]) -> Node {
|
||||
Node::path(d.alloc_id(), shapes::rectangle(r), Appearance::basic(Paint::solid(Color::rgb(c[0], c[1], c[2])), Paint::None, 0.0))
|
||||
}
|
||||
|
||||
/// An unpainted clipping path, as Make Clipping Mask leaves it (a painted one paints too).
|
||||
fn clip_path(d: &mut Document, r: Rect) -> Node {
|
||||
Node::path(d.alloc_id(), shapes::rectangle(r), Appearance::basic(Paint::None, Paint::None, 0.0))
|
||||
}
|
||||
|
||||
/// A Multiply rectangle at 20..80 (the group's content).
|
||||
fn multiply(d: &mut Document) -> Node {
|
||||
let mut n = rect(d, Rect::new(20.0, 20.0, 80.0, 80.0), S);
|
||||
@@ -98,7 +103,7 @@ fn a_group_blend_mode_applies_again_to_the_non_isolated_result() {
|
||||
#[test]
|
||||
fn a_clip_group_does_not_isolate_its_children() {
|
||||
let d = doc(|d| {
|
||||
let clip = rect(d, Rect::new(30.0, 30.0, 70.0, 70.0), [0.0; 3]);
|
||||
let clip = clip_path(d, Rect::new(30.0, 30.0, 70.0, 70.0));
|
||||
let m = multiply(d);
|
||||
Node::new(d.alloc_id(), NodeKind::Group { children: vec![Arc::new(clip), Arc::new(m)], clip: true })
|
||||
});
|
||||
@@ -112,7 +117,7 @@ fn a_non_isolated_group_inside_a_clip_group_stays_clipped() {
|
||||
// The half-opaque group copies its backdrop while the clip is in force: the clip still applies
|
||||
// to it and to what follows.
|
||||
let d = doc(|d| {
|
||||
let clip = rect(d, Rect::new(30.0, 30.0, 70.0, 70.0), [0.0; 3]);
|
||||
let clip = clip_path(d, Rect::new(30.0, 30.0, 70.0, 70.0));
|
||||
let m = multiply(d);
|
||||
let g = group(d, vec![m], 0.5, Blend::Normal, false);
|
||||
let after = rect(d, Rect::new(0.0, 60.0, 100.0, 100.0), S);
|
||||
|
||||
@@ -806,11 +806,32 @@ impl Writer<'_> {
|
||||
self.def(1, "</clipPath>");
|
||||
let id = self.id_attr(n);
|
||||
let a = self.attrs(&self.group_props(n));
|
||||
self.line(&format!("<g{id} clip-path=\"url(#{cid})\"{a}>"));
|
||||
// The clipping path's fill paints behind the clipped art and its stroke over it,
|
||||
// outside the clip (the group then wraps both).
|
||||
let paint = clip.clip_paint();
|
||||
if paint.stroke.is_some() {
|
||||
self.line(&format!("<g{id}{a}>"));
|
||||
self.depth += 1;
|
||||
self.line(&format!("<g clip-path=\"url(#{cid})\">"));
|
||||
} else {
|
||||
self.line(&format!("<g{id} clip-path=\"url(#{cid})\"{a}>"));
|
||||
}
|
||||
self.depth += 1;
|
||||
if let Some(fill) = &paint.fill {
|
||||
self.node(fill);
|
||||
}
|
||||
self.group_children(n, rest);
|
||||
self.depth -= 1;
|
||||
self.line("</g>");
|
||||
if let Some(stroke) = &paint.stroke {
|
||||
// One element keeps the clipping path's id.
|
||||
let anonymous = self.anonymous;
|
||||
self.anonymous |= paint.fill.is_some();
|
||||
self.node(stroke);
|
||||
self.anonymous = anonymous;
|
||||
self.depth -= 1;
|
||||
self.line("</g>");
|
||||
}
|
||||
}
|
||||
NodeKind::Path { guide: true, .. } => {}
|
||||
NodeKind::Path { path, rule, .. } => {
|
||||
|
||||
+4
-1
@@ -239,7 +239,10 @@ like the mask view: the grid shows behind the artboards of the documents that tu
|
||||
`object.clippingMask.make` clips the selected objects by the topmost one, which may be a path, a compound path or a
|
||||
text object (it loses its paint). Compound holes, even-odd fills, glyph outlines and the union of a group's members
|
||||
clip alike on screen, in raster export and in SVG and PDF. `object.clippingMask.release` turns the clip group into a
|
||||
plain group and keeps the clipping path, unpainted.
|
||||
plain group and keeps the clipping path with whatever paint it has.
|
||||
|
||||
A clipping path can be painted after Make (`paint.setFill`, `paint.setStroke`, `stroke.set` with its `ids`): its fill
|
||||
paints behind the clipped art and its stroke over it, not clipped, on screen and in SVG and PDF.
|
||||
|
||||
```json
|
||||
{"name":"run_command","arguments":{"command":"select.set","params":{"ids":[12,15]}}}
|
||||
|
||||
Reference in new issue
Block a user