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