Files
vectorcraft/crates/render/src/tests_clip.rs
T
bflatasticandClaude Opus 5.5 4f685be57a M3.26: clipping sets with any clip shape
Clip groups clip by compound paths (with their holes), even-odd paths, text
outlines and groups (the union of their members) on screen, in raster
export, in SVG and in PDF alike, through one shared region:
Node::clip_outline (the model walk) and effects::clip_outline (text
outlined, several shapes united), cached per clipping path in the
renderer. Nothing to clip by hides the clipped art everywhere, and clicks
hit a clip group only inside its clipping region. Make Clipping Mask takes
a compound path or text object on top (it loses its paint) and Release
gives it back unchanged in kind.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 17:23:57 +07:00

107 lines
4.4 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Clip groups clip by any clipping path (M3.26): compound holes, even-odd fills, the union of a
//! group's members, text outlines; with nothing to clip by the clipped art is hidden.
use super::*;
use vectorcraft_color::{Color, Paint};
use vectorcraft_doc::{Appearance, CharStyle};
use vectorcraft_geom::{PathData, Point, SubPath, shapes};
const BLACK: [u8; 4] = [0, 0, 0, 255];
const CLEAR: [u8; 4] = [0, 0, 0, 0];
fn path(d: &mut Document, p: PathData, rule: FillRule) -> Arc<Node> {
let mut n = Node::path(d.alloc_id(), p, Appearance::basic(Paint::None, Paint::None, 0.0));
if let NodeKind::Path { rule: r, .. } = &mut n.kind {
*r = rule;
}
Arc::new(n)
}
fn rect(r: Rect) -> PathData {
shapes::rectangle(r)
}
/// `clip` clipping a black square over the whole 100×100 canvas, rendered transparent.
fn clipped(mut d: Document, clip: Arc<Node>) -> Rendered {
let art = Node::path(d.alloc_id(), rect(Rect::new(0.0, 0.0, 100.0, 100.0)), Appearance::basic(Paint::solid(Color::BLACK), Paint::None, 0.0));
let g = Node::new(d.alloc_id(), NodeKind::Group { children: vec![clip, Arc::new(art)], clip: true });
let l = d.layers[0].id;
d.insert(Some(l), 0, g).unwrap();
Renderer::new().render(&d, 100, 100, Affine::IDENTITY, &RenderOptions::default())
}
#[test]
fn compound_clip_keeps_its_hole() {
let mut d = Document::new(100.0, 100.0);
let (outer, inner) = (rect(Rect::new(10.0, 10.0, 90.0, 90.0)), rect(Rect::new(35.0, 35.0, 65.0, 65.0)));
let children = vec![path(&mut d, outer, FillRule::NonZero), path(&mut d, inner, FillRule::NonZero)];
let donut = Arc::new(Node::new(d.alloc_id(), NodeKind::Compound { children, rule: FillRule::EvenOdd }));
let r = clipped(d, donut);
assert_eq!(r.pixel(20, 50), BLACK, "the ring");
assert_eq!(r.pixel(50, 50), CLEAR, "the hole stays transparent");
assert_eq!(r.pixel(5, 5), CLEAR);
}
#[test]
fn even_odd_star_clips_with_an_empty_centre() {
let mut d = Document::new(100.0, 100.0);
// A pentagram drawn in one stroke: its centre pentagon is outside under even-odd.
let pts: Vec<Point> = (0..5)
.map(|k| {
let a = (k * 2 % 5) as f64 * std::f64::consts::TAU / 5.0 - std::f64::consts::FRAC_PI_2;
Point::new(50.0 + 48.0 * a.cos(), 52.0 + 48.0 * a.sin())
})
.collect();
let star = path(&mut d, PathData::single(SubPath::polyline(&pts, true)), FillRule::EvenOdd);
let r = clipped(d, star);
assert_eq!(r.pixel(50, 52), CLEAR, "the centre is empty");
assert_eq!(r.pixel(50, 12), BLACK, "a point");
}
#[test]
fn group_clip_is_the_union_of_its_members() {
let mut d = Document::new(100.0, 100.0);
// Opposite windings: a plain concatenation would cancel out where they overlap.
let mut b = rect(Rect::new(40.0, 0.0, 100.0, 50.0));
b.reverse();
let members = vec![path(&mut d, rect(Rect::new(0.0, 0.0, 60.0, 50.0)), FillRule::NonZero), path(&mut d, b, FillRule::NonZero)];
let g = Arc::new(Node::group(d.alloc_id(), members));
let r = clipped(d, g);
for x in [20, 50, 80] {
assert_eq!(r.pixel(x, 25), BLACK, "x = {x}");
}
assert_eq!(r.pixel(50, 75), CLEAR);
}
#[test]
fn text_clips_by_its_glyph_outlines() {
let mut d = Document::new(100.0, 100.0);
let t = vectorcraft_doc::TextObject::point(Point::new(5.0, 80.0), "O", CharStyle { size: 90.0, ..CharStyle::default() });
let text = Arc::new(Node::new(d.alloc_id(), NodeKind::Text(Box::new(t))));
let b = text.geometric_bounds().unwrap();
let r = clipped(d, text);
let cy = ((b.y0 + b.y1) / 2.0) as u32;
// Across the middle of the "O": clear, ring, the counter (clear), ring, clear.
let row: Vec<bool> = (0..100).map(|x| r.pixel(x, cy)[3] > 128).collect();
let runs = row.windows(2).filter(|w| w[0] != w[1]).count();
assert_eq!(runs, 4, "two ring crossings with the counter between: {row:?}");
}
#[test]
fn nothing_to_clip_by_hides_the_art() {
let mut d = Document::new(100.0, 100.0);
let empty = Arc::new(Node::group(d.alloc_id(), vec![]));
let r = clipped(d, empty);
assert_eq!(r.pixel(50, 50), CLEAR);
}
#[test]
fn clip_regions_are_cached_per_clipping_path() {
let mut d = Document::new(100.0, 100.0);
let clip = path(&mut d, rect(Rect::new(0.0, 0.0, 50.0, 100.0)), FillRule::NonZero);
let mut r = Renderer::new();
let first = r.clip_of(&clip).unwrap();
assert!(Arc::ptr_eq(&first, &r.clip_of(&clip).unwrap()));
assert_eq!(first.1, FillRule::NonZero);
}