Geometry Attributes — the propagation contract
Per-element data (colors, sizes, groups, delays) survives geometry operations. This page is the contract: what each op does with your attributes, which policies apply, and the one honest exception.
The one rule
No geometry operation may silently drop an attribute column. An op may copy values, blend them, snap them, renumber them, or — where no correspondence to the source exists — reset them to registry defaults. What it may never do is make the column vanish, because a vanished column fails silently everywhere downstream: Apply To selects nothing, gradients render flat, delays stop delaying, with no diagnostic anywhere.
This is enforced in CI (engine/tests/attr_contract.rs, the contract_* tests) and gated in the editor: a new geometry node fails the build until it's declared enforced or exempt (node-layout.test.ts, "propagation-contract declaration gate").
The policies
When an op derives a new element between source elements (resampling, subdivision, morphing), the attribute registry decides how each attribute interpolates — the node never chooses:
| Data | Policy | Why |
|---|---|---|
Continuous — scale, opacity, color, strokeColor, strokeWidth, curveu, user attributes, groups | Blend (linear) | A point 30% along an edge gets 30% of the way between the values. Groups blend too, then resolve crisply at the > 0.5 member test — biased to the nearer parent. |
Categorical — glyphIndex, sourceIndex, shapeIndex | Snap to nearer parent | Half of a category is nonsense; glyph 5.5 belongs to no glyph. |
rotation | Blend, shortest arc | 350° to 10° passes through 0°, never the long way around. |
index | Renumber 0…n | index is an order (owner rule): any membership change renumbers survivors. Holes and duplicates are not an order. Track identity across edits with @id instead. |
| Absent values | Registry fill | Missing scale reads 1 (not 0 — points don't collapse), missing color reads opaque white, missing group membership reads 0 ("not a member"). |
What each op does
| Op | Per-vertex attributes | Notes |
|---|---|---|
| Resample Shape | Blended at the arc position | A red→blue gradient resamples as a red→blue gradient. |
| Subdivide Shape | Boundary copies nearest source vertex; interior blends all of the path's vertices (inverse-distance) | Corner colors fill the triangulated interior smoothly. |
| Thicken | Both stroke contours copy the source vertex they hug | A gradient path thickens into a gradient outline. |
| Merge Shapes | Concatenated (union: keys one input lacks read registry fills) | Same semantics as Merge Points. |
| Shape Morph | Blended along the correspondence, then cross-faded A→B at the mix | Keys present on only one side carry through unchanged. |
| Text On Path / Shape Exploder / Clone To Points | Copied 1:1 (rigid remaps) | Clone's palette mode reads each instance's own variant; the target point's color/opacity still write per-path instance overrides. |
| Point Delete / Path Delete | Filtered in parallel with the survivors | index renumbers. |
| Round Corners / Offset Path / Shape Smooth / Trim Path | Preserved | Position-only or resample-with-carry ops. |
| Shape Boolean | Present but reset to registry defaults | The honest exception: boolean clipping produces vertices with no source correspondence, so values can't be carried. The columns exist (color reads white, groups read non-member) so downstream nodes keep working — strictly better than the old behavior, where they vanished. Paths inherit the subject shape's first path's attributes. |
Per-path attributes (glyph groups, delays, per-path color/opacity/strokeWidth) ride through every op above with their paths.
Practical notes
- Fill vs. stroke gradients differ slightly by design. Per-vertex color is sampled at the vertices; fill interpolates it across the interior while a stroke's color is constant across its width. Same data, different interpolation domains — a wide stroke can read as a subtle bevel against the fill. Add vertices (Resample Shape) to tighten it, or plug the gradient's image output into Draw Shape's
fill_source/stroke_sourcefor a pixel-exact match. - Merge fills are correct by registry. Points merged in from a branch without
grp1read 0 — not a member — which is exactly what a selection should say about them. - Attributes are cheap. Columns are plain arrays; carrying them through an op is a copy, not a re-evaluation of whatever produced them.
Related
- Attribute name fields — the picker, intrinsics, Apply To
- Select — authoring groups
shared/src/attrs.rs— the registry (ATTR_SPECS);shared/src/attr_gather.rs— the machinery