Skip to content

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:

DataPolicyWhy
Continuous — scale, opacity, color, strokeColor, strokeWidth, curveu, user attributes, groupsBlend (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, shapeIndexSnap to nearer parentHalf of a category is nonsense; glyph 5.5 belongs to no glyph.
rotationBlend, shortest arc350° to 10° passes through 0°, never the long way around.
indexRenumber 0…nindex 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 valuesRegistry fillMissing 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 ​

OpPer-vertex attributesNotes
Resample ShapeBlended at the arc positionA red→blue gradient resamples as a red→blue gradient.
Subdivide ShapeBoundary copies nearest source vertex; interior blends all of the path's vertices (inverse-distance)Corner colors fill the triangulated interior smoothly.
ThickenBoth stroke contours copy the source vertex they hugA gradient path thickens into a gradient outline.
Merge ShapesConcatenated (union: keys one input lacks read registry fills)Same semantics as Merge Points.
Shape MorphBlended along the correspondence, then cross-faded A→B at the mixKeys present on only one side carry through unchanged.
Text On Path / Shape Exploder / Clone To PointsCopied 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 DeleteFiltered in parallel with the survivorsindex renumbers.
Round Corners / Offset Path / Shape Smooth / Trim PathPreservedPosition-only or resample-with-carry ops.
Shape BooleanPresent but reset to registry defaultsThe 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_source for a pixel-exact match.
  • Merge fills are correct by registry. Points merged in from a branch without grp1 read 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.
  • 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