Selection: effects on a hidden member of a clipping group leave clicks alone

A clipping path that is a group clips by its visible members only. A
geometry effect on a hidden member made clicks inside the clip group
count the group's type by its glyph outlines, where they count it by its
frame when nothing has an effect.

Clicks now ask `effects::reshaped_clip_outline`, which gives a region
only when an object the clipping path clips by has a geometry effect.
`clip_outline` uses the same function.
This commit is contained in:
dotCooCoo
2026-11-14 08:29:00 -08:00
parent ef74b358f6
commit e001227ce1
5 changed files with 37 additions and 18 deletions
+8 -4
View File
@@ -12,10 +12,14 @@ use vectorcraft_geom::{BezPath, FillRule, PathData};
/// fills and strokes: the region is then the union of the paths they give, as an export bakes them.
/// `None` when there is nothing to clip by (the clipped art is then hidden).
pub fn clip_outline(clip: &Node) -> Option<(BezPath, FillRule)> {
match clip_effects_applied(clip, true) {
Some(m) => m.clip_outline(Some(&outline_text), &unite),
None => clip.clip_outline(Some(&outline_text), &unite),
}
reshaped_clip_outline(clip).unwrap_or_else(|| clip.clip_outline(Some(&outline_text), &unite))
}
/// [`clip_outline`] of a clipping path that a live geometry effect reshapes; `None` when nothing
/// `clip` clips by has a geometry effect of its own or on a fill or stroke. A hidden member of a
/// group or layer does not clip, so its effects are not counted.
pub fn reshaped_clip_outline(clip: &Node) -> Option<Option<(BezPath, FillRule)>> {
clip_effects_applied(clip, true).map(|m| m.clip_outline(Some(&outline_text), &unite))
}
/// `n`, the clipping path of a clip group, with the live geometry effects of everything it clips by
+1 -1
View File
@@ -69,7 +69,7 @@ pub use adjust::{
is_channel,
};
pub use bake::{StrokeArt, bake_appearance, bake_document, bake_type_appearance, expand_art, expand_leaf, fresh_ids, needs_bake};
pub use clip::{WarpSeams, clip_outline, knockout_silhouette, warp_pieces_translucent};
pub use clip::{WarpSeams, clip_outline, knockout_silhouette, reshaped_clip_outline, warp_pieces_translucent};
pub use group::{
OutlineHook, PATHFINDER_EFFECTS, compound_shape_area, compound_shape_art, evaluate_container, has_container_appearance, has_pathfinder,
is_pathfinder, member_leaves, member_shapes, paints, pathfinder_children, shape_art,
+1 -12
View File
@@ -25,7 +25,7 @@ impl Session {
// Clicks find an image's own fills and strokes where their effects draw them.
vectorcraft_doc::hit::set_image_paint(vectorcraft_render::effects::image_paint);
// Clicks inside a clip group fall in the region its clipping path's effects give it.
vectorcraft_doc::hit::set_clip_region(reshaped_clip_region);
vectorcraft_doc::hit::set_clip_region(vectorcraft_render::effects::reshaped_clip_outline);
// Clicks find a blend's steps and an envelope's distorted art where the canvas draws them,
// evaluated again when the fonts, the hyphenation exceptions or the plug-ins change.
vectorcraft_doc::hit::set_live_art(vectorcraft_render::effects::expand_live, || {
@@ -493,17 +493,6 @@ impl Session {
}
}
/// The region clipping path `clip` clips to with its live geometry effects applied
/// ([`vectorcraft_render::effects::clip_outline`]), for clicks inside its clip group; `None` when
/// nothing in it has a visible geometry effect of its own or on a fill or stroke, so that clicks
/// count its plain shapes, type by its frame.
fn reshaped_clip_region(clip: &Node) -> Option<Option<(vectorcraft_geom::BezPath, vectorcraft_geom::FillRule)>> {
use vectorcraft_render::effects::has_geometry;
let mut reshaped = false;
clip.walk(&mut |n| reshaped |= has_geometry(&n.appearance.effects) || n.appearance.items.iter().any(|i| has_geometry(i.effects())));
reshaped.then(|| vectorcraft_render::effects::clip_outline(clip))
}
/// Whether some of type `t` is set in a font named (in any case) one of `names`, lowercased.
fn sets_in_any(t: &TextObject, names: &HashSet<String>) -> bool {
!names.is_empty() && t.runs.iter().any(|r| names.contains(&r.style.font_family.to_lowercase()))
+26
View File
@@ -128,6 +128,32 @@ fn text_on_top_clips_by_its_glyphs() {
assert!(matches!(s.doc().unwrap().doc.node(t).unwrap().kind, NodeKind::Text(_)));
}
#[test]
fn a_hidden_member_of_a_clipping_group_leaves_clicks_on_the_type_frame() {
// The clipping path is a group of an "O" and a hidden rectangle with a Transform effect. The
// hidden rectangle does not clip, so clicks inside the clip group still count the type by its
// frame, the counter of the "O" included.
let mut s = session();
let art = id_of(&s.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 100, "height": 100})).unwrap());
let t = id_of(&s.execute("text.create", &json!({"x": 5, "y": 80, "text": "O", "size": 90})).unwrap());
let b = s.doc().unwrap().doc.node(t).unwrap().geometric_bounds().unwrap();
let r = id_of(&s.execute("shape.rectangle", &json!({"x": 40, "y": 40, "width": 10, "height": 10})).unwrap());
s.execute("effect.apply", &json!({"effect": "distort.transform", "params": {"moveH": 20}, "ids": [r.0], "item": null})).unwrap();
select(&mut s, &[art, t]);
let g = id_of(&s.execute("object.clippingMask.make", &json!({})).unwrap());
// Make Clipping Mask takes no group on top, so the group goes in place of the type.
let mut d = (*s.doc().unwrap().doc).clone();
let mut rect = (*d.remove(r).unwrap()).clone();
rect.visible = false;
let text = d.remove(t).unwrap();
let group = vectorcraft_doc::Node::group(d.alloc_id(), vec![text, Arc::new(rect)]);
d.insert(Some(g), 0, group).unwrap();
let leaf = |x: f64, y: f64| vectorcraft_doc::hit::hit_test(&d, vectorcraft_geom::Point::new(x, y), Default::default()).map(|h| h.leaf);
let middle = ((b.x0 + b.x1) / 2.0, (b.y0 + b.y1) / 2.0);
assert_eq!(leaf(middle.0, middle.1), Some(art), "in the counter, inside the frame");
assert_eq!(leaf(b.x1 + 3.0, middle.1), None, "beside the text");
}
#[test]
fn clicks_follow_the_region_a_clipping_paths_effects_give_it() {
// Art from 10 to 90 pt, clipped by the artboard's left half, which a Transform effect moves
+1 -1
View File
@@ -1610,7 +1610,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
- **Masks and clipping:** opacity-mask controls work while the mask is being edited; transparency commands take ids and work without a selection; clipping sets clip by compound paths, even-odd paths, text and groups on screen and in raster export, matching SVG/PDF. Alt-click shows only the mask; SVG masks keep Clip and Invert; the transparency grid is per-document state; a clipping path's own fill and stroke paint behind and over the clipped art; layers can be clipping masks.
- **Masks and clipping:** opacity-mask controls work while the mask is being edited; transparency commands take ids and work without a selection; clipping sets clip by compound paths, even-odd paths, text and groups on screen and in raster export, matching SVG/PDF. Alt-click shows only the mask; SVG masks keep Clip and Invert; the transparency grid is per-document view state saved in native files (#1968); a clipping path's own fill and stroke paint behind and over the clipped art; layers can be clipping masks.
The geometry effects of a clipping path (Roughen, Transform and the other effects that reshape a path), of its fills and strokes, and of the members of a group used as one, shape the area it clips to on the canvas and in raster export, as in SVG and PDF export: a roughened clipping path clips by its roughened outline. They apply before the geometry effects of the clip group or of a group around it, on the canvas and in every export. When the clipping path has a fill or stroke of its own, the geometry effects of those fills and strokes apply after the group's geometry effects instead. Type used as a clipping path keeps its geometry effects when a group's effect outlines it. Raster effects such as Gaussian Blur leave that area as it is. `effects::tests::clipfx` and the renderer's `tests::clip` cover it.
Clicks, Select Behind, the Unlock submenu, marquees and the Enclosing Lasso inside a clip group or a clipping layer follow that area too: the clipped art is found where the effects moved it and not where they took it away. The clipping path itself is still found by its own outline and anchors. Outline view and layers with Preview off draw the plain clipping path, and clicks and marquees there count it. `doc::hit::tests` and the engine's `tests::clip` cover it.
Clicks, Select Behind, the Unlock submenu, marquees and the Enclosing Lasso inside a clip group or a clipping layer follow that area too: the clipped art is found where the effects moved it and not where they took it away. The clipping path itself is still found by its own outline and anchors. A hidden member of a group used as a clipping path does not clip, and a geometry effect on it does not change where clicks land. Outline view and layers with Preview off draw the plain clipping path, and clicks and marquees there count it. `doc::hit::tests` and the engine's `tests::clip` cover it.
- **Transparency:** three-state Knockout Group (on, neutral, off), Opacity & Mask Define Knockout Shape, Page Isolated Blending and Page Knockout Group, on screen and in SVG/PDF. SVG export writes a knockout group of opaque art plainly, without masks, so shapes meeting edge to edge leave no seam; translucent, blended, masked or brushed art knocks out through masks. Blend modes match the standard formulas on screen, in SVG and in PDF, and non-isolated groups blend with the art below. Flatten Transparency with presets, a dialog and a Flattener Preview panel. Flattening for files without transparency (PDF/X-1a, PDF/X-3) in a CMYK document keeps the inks where it rasterizes, as under an image with see-through pixels: the images it makes hold ink amounts, not screen colours the file would separate again, so a C100 M6 Y41 K3 panel stays C100 M6 Y41 K3 there (#2160, `engine/src/cmd/fileio/tests_pdfx.rs`).
On the canvas, the renderer no longer draws the picture of the art below back as an image each time it copies the backdrop of a non-isolated group that blends: it keeps that picture and draws the art that follows over it. At 2880 × 1800, 50 translucent groups with Multiply members take 65 ms a frame instead of 299 ms on 8 threads (`vectorcraft-cli bench`), and the frames have the same pixels.
Type inside a group, layer, symbol instance, blend or repeat keeps its opacity, blend mode, opacity mask, Opacity & Mask Define Knockout Shape, own effects, perspective and visibility when the effects on the container outline it. Hidden type with a geometry effect of its own is left out of exports.