docs(mcp): add 10-agent research on scene-save workflow

10 parallel research agents (R1-R10) investigated whether MCP can save
scenes as saveable entities that the user later opens in the editor —
rather than injecting via a dev-only window.__pascalScene hook.

Key findings:
- Editor is already backend-agnostic via onLoad/onSave callbacks (R2,R7)
- Current persistence is localStorage-only, single key (R1)
- Zero dynamic routes and no backend code yet — env declared (R4,R5)
- File import exists as "Load Build" but lacks Zod validation (R6)
- MCP-written scenes load cleanly into the editor today (R6, Casa del
  Sol test already proved this)
- Best path: filesystem-handoff this week, Supabase in weeks 2-4,
  Supabase Realtime for live mode in Q2. Skip Yjs for now. (R8)
- 4-5 weeks to private beta, 10-14 to GA (R9)
- 10 high-value ideas ranked; "photo -> scene" is the unblocker (R10)

SYNTHESIS.md pulls all 10 reports together with the recommended
implementation plan.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Adrian Perez
2026-04-18 18:47:36 +02:00
co-authored by Claude Opus 4.7
parent bdb47a6e5d
commit 42bd05db9c
11 changed files with 1463 additions and 0 deletions
@@ -0,0 +1,78 @@
# R2 — `projectId` semantics + Editor public API
## TL;DR
- **`projectId` is a namespace**, not a scene identifier.
- The Editor is **scene-agnostic** — it loads/saves via `onLoad` / `onSave` callbacks.
- The editor defaults to `loadSceneFromLocalStorage()` / `saveSceneToLocalStorage()` when callbacks aren't supplied.
- One project can contain many scenes (1:N). That mapping is a **host-app concern**, not an Editor concern.
## `<Editor>` public props
| Prop | Type | Default | Purpose |
|---|---|---|---|
| `projectId` | `string \| null` | none | Namespace key for UI-state localStorage, passed to host callbacks |
| `layoutVersion` | `'v1' \| 'v2'` | `'v1'` | Sidebar layout flavour |
| `onLoad` | `() => Promise<SceneGraph \| null>` | `loadSceneFromLocalStorage()` | Fetch initial scene on mount, and when `onLoad` identity changes (scene switch) |
| `onSave` | `(scene: SceneGraph) => Promise<void>` | `saveSceneToLocalStorage()` | Debounced (1000 ms) autosave after every scene change |
| `onDirty` | `() => void` | — | First change after last save |
| `onSaveStatusChange` | `(status: SaveStatus) => void` | — | `'idle' \| 'pending' \| 'saving' \| 'saved' \| 'paused' \| 'error'` |
| `onThumbnailCapture` | `(blob: Blob, cameraData) => void` | — | Auto-fires after 10 s idle OR manual "Generate thumbnail" |
| `previewScene` | `SceneGraph` | — | Read-only version-preview mode |
| `isVersionPreviewMode` | `boolean` | `false` | Locks scene graph |
| `isLoading` | `boolean` | `false` | Spinner overlay |
| `sidebarTabs` | `SidebarTab[]` | `[]` | v2 sidebar tabs w/ custom components |
| `appMenuButton`, `sidebarTop`, `navbarSlot`, `viewerToolbarLeft`, `viewerToolbarRight`, `sidebarOverlay`, `viewerBanner` | `ReactNode` | — | UI slots |
| `settingsPanelProps` | `{ projectId?, projectVisibility?, onVisibilityChange? }` | — | Settings-panel config |
| `sitePanelProps` | `{ projectId?, onUploadAsset?, onDeleteAsset? }` | — | Asset callbacks |
| `presetsAdapter` | `PresetsAdapter` | localStorage | Presets backend |
| `extraSidebarPanels` | `ExtraPanel[]` | `[]` | Additional v1 sidebar panels |
| `commandPaletteEmptyAction` | `CommandPaletteEmptyAction` | — | Fallback on no-match search |
## Data flow
```
host page → <Editor projectId="…"> → useEffect sync (index.tsx:757)
useViewer.setProjectId() (packages/viewer/src/store/use-viewer.ts:147157)
localStorage keys prefixed with `pascal-editor-selection:${projectId}` (lib/scene.ts:32)
Host callbacks: onUploadAsset(projectId, levelId, file, type), onDeleteAsset(projectId, url)
```
## Scene vs project
- Scene = `{ nodes, rootNodeIds, collections? }` — the graph
- Project = namespace (which buildings/levels/zones this user can select; which assets belong)
- 1 project → N scenes (via different `onLoad` identities → scene switch)
## Host-app integration — the minimal server-backed example
```tsx
const [sceneId, setSceneId] = useState<string | null>(null)
<Editor
projectId={projectId}
onLoad={sceneId
? () => fetch(`/api/projects/${projectId}/scenes/${sceneId}`).then(r => r.json())
: () => null} // null → blank
onSave={async (scene) => {
if (!sceneId) {
const r = await fetch(`/api/projects/${projectId}/scenes`, { method: 'POST', body: JSON.stringify(scene) })
setSceneId((await r.json()).id)
} else {
await fetch(`/api/projects/${projectId}/scenes/${sceneId}`, { method: 'PUT', body: JSON.stringify(scene) })
}
}}
/>
```
## Verdict
**The Editor already gives us everything we need on the client side.** The "scene save → open" workflow just needs:
1. A backend table / API keyed by `(projectId, sceneId)`.
2. A scene-picker UI in the host app (route `/scene/[id]` or a dropdown).
3. MCP writes to the same backend.
**No Editor changes required** for the baseline flow. The missing UI (scene list, naming) can be added to the Settings panel (per R3) when we want it inside the Editor package.