Files
editor/packages/mcp/test-reports/research/R2-project-id.md
T
Adrian PerezandClaude Opus 4.7 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

3.9 KiB
Raw Blame History

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

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.