PURPOSE

Right-pane editor for a selected VFX component inside the Weapon Workbench. Renders the live WebGL preview, exposes shape/palette/uniform/anchor controls for baked components, hosts the GLSL editor for live_shader components, and ships changes either to the runtime atlas (bake) or to disk as a JSON definition.

OWNS

  • Local editor state: params (uniform values), anchors, palette, live (LiveSection for live_shader), layers + selectedLayerId (schema v2 layer stack), shapePickerOpen, timelineT, showAnchors, baking, bakeStatus.
  • Refs: canvasBoxRef (DOM mount for preview canvas), surfaceRef (PreviewSurface instance), rafRef (animation frame handle), startTimeRef, scrubTRef (null = auto-play, number = scrub time).
  • Lifecycle of the PreviewSurface WebGL context: construction, template selection, uniform pushes, the rAF render loop, and destruction on unmount/component switch.
  • Bake and save-to-disk request orchestration including in-flight status string.

READS FROM

  • useWorkbenchStoreselectedComponentId.
  • SEED_COMPONENTS (./fixtures) — looked up by id to resolve the active component definition.
  • TEMPLATES registry from @engine/vfx-workbench/shader-templates/registry — provides uniformSchema and template id for the shape section, shape knobs, draggable handles, and surface template selection.
  • DEFAULT_PALETTE from @engine/vfx-workbench/palette — fallback when component has no palette.
  • Component fields read defensively via any: kind, name, category, id, baked.params, baked.palette, baked.layers, baked.shader, anchors, live.

PUSHES TO

  • POST /__dev/live-shader-write — body { componentId, name, category, fragGlsl, vertGlsl, uniformSchema, previewSampleInputs, blendMode, estCostMs, anchors } for live_shader bake.
  • POST /__dev/atlas-write — body { category, componentId, pngBase64, manifestEntry } for baked-shader bake; PNG produced by bakeComponent + bytesToBase64.
  • POST /__dev/vfx-save — body { kind: 'component', id, content } where content is JSON-stringified component definition with merged params/palette/anchors.
  • PreviewSurface instance — useTemplate, setUniforms (params, u_palette via flattenPalette), render(t), destroy.
  • Child components via props: LiveShaderEditor (value, onChange), AnchorEditor (anchors, onChange), PaletteWidget (value, onChange), ShapePicker (currentTemplateId, onPick), TimelineScrubber (currentT, onScrub, onAutoResume), AnchorOverlay (anchors, size, visible), DraggableHandleOverlay (canvasSize, uniforms, values, onChange), LayerStackEditor (layers, selectedLayerId, onSelect, onChange).

DOES NOT

  • Mutate the useWorkbenchStore; the editor is a read-only consumer of selectedComponentId.
  • Modify SEED_COMPONENTS in memory — edits live in local React state until baked or saved.
  • Implement Clone: the button is rendered but the onClick body is an empty placeholder comment.
  • Render anything for components whose kind is neither baked nor live_shader.
  • Handle per-layer parameter editing for schema v2 stacks — caption explicitly defers that to “Phase 4b.”
  • Persist UI prefs (overlay toggle, scrub state) across component switches; they reset in the id-change effect.

Signals

  • setBakeStatus strings: 'Shipping live shader…', `Shipped ✓ at ${data.shippedAt}`, `Ship failed: ${e.message}`, 'Baking…', `Baked ✓ layer ${data.layerIndex} at ${data.bakedAt}`, `Bake failed: ${e.message}`, 'Saved ✓', `Save failed: ${text}`.
  • data-testid attributes exposed for tests: component-editor-empty, clone-button, shape-section, change-shape-button, component-editor-preview, anchor-overlay-toggle, bake-button, save-to-disk-button.
  • aria-label on each uniform range input is the uniform name.
  • timelineT state machine: 'auto' = rAF loop owns t; numeric = scrubber owns t and rAF is cancelled until handleAutoResume.

Entry points

  • Default export: none. Named export ComponentEditor() React function component.
  • Constants: module-local PREVIEW_SIZE = 192, SECTION_HEADER and SECTION_DIVIDER CSS objects.
  • Handlers: handleBake, handleSaveToDisk, handleShapePick, handleScrub, handleAutoResume — all closures; not exported.
  • Mounted by the Weapon Workbench right pane; no router entry.

Pattern notes

  • Branches early on component.kind: empty placeholder → live_shader returns a minimal editor (header + LiveShaderEditor + AnchorEditor) → non-baked fallback → full baked editor.
  • Three effects keyed off component?.id: state reset, preview surface mount + rAF loop teardown, plus two effects that push params and palette into the live surface on change.
  • The shape picker rebuilds defaults from template.uniformSchema (only numeric defaults), and re-pushes them plus the palette to the surface via useTemplate + setUniforms.
  • DraggableHandleOverlay’s onChange flattens array-valued uniforms into name[i] keys before merging into params.
  • Uniform sliders read range[0..1] with 0..1 fallback and a (max - min) / 100 step.
  • Save-to-disk merges params, palette, and anchors into the existing baked block (or just anchors for live_shader) before JSON-stringifying.
  • Layer stack section renders only when bc.baked.layers is a non-empty array; selection defaults to the first layer.
  • Component switching cancels rAF, destroys the WebGL surface, and clears refs in the cleanup function.