Files
vectorcraft/crates/render/tests/dash_align.rs
T
bflatasticandClaude Opus 5.5 8a837fd4b9 M3.68: groups, layers and type have their own fills, strokes and effects, with Contents and Characters rows
A group's or layer's own fills and strokes paint every member's geometry, below or above the
members as the new contents slot (Appearance.contents_index) says; its geometry effects reshape
the members as one piece and its raster effects apply to the composite (one combined shadow),
respecting knockout groups. Type's own fills and strokes paint under or over its characters.
Render, SVG/PDF (via bake) and Expand Appearance evaluate them alike.

Commands: appearance.* / effect.* take target: "object"|"contents" and layer ids;
appearance.moveItem moves the Contents slot (from: "contents") or an item across it
(contents); appearance.targetContents selects a group's members. Graphic styles applied to a
group now style the group itself. The Appearance panel shows Contents/Characters rows that
drag, and double-click to target the members or edit the characters' paint.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 00:59:34 +07:00

79 lines
3.3 KiB
Rust

//! Pixel tests for dashes fitted to corners and for width profiles with dashes and joins, drawn
//! on the canvas from the shared stroke geometry (`vectorcraft_effects::stroke`).
use vectorcraft_color::{Color, Paint};
use vectorcraft_doc::{Appearance, AppearanceItem, Dash, Document, LineJoin, StrokeLayer, WidthProfile};
use vectorcraft_geom::{PathData, Point, Rect, SubPath, shapes};
use vectorcraft_testkit::fixtures::DocBuilder;
use vectorcraft_testkit::raster::{Image, render_region};
/// Pixels per point in these tests.
const S: f64 = 4.0;
fn doc(path: PathData, width: f64, f: impl FnOnce(&mut StrokeLayer)) -> Document {
let mut st = StrokeLayer::new(Paint::solid(Color::BLACK), width);
f(&mut st);
let mut b = DocBuilder::new(160.0, 120.0);
b.path(path, Appearance { items: vec![AppearanceItem::Stroke(st)], ..Default::default() }, |_| {});
b.build()
}
fn render(d: &Document) -> Image {
render_region(d, Rect::new(0.0, 0.0, 160.0, 120.0), S)
}
/// Is the pixel at document point (x, y) dark?
fn dark(img: &Image, x: f64, y: f64) -> bool {
img.over_white((x * S) as u32, (y * S) as u32)[0] < 128
}
fn rect_doc(align_corners: bool) -> Document {
doc(shapes::rectangle(Rect::new(20.0, 20.0, 120.0, 70.0)), 2.0, |s| s.dash = Some(Dash { pattern: vec![12.0, 6.0], offset: 0.0, align_corners }))
}
#[test]
fn fitted_dashes_wrap_every_corner_of_a_rectangle() {
let img = render(&rect_doc(true));
// Half a dash (6 · 100/108) runs each way from every corner, then a gap of the same length.
let half = 6.0 * 100.0 / 108.0;
for (c, dirs) in [
((20.0, 20.0), [(1.0, 0.0), (0.0, 1.0)]),
((120.0, 20.0), [(-1.0, 0.0), (0.0, 1.0)]),
((120.0, 70.0), [(-1.0, 0.0), (0.0, -1.0)]),
((20.0, 70.0), [(1.0, 0.0), (0.0, -1.0)]),
] {
for (dx, dy) in dirs {
let at = |d: f64| (c.0 + dx * d, c.1 + dy * d);
let (x, y) = at(half / 2.0);
assert!(dark(&img, x, y), "dash beside corner {c:?}");
let (x, y) = at(half * 1.5);
assert!(!dark(&img, x, y), "gap after corner {c:?} along ({dx}, {dy})");
}
}
// Exact dashes keep their lengths instead (the exact golden covers how they look).
assert_ne!(render(&rect_doc(false)).rgba, img.rgba);
}
#[test]
fn dashes_of_a_lens_stroke_widen_towards_the_middle() {
let img = render(&doc(shapes::line(Point::new(30.0, 60.0), Point::new(130.0, 60.0)), 10.0, |s| {
s.profile = Some(WidthProfile::lens());
s.dash = Some(Dash { pattern: vec![10.0, 10.0], offset: 0.0, align_corners: false });
}));
// The dash over 40..50 of the 100 pt line is nearly full width, the one over 0..10 thin.
assert!(dark(&img, 75.0, 60.0 - 3.8) && dark(&img, 75.0, 60.0 + 3.8));
assert!(dark(&img, 35.0, 60.0) && !dark(&img, 35.0, 60.0 - 1.5));
assert!(!dark(&img, 45.0, 60.0), "gap");
}
#[test]
fn a_round_join_on_a_profile_stroke_has_no_spike() {
let l = SubPath::polyline(&[Point::new(20.0, 40.0), Point::new(100.0, 40.0), Point::new(100.0, 110.0)], false);
let img = render(&doc(PathData::single(l), 20.0, |s| {
s.profile = Some(WidthProfile { points: vec![(0.0, 1.0, 1.0), (1.0, 1.0, 1.0)] });
s.join = LineJoin::Round;
}));
assert!(dark(&img, 106.0, 34.0), "the round join covers the corner");
assert!(!dark(&img, 109.0, 31.0), "but not the miter's point");
}