Adrian Perez 42bd05db9c 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>
2026-04-18 18:47:36 +02:00

79 lines
3.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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:147–157)
↓
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.