anchor-types

PURPOSE

Type contract and pure CRUD helpers for the named-anchor map used by the VFX workbench. Defines anchor kinds (pivot / edge / radial), an optional direction + radius + local offset, and immutable add / remove / rename / update operations over Record<string, AnchorSpec>. Enforces a name regex and protects the reserved center anchor.

OWNS

  • AnchorKind — union 'pivot' | 'edge' | 'radial'.
  • AnchorDirection — union 'forward' | 'backward'.
  • AnchorSpec{ kind, direction?, radius?, localOffset?: { x, y } }.
  • AnchorMapRecord<string, AnchorSpec>.
  • VALID_NAME — module-private regex /^[a-z][a-z0-9_]{0,31}$/i. Lowercase or uppercase leading letter, then [a-z0-9_], total length 1–32.
  • isValidAnchorName(name) — boolean test against VALID_NAME.
  • addAnchor(map, name, spec) — returns a new map with the entry; throws on invalid name or existing key.
  • removeAnchor(map, name) — returns a new map without the entry; throws if name === 'center'; no-op if key absent.
  • renameAnchor(map, oldName, newName) — returns a new map with the key renamed (spec preserved); throws on oldName === 'center', invalid newName, missing oldName, or collision with an existing different newName.
  • updateAnchor(map, name, patch) — returns a new map with { ...existing, ...patch } merged into the entry; throws if the key is missing.

READS FROM

Nothing. Self-contained module with no imports.

PUSHES TO

Nothing. Pure functions returning new AnchorMap values.

DOES NOT

  • Mutate the input AnchorMap (every helper spreads into a new object).
  • Persist, serialize, or load anchors.
  • Render anchors or resolve anchor positions to world/screen space.
  • Validate radius, localOffset, direction, or kind field values beyond TypeScript types.
  • Create the center anchor — it only refuses to remove/rename it.
  • Distinguish casing in name uniqueness (name in map is an exact-string check; regex itself is case-insensitive via /i).

Signals

  • Throws Error('invalid anchor name: <name>') from addAnchor and renameAnchor when the name fails VALID_NAME.
  • Throws Error('anchor already exists: <name>') from addAnchor when the key is already present.
  • Throws Error('cannot remove center anchor') / Error('cannot rename center anchor') from removeAnchor / renameAnchor.
  • Throws Error('anchor not found: <name>') from renameAnchor and updateAnchor when the key is missing.
  • Throws Error('target name exists: <name>') from renameAnchor when newName !== oldName and newName is already in the map.
  • removeAnchor returns the same map reference when the key is absent (silent no-op).

Entry points

  • Importers will call the four CRUD functions to derive a new AnchorMap for state updates.
  • isValidAnchorName is exported for UI prevalidation (e.g., disable a submit button before calling addAnchor).
  • Types AnchorKind, AnchorDirection, AnchorSpec, AnchorMap are the canonical shape consumers should reference.

Pattern notes

  • Pure / immutable: every mutator returns a fresh object; safe for Zustand / React state without manual cloning.
  • Reserved-name guard is hard-coded to the literal string 'center' — no constant exported. Callers cannot reconfigure the protected name.
  • addAnchor rejects duplicate keys but updateAnchor requires existence; together they force callers to pick create-vs-update explicitly.
  • renameAnchor allows a no-op rename to the same name (only collides if newName !== oldName && newName in map).
  • Name regex is case-insensitive at the character class but anchored to a single leading letter; max 32 chars total.
  • No optional field defaults — consumers reading direction, radius, localOffset must handle undefined.

EXTRACT-CANDIDATE

  • The VALID_NAME regex + isValidAnchorName pair is a generic identifier-validation primitive — if other workbench domains (clip names, layer names, slot keys) reuse the same 32-char [a-z][a-z0-9_]{0,31} rule, hoist to a shared engine/vfx-workbench/identifier.ts.
  • The immutable add/remove/rename/update-over-Record<string, T> pattern is generic. If a second domain in vfx-workbench needs the same CRUD shape, extract a keyedMap<T>() factory that takes a reserved-keys set and a name validator.