Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ src/
exportShader.ts / exportSingleGlb.ts / glbShaderContract.ts — export assembly
teapotData.ts / teapotGeometry.ts — runtime Utah teapot tessellator
eval/ — the user-study mode (arm, consent, telemetry, SUS, package, upload)
embed/ — FastShaders inside another app's iframe (`?embed=1`, SuperWXR's shader panel): `embedMode` (a LEAF, sampled once at module init; never true at the top level) + `EmbedBridge` (a `lazyOr` chunk from App.tsx, never in a study): postMessage to and from the PARENT at the page's own origin only — `superwxr:open` restores a module through the shared "Load Script…" path, `superwxr:module` broadcasts the settled export module (what EXPORT would write), `superwxr:export` hands EXPORT's bytes to the host instead of the download folder (exportShader.ts)
intro/ — the first-open intro window (toolbar "i" reopens it): fourteen live slides (drag-connect, wire values, socket labels, insert/delete, Preview/Attach, point budget, image drop, exposing sockets, Organize, code→graph, uniforms, dropped models, Work folder, drawing + notes) on the real cards, menus, planners, encoder, parser and preview document; writes nothing; in a study only after Agree (every entry, /evalp without the budget slide), logged
tutorials/ — the Tutorials tab: twenty-two hands-on lessons built on the REAL canvas (takeover, Previous/Next, Do it for me, Keep/New); lesson data a LEAF of registry ids, a pure evaluator, a store-writing auto step; in a study only on `/evalpro`
hooks/ — useSyncEngine, useLongPress, usePersistedState, useDismiss, useFitText, useHistoryBracket, useFrameLoop,
Expand Down
1 change: 1 addition & 0 deletions docs/dev/project-structure.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ src/
exportShader.ts — shared export assembly, read imperatively from the store: `buildShaderBundle(opts?)` (module generation + project embed + exportBundle) is the ONE bundle builder — the toolbar EXPORT download, the NEW-shader save-first path, the desktop Work-folder Save and the study package all emit byte-identical bundles through it (`opts.includeMesh` overrides the session `exportIncludeMesh` flag for ONE build and is only ever passed `false`, by the pre-flight). `buildShaderBundleChecked(ask)` is the pre-flight wrapper the three USER surfaces go through; the study package (SusModal) calls `buildShaderBundle()` directly and never pauses on a dialog. `buildShaderExportChecked(asks)` is now the ONE wrapper those three surfaces call: for the bundle format it delegates to `buildShaderBundleChecked`, and for one `.glb` it runs `prepareSingleGlb` (engine/exportSingleGlb.ts, loaded by a dynamic `import()` so the repacker stays off the boot path) behind the GlbExportModal — under `isEvalMode()` it never reaches that path. `downloadShader(bundle?: ShaderExport)` is the blob/anchor browser-download tail and the `evalLog('export')` chokepoint, and it posts what the delivery has to say after the click (`announceExportDelivered`: a `.glb`'s report lines plus N1's desktop-only line, in ONE note, since `showImportNote` replaces)
intro/ — the window FastShaders opens with: `introGate` (a LEAF on the boot graph — never at boot in a study session (every entry opens it after Agree, `introInStudy`; logged, writes nothing, no Work-folder slide, no budget slide where points are hidden), `?intro` reopens it, `fs:introSeen` while "Don't show this again" is ticked), `IntroWindow` (a `lazyOr` chunk from App.tsx; slides stepped by arrows, boxes or ←/→, only the shown one mounted; their ORDER is its `SLIDES` list, never the order the scenes are described in here — owner 2026-10-23: Preview/Attach right after insert/delete, code→graph after Organize, then uniforms, dropped models, the Work folder, and drawing + notes last), `IntroStage` (the shared small canvas, pointer and key cap), `timeline` (the scenes' clock), `sceneGraph` (their tiny graphs built like palette drops, wired values, TypedEdge's still-wire geometry, `GraphCard` — one node drawn by its flow type's card replica). `ConnectScene` + `connectTour`: drag-connect, sockets picked each frame by `planDragConnect` on the measured cards, the drop at `CONNECT_SNAP_GAP`. Then `ValuesScene` + `valuesTour`: one wire at a time, close up (zoom 1.25), each fading in beside the same Output and clicked — a Float (a number), UV (a range on two channels), Position (three), Time → Sine (live, off the scene's clock with the clock hand and the wave) — each opening EdgeInfoCard's own chip (`edgeChipChannels`/`EdgeChipValues` over `evaluateEdgeRange`) at mid-arc, the wire thickened by TypedEdge's `SELECTED_EDGE_BOOST`; off-phase cards are hidden by OPACITY, never visibility (`applyWaveFrame` sets its dot's own visibility, which shows through a hidden card). Then `LabelsScene` + `labelsTour`: Image → Mix → Output; a double-click on the Image (its output sockets carry no text; a fresh one has no inputs) and a fingertip double-tap on Mix (A, B, Factor in, Result out) pin every socket label through the canvas's own `[data-fs-labels-shown]` attribute (labelPeek.ts) — NodeVisual's static sockets carry TypedHandle's `data-tooltip` under `CardSocketLabels` (`NodeEditor/cardSocketLabels.ts`, its own module so NodeVisual can read it); the picture is `introPhotoThumbnail`. The pointer's `touch` state (`paintCursor`) swaps the arrow for a fingertip. `InsertScene` + `insertTour`: right-click a wire → Insert node → search (the real `searchNodes`) → Round lands on the wire (`pickSpliceInputPort`) → Delete bridges it back (`bridgeEdgesAcrossDeletedNodes`). `BudgetScene` + `budgetTour`: the right end of the real Gas Giant texture (imported when the slide first shows — building it needs the Babel chunk, never on the boot), GroupNode's own `GroupHeader`, the cost bar's own `CostMeter`/`deviceOptionText` over the user's device; a wire dragged from the asset's last node (its `outputEdges`) into Color takes `computeReachableCost` from 0 past the budget. `ImageScene` + `imageTour` + `introPhoto`: a painted 12 MP photo (no photo ships in the app) run through the REAL drop — `encodeImageFile` at the device cap in 'convert' mode (imported on slide show), `resolveImageDrop` with the stash answered "kept" (nothing reaches IndexedDB), `makeImageNodeFromEncode` with `filePictureOrientation` — then the app's own `ImageImportPanel` question and `LimitNoticePanel` device-downscale notice, and the card through ShaderNode's `ImageCardPicture`. `FolderScene` + `folderTour`: an EXAMPLE folder in a strip of the toolbar (its own classes, `WorkFolderRow`/`WorkFolderCaretIcon` from Layout/WorkFolderView.tsx — WorkFolder.tsx itself is desktop-only); the shader name is `adoptShaderName`, the current row `sameShaderFile`. `CodeScene` + `codeTour`: edits typed into `graphToCode`'s fresh-Output text, each Apply built by `codeToGraph` → `autoExposeConnectedParamPorts` → `autoLayout` (both imported on slide show); the first edit retypes the red-fallback `return`, which would otherwise parse into a Vec3; Time/Sin/noise move through `applyClockFrame`/`applyWaveFrame`/`renderNoisePreview`, live wires march; the code panel is its own tab bar (CodeEditor.css) over a Monaco-coloured body, never Monaco. `ExposeScene` + `exposeTour`: right-click menus cropped to the rows that expose — the generic menu's `ParamRowView` (noise "scale" → a socket at PreviewNode's `handleTop`) and the Output menu's `OutputPortToggles` (Emissive) — then a wire into the new socket, aimed at a hidden probe Output that already shows it. `DrawScene` + `drawTour`: the canvas bar's markup with the real draw cluster (`DrawToolbarView`), a freehand flower (`flowerStrokes` — five uneven petals, a scribbled centre, a swaying stem and a leaf, every deviation from a SEEDED random source so each loop draws the same flower) in the user's own pen colour/opacity at width 7, drawn as DrawingLayer draws ink, then the Add menu's Annotate row and a note (`NoteFace`, NoteTileCard) typed into; the canvas there is at 80%, the chrome at chrome size. `OrganizeScene` + `organizeTour`: nine nodes in a heap, a Partial marquee (`marqueeHits`) in React Flow's own `.react-flow__selection`/`.react-flow__nodesselection-rect` classes, a right-click on the selection opens AddNodeMenu's Selection rows, and Organize glides the nodes to where `organizeLayout` (engine/layoutEngine.ts — the pure half of the store's `organizeSelection`) puts them from each card's measured layout box; the canvas moves them at once. Three scenes run the REAL preview document (`IntroPreview`: `tslToPreviewHTML` + the pane's message contract — `fs:obj-model`, hot `fs:shader`, `fs:uniform`; cold-built only per model; never ShaderPreview, which owns the store): `PreviewScene` + `previewTour` — Alt/⌥-click → `previewGraph`'s derived module, the canvas's own `fs-previewing` dim and PreviewRoute line; Ctrl/⌘-click → the Attach rows over `attachSinks` → wired; `UniformScene` + `uniformTour` — a Property node's slider (the pane's uniform-row classes, `seedBounds`) posting `fs:uniform`, then Set as default as a graph change swapped in hot; `ModelScene` + `modelTour` — `public/models/intro/` Rodin then PoseableFigure dropped onto the view (1.5× the pane's framing) under Vertex Wave + Checker, each with its own `ShaderTune` (the sphere: amplitude 0.03, the Checker's own UVs; Rodin: 0.03 and a 3D checker over `positionGeometry` — x, y AND z, since a flat x/y projection smears into a band — instead of `uv()`; the figure: 0.001, frequency 150, count 16, the same 3D checker), the Metals palette's Iron for its dark squares (`combinePresets`, the presets' own `outputEdges`), the import question only where `planGlbImportDialog` offers it, drawn by `GlbImportPanel`. Shared by every scene: `placeMenu` (IntroStage), `IntroConnectionLine`/`connectionEnd`/`connectionPath` (sceneGraph). The header carries the study dialogs' `LangSwitch`; there is no ✕; the toolbar's "i" (after reload; in a study too) fires `INTRO_OPEN_EVENT` with its opener (`IntroVia`), which App answers. Cards are the tiles' replicas at canvas size; nothing writes the store, storage or IndexedDB (`introScenes.test.ts` sweeps every file) — GraphModal's feed-the-store trick would let the autosave write the intro over the user's work
eval/ — the user-study mode (`…/eval`, `…/evalpro`, `…/evalp`): `evalMode` + `evalArm` (the sessionStorage arm, sampled once at module init — a LEAF so the registry can read it), `EvalGate` + `ConsentModal` + `DataDisclosureModal` (consent BEFORE any logging) + `LangSwitch` (the ONE EN/LV button every study dialog renders — the consent, its Details, the finish dialog, the questionnaire and its thank-you screen; they are modal, so the toolbar's is unreachable behind the backdrop), `answerLanguage` (which language the SUS was ANSWERED in — per item, or `mixed`), `telemetry` + `telemetryModel` (the closed event vocabulary and the derived summary), `evalTask` + `evalContext` (which task, which price table, which device), `background` + `proQuestions` + `SusModal` + `susScore` (the questionnaire), `EvalFinishModal`, `previewShot`, `evalPackage` (the zip) and `evalUpload`. See the Eval-mode convention for what each one is FOR — this tree entry is only the map
embed/ — FastShaders inside another app's iframe (`?embed=1`, SuperWXR's shader panel): `embedMode` (a LEAF like `eval/evalArm.ts`, sampled ONCE at module init and never true at the top level — only an embedding page can answer the bridge) + `EmbedBridge` (a `lazyOr` chunk from App.tsx on EvalGate's terms, so an ordinary boot never fetches the import path and the export assembly it pulls; never in a study, since it is a second way to change the graph). postMessage to and from the PARENT window at the page's own origin only: in `superwxr:open` (a shader `.js` export — a FASTSHADERS_PROJECT_V1 block restores the graph, a bare module is parsed back — through `importShaderText`, the "Load Script…" path, no Open/Add dialog); out `superwxr:ready` once it listens, `superwxr:module` (the settled EXPORT module with images inlined and the project block appended, trailing-debounced like the preview, deduped on the text) and `superwxr:export` (what `downloadShader` would have downloaded — exportShader.ts posts it instead of saving when embedded). Its store subscription compares three references and the Output node's settings object by reference — it runs once per drag frame
tutorials/ — the Tutorials tab's hands-on LESSONS on the REAL canvas — the intro's opposite: it WRITES the store, which is why it lives outside `intro/`'s no-write sweep (introScenes.test.ts). `tutorialTypes.ts` (the step model — add / wire / set / expose / look — the `TutorialRun` slice, `graphIsBlank`), `tutorialDefs.ts` (the twenty-two lessons: a LEAF of registry ids, never TSL — bootWave's rule), `tutorialRun.ts` (the pure evaluator: bindings, `evaluateStep`, `stepTargets`, `wantedNodeType`, `planAddPosition`), `tutorialAutoStep.ts` ("Do it for me" through `addNode` / `connectNodes` / `updateNodeData`, one undo entry per step), `tutorialsTab.ts` (the pseudo-tab's pure rules: offered outside a study and on `/evalpro`, a stored tab derived to All), `TutorialCard` (the numbered tile, a button — not a drag tile), `TutorialPanel` (bottom-right canvas chrome inside NodeEditor's canvas div, NOT a dialog: the canvas stays usable, plain keys on its buttons are stopped from the window handlers, Next reads `reached`), `tutorialHighlights.ts` (`data-fs-tutorial` marks on wrappers and handles, attributes never classes)
hooks/
useSyncEngine.ts — bidirectional graph↔code sync, undo/redo, complexity calc; graph→code is `panelCodegen` (the connected-only panel, whole in a study) and its Apply carries what that text hid (`utils/panelCarry.ts`) (its Cmd/Ctrl+Z handler stands down behind an aria-modal dialog and leaves a text field its native undo — `keepsNativeUndo` in `utils/isTypingTarget.ts`; number fields and selects keep graph undo)
Expand Down
28 changes: 28 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
import { AppLayout } from './components/Layout/AppLayout';
import { useSyncEngine } from './hooks/useSyncEngine';
import { isEvalMode } from './eval/evalMode';
import { isEmbedMode } from './embed/embedMode';
import { installFileDropGuard } from './utils/fileDropGuard';
import complexityData from './registry/complexity.json';
import type { AppNode, AppEdge, OutputNodeData, ShaderNodeData } from './types';
Expand Down Expand Up @@ -96,6 +97,24 @@ const EvalGate = lazyOr(
EvalGateUnavailable,
);

/**
* The SuperWXR embed bridge (embed/EmbedBridge.tsx), a lazy chunk on the same
* terms as EvalGate: `isEmbedMode()` is sampled once at module init
* (embed/embedMode.ts), so an ordinary visitor's boot never creates the
* element and never fetches the chunk — the bridge pulls the import path and
* the export assembly, which the boot wave must not carry for a feature only an
* embedding page can use. A chunk that fails to load leaves the host page
* without its `superwxr:ready`; the host's panel says so itself, and nothing
* rendered here could reach it.
*/
function EmbedBridgeUnavailable() {
return null;
}
const EmbedBridge = lazyOr(
() => import('./embed/EmbedBridge').then((m) => m.EmbedBridge),
EmbedBridgeUnavailable,
);

/**
* The intro window, a lazy chunk fetched only on a boot that shows it (the
* first one, or `?intro`). A failed fetch shows nothing — the intro is
Expand Down Expand Up @@ -348,6 +367,15 @@ export default function App() {
return (
<ReactFlowProvider>
<SyncController />
{/* Embedded in another app's iframe (?embed=1, SuperWXR's shader panel):
the postMessage bridge that opens a module, broadcasts the settled
export module and hands EXPORT to the host. Never in a study. The
bridge renders nothing, so the Suspense fallback is null too. */}
{isEmbedMode() && !isEvalMode() && (
<Suspense fallback={null}>
<EmbedBridge />
</Suspense>
)}
{/* User-study mode (…/eval): consent gate + telemetry boot. The guard
keeps every eval-mode module inert for normal sessions — and, since
EvalGate is lazy, unfetched. Suspense fallback is null on purpose: the
Expand Down
Loading