PURPOSE

Authoring panel for a single LiveSection of a VFX component. Exposes a Monaco-based GLSL fragment editor, a uniform-schema editor, a sample-inputs editor, and a blend-mode selector, alongside a small live WebGL preview that recompiles on shader or schema changes.

OWNS

  • The 128x128 preview canvas DOM node mounted into a ref’d container div.
  • A PreviewSurface instance held in a ref for the lifetime of the current shader/schema pair.
  • The requestAnimationFrame loop driving the preview render.
  • Local helpers setFragGlsl, setUniforms, setBlend that spread updates back through onChange.
  • The constant PREVIEW_SIZE = 128.

READS FROM

  • Props value: LiveSection and onChange: (next: LiveSection) => void.
  • value.fragGlsl, value.uniformSchema, value.previewSampleInputs, value.blendMode for editor state and preview uniforms.
  • Types LiveSection, BlendMode, UniformDecl from @engine/vfx-workbench/component-schema.
  • PreviewSurface class from @engine/vfx-workbench/preview-surface.
  • performance.now() for the preview animation clock.

PUSHES TO

  • Parent component via onChange with a new LiveSection whenever fragGlsl, uniformSchema, previewSampleInputs, or blendMode changes.
  • PreviewSurface.useRawFragment(fragGlsl, uniformSchema) on (re)mount of the preview effect.
  • PreviewSurface.setUniforms(previewSampleInputs) per animation frame and on previewSampleInputs change.
  • PreviewSurface.render(tSec % 1) per animation frame.
  • Delegates uniform-schema editing to UniformSchemaEditor and sample-input editing to PreviewSampleInputsEditor.

DOES NOT

  • Persist or load LiveSection data; it is a controlled component.
  • Compile or validate GLSL itself; compilation is delegated to PreviewSurface.useRawFragment and compile errors are swallowed so the preview stays blank.
  • Resize the preview; the canvas is hard-coded to PREVIEW_SIZE square.
  • Drive the production VFX render path; the surface here is preview-only.
  • Edit anything outside the single LiveSection passed in (no full-component editing, no save/export).
  • Wrap previewSampleInputs writes in the recompile effect; sample-input changes intentionally skip recompilation.

Signals

  • data-testid="live-preview-canvas" on the preview container div.
  • data-testid="live-blend-mode" on the blend-mode <select>.
  • The <select> exposes options additive and alpha for BlendMode.
  • Monaco editor uses defaultLanguage="cpp" and theme="vs-dark" for GLSL syntax highlighting.

Entry points

  • Exported function component LiveShaderEditor({ value, onChange }: Props).
  • Mounted by the weapon-workbench live-section UI as part of editing a VFX component’s live shader stack.

Pattern notes

  • Two useEffect hooks split concerns: one keyed on [value.fragGlsl, value.uniformSchema] rebuilds the PreviewSurface and starts a fresh requestAnimationFrame loop; the second keyed on [value.previewSampleInputs] pushes uniform updates into the live surface without recompiling.
  • Cleanup of the recompile effect cancels the pending animation frame, calls surface.destroy(), and clears surfaceRef.current to prevent leaked GL contexts.
  • On compile failure, the surface is destroyed and the effect returns early, leaving the preview box blank rather than throwing.
  • The animation loop normalizes time as (performance.now() - start) / 1000 % 1, so the preview drives the shader on a one-second loop.
  • An eslint-disable-next-line react-hooks/exhaustive-deps comment is used to scope the recompile dependency array deliberately.
  • Inline styles are used throughout; typography uses Cal Sans, sans-serif with letter-spacing for section labels.