PURPOSE

Renders an absolute-positioned overlay layered on top of the shader preview canvas in the weapon workbench. For every UniformEntry that declares a handle kind (point, radius, angle), the component draws a circular grabber at the canvas-space pixel that corresponds to the uniform’s current value and lets the user drag the value live with pointer input. A bottom-left tooltip mirrors the in-flight value as a string. Implements the visual coordinate convention uniform (0,0) = canvas center, Y-up to match the shader convention.

OWNS

  • HANDLE_RADIUS constant (6 px) — visual size of each grabber.
  • HANDLE_RING_R constant (80 px) — orbit-ring radius used to map radius and angle uniforms into pixel space.
  • Props shape: canvasSize, uniforms: UniformEntry[], values: Record<string, number | number[]>, onChange(name, newValue).
  • HandleDragState shape: tracks the active drag’s name, kind, startMouseX, startMouseY, startValue.
  • clamp, degToRad, radToDeg local helpers.
  • DraggableHandleOverlay exported function component.
  • handlePixelPos(u) — pure mapping from uniform value to overlay pixel coordinates per handle kind.
  • handlePointerDown / handlePointerMove / handlePointerUp callbacks driving the live drag state machine.
  • tooltip local state — { name, value } displayed at the bottom-left of the overlay.
  • Refs: overlayRef (overlay div) and dragRef (current drag state, kept in a ref so move handlers do not re-render per pointer event).

READS FROM

  • @engine/vfx-workbench/shader-templates/registryUniformEntry type, including name, type (vec2 / float), handle (point / radius / angle), and optional handleColor.
  • Props.values — the live uniform value map provided by the parent (workbench shader state).
  • Props.canvasSize — defines the center pixel used for the coordinate mapping.
  • The overlay DOM rect via overlayRef.current?.getBoundingClientRect() to convert client coordinates into canvas-relative coordinates during pointer moves.

PUSHES TO

  • Props.onChange(name, newValue) — the only outward write path. Called on every pointer-move while dragging:
    • point handles push [mx, my] vec2 values in canvas-centered, Y-up coordinates.
    • radius handles push a clamped scalar in [0, 1] derived from horizontal drag distance over HANDLE_RING_R.
    • angle handles push a scalar in [0, 360) degrees derived from atan2(my, mx) and normalized.
  • Local tooltip state — updated on hover, drag, and on leave.

DOES NOT

  • Does not own the uniform values; it is a controlled component that delegates state to the parent via onChange.
  • Does not render the shader, the canvas, or any uniform UI controls (sliders, number fields).
  • Does not call any store, network, telemetry, or audio code.
  • Does not read or write localStorage, refs outside its own subtree, or window globals.
  • Does not enforce per-uniform min/max ranges beyond the radius clamp to [0, 1]; the angle wraps via modulo and the point handle is unclamped. The code references a __uniformRange lookup but does not use it.
  • Does not handle keyboard input, touch gestures beyond what the Pointer Events API provides, or right-click.
  • Does not animate; values flow directly from pointer position to onChange.

Signals

  • data-overlay="true" on the root div — used by descendants to locate the overlay via closest('[data-overlay]').
  • data-testid="draggable-handle-overlay" on the root div.
  • data-testid="handle-${u.name}" on each handle grabber.
  • data-handle-kind attribute on each grabber, set to point, radius, or angle.
  • data-testid="handle-tooltip" on the tooltip element.
  • Pointer-events are off on the overlay root (pointerEvents: 'none') and on the tooltip; only handle grabbers opt back in with pointerEvents: 'auto'.

Entry points

  • Default named export: DraggableHandleOverlay({ canvasSize, uniforms, values, onChange }) — invoked by the weapon-workbench screen above its shader preview canvas.

Pattern notes

  • Coordinate convention is explicit and load-bearing: uniform (x, y) maps to canvas pixel (center + x, center - y). Y is flipped at both the forward (handlePixelPos) and inverse (handlePointerMove) sides so the shader’s Y-up convention is preserved end-to-end.
  • Drag state is held in a useRef, not React state, so high-frequency pointer-move events do not trigger re-renders. Only the tooltip and the controlled uniform values cause re-renders.
  • Pointer capture is acquired on pointerdown via setPointerCapture(pointerId), so move and up events continue to fire on the same element even if the cursor leaves the handle.
  • Handle layout per kind:
    • point on vec2 — pixel = (center + vx, center - vy).
    • radius on float — pixel = (center + v * HANDLE_RING_R, center); v is treated as normalized [0, 1].
    • angle on float — pixel = (center + HANDLE_RING_R * cos(deg), center - HANDLE_RING_R * sin(deg)).
  • Only uniforms whose handle field is set and whose handlePixelPos returns a non-null coordinate are rendered, so non-spatial uniforms are silently skipped.
  • Grabber visuals: 12 px circle with a 2 px handleColor border (default #00ffcc), ${color}33 translucent fill, additional ${color}44 1 px box-shadow ring on point handles.
  • Tooltip formatting differs by source:
    • Hover on a vec2 handle prints [x.x, y.y] with one decimal per axis.
    • Hover on a scalar prints three decimals.
    • During a drag, point handles print [x.x, y.y], radius prints three decimals, angle prints ${deg}° with one decimal.
  • onPointerLeave clears the tooltip only when no drag is active, so the tooltip stays visible mid-drag even if the pointer wanders off the grabber.
  • The component is purely presentational with respect to persistence: closing or remounting the workbench resets only the tooltip and drag refs; uniform state is the parent’s responsibility.