Skip to content

Share a path's points between copies - #557

Closed
fdb wants to merge 3 commits into
nodebox4from
claude/project-thread-idzlcq
Closed

fdb wants to merge 3 commits into
nodebox4from
claude/project-thread-idzlcq

Conversation

@fdb

@fdb fdb commented Oct 15, 2026

Copy link
Copy Markdown
Member

Requested by Frederik · project thread

Before: Copy, Translate, Rotate, Scale, Skew, Align, Fit and Reflect copied every point of every path, and Vello converted every path separately.

After: a Path is an immutable, shared Shape plus a placement transform and a style. Those ops only compose the placement, and Vello converts each shape once. The output looks the same: all tests pass unchanged, and Vello's pixels for placed and baked copies differ by at most 1/255 (checked on llvmpipe).

The rule, now in AGENTS.md: every point a Path hands out is placed. contours() borrows the shape's points when the placement is the identity and computes them otherwise. Only shape() plus placement() expose shape coordinates, for renderers and caches. Building or editing points goes through with_contours() or contours_mut(), which copies a shared shape first.

How: contours became private behind contours(), contours_mut(), contour_count() and friends, and Path::transform composes the placement. Bounds are cached per shape. Serde still writes placed contours, so the TypeScript side sees no change. In Vello, fills draw the shared shape with the placement. Strokes do too when the placement is a rotation plus uniform scale (the width divided by the scale); mirrored, skewed or unevenly scaled paths stroke their placed points, so the stroke width is never scaled. SVG export keeps writing placed points, because an SVG transform would scale the stroke.

benches/copy_paths.rs, 64-point shape, against nodebox4:

before after
copy 10,000 12.6 ms 0.46 ms
clone that output 9.9 ms 0.24 ms
copy 50,000 59.4 ms 2.6 ms
clone that output 68.1 ms 1.2 ms
bounds of 10,000 rotated copies 1.5 ms 2.6 ms

Rotated bounds are slower because they transform points on each call. Bounds of translated or scaled copies come from the cached shape bounds.

Tests run: cargo test --workspace --exclude nodebox-python (751 passed), cargo test -p nodebox-core --features serde, cargo check -p nodebox-electron --target wasm32-unknown-unknown, cargo bench -p nodebox-core --bench copy_paths. Not run: the Electron Playwright suite. nodebox-python already fails to build on nodebox4 (pyo3 API), and its two touched files were updated by hand.


Generated by Claude Code

A Path is now an immutable, shared Shape plus a placement transform and a
style. Transforming a path composes the placement and shares the shape, so
Copy, Translate, Rotate, Scale, Skew, Align, Fit and Reflect no longer copy
points. Every accessor returns placed points; contours() borrows them when
the placement is the identity.
convert_paths converts each shape once and keeps the placement as an Affine.
Fills draw the shared shape with the placement. Strokes do too when the
placement is a rotation and uniform scale, with the width divided by the
scale; otherwise they stroke the placed points, so the stroke width is never
scaled. The desktop viewer uses the shared scene builder. AGENTS.md states
the rule that every point a Path hands out is placed.
# Conflicts:
#	crates/nodebox-core/src/geometry/path.rs

fdb commented Oct 16, 2026

Copy link
Copy Markdown
Member Author

Closing: NodeBox 4 renderer and path-model work continues in fdb/nodebox4 only. The branch stays as a reference.


Generated by Claude Code

@fdb fdb closed this Oct 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants