PURPOSE
Renders a visual overlay of named anchor points on top of the weapon-workbench preview canvas. Each anchor in the supplied AnchorMap is drawn as a colored crosshair with a label showing its name and kind, so the author can see where attachment points live on a sprite. Anchor positions are synthesized at author time by laying them out in a grid across the preview, because real world positions are not available outside the running engine.
OWNS
- The
AnchorOverlayexported React component. - The
KIND_COLORlookup mapping eachAnchorKind(pivot,edge,radial) to a hex color string used for the crosshair and label. - The
KIND_TOOLTIPlookup mapping eachAnchorKindto a short human-readable description shown as the marker’stitletooltip. - The
anchorScreenPoshelper that converts an(index, total, size)triple to an{ x, y }screen position by row-major grid placement with a fixed margin and a column count ofceil(sqrt(total)). - The inline-styled DOM: an absolutely positioned container sized to the preview, plus one absolutely positioned marker per entry containing a 12x12 crosshair (two colored bars) and a monospace label with a double black
textShadow. - The empty-state handling when
total === 0, returning the canvas center. - The non-rendering early return when
visibleis false.
READS FROM
@engine/vfx-workbench/anchor-typesfor theAnchorMapandAnchorKindtypes.- The
Propsit receives:anchors: AnchorMap— the dictionary of anchor specs keyed by name; iterated viaObject.entries.size: number— the side length of the square preview area; controls both the container box and the grid layout math.visible: boolean— gate; when false the component renders nothing.
- For each entry, the spec’s
kindfield is used to look up color and tooltip; the entry key is used as the displayed anchor name.
PUSHES TO
- The DOM only. Renders into whatever parent positions it; the root container uses
position: absoluteattop: 0, left: 0with the configuredsize, so it expects to overlay a relatively-positioned preview parent. - No Zustand store writes, no events, no engine calls, no network, no side effects.
DOES NOT
- Does not compute real anchor world positions. Grid placement is decorative and does not reflect the sprite’s actual pivot, edge, or radial geometry.
- Does not read sprite, ship, weapon, or VFX data; only consumes the
AnchorMappassed in by the parent. - Does not handle input or selection. Markers have
pointerEvents: 'all'so the browser shows the nativetitletooltip on hover, but there are no click, drag, or keyboard handlers. - Does not animate. There are no transitions, timers, or effects.
- Does not validate that
spec.kindis a known key — an unknown kind would produceundefinedcolor and tooltip. - Does not subscribe to any store. Re-renders only when its props change.
- Does not memoize or use refs.
Signals
data-testid="anchor-overlay"on the root container.data-testid={anchor-marker-${name}}on each per-anchor marker, wherenameis theAnchorMapkey.titleattribute on each marker carries the kind tooltip text for hover.- Each anchor’s color and kind label are derived purely from
spec.kind.
Entry points
- Default React component invocation:
<AnchorOverlay anchors={...} size={...} visible={...} />. - Imported by the weapon-workbench preview composition that owns the sprite canvas and toggles overlay visibility.
Pattern notes
- Pure presentational component: no hooks, no state, no effects. Re-rendering is the only update path.
- Color and tooltip use
Record<AnchorKind, string>lookups so adding a newAnchorKindis a type error until both maps are updated. - Grid layout uses
Math.ceil(Math.sqrt(total))columns and row-major insertion order; ordering is stable as long asObject.entriesorder on theAnchorMapis stable. - The
margin = 16and the 12x12 crosshair / 2px bar dimensions are inline constants; there is no theming hook. - Container is
pointerEvents: 'none', individual markers re-enablepointerEvents: 'all'so hover tooltips work without blocking interaction with the rest of the preview surface. - Labels use a double
0 0 3px #000textShadowfor legibility against arbitrary preview backgrounds. - Empty-map and hidden states are handled by short-circuit returns rather than conditional content inside the JSX tree.