Skip to content
Merged
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
47 changes: 35 additions & 12 deletions packages/editor/src/components/editor/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ import { PanelManager } from '../ui/panels/panel-manager'
import { ErrorBoundary } from '../ui/primitives/error-boundary'
import { useSidebarStore } from '../ui/primitives/sidebar'
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/primitives/tooltip'
import { SceneLoader } from '../ui/scene-loader'
import { SceneLoader, SceneLoadFailed } from '../ui/scene-loader'
import { AppSidebar } from '../ui/sidebar/app-sidebar'
import type { ExtraPanel } from '../ui/sidebar/icon-rail'
import { SettingsPanel, type SettingsPanelProps } from '../ui/sidebar/panels/settings-panel'
Expand Down Expand Up @@ -1255,6 +1255,11 @@ function EditorContent({

const [isSceneLoading, setIsSceneLoading] = useState(false)
const [hasLoadedInitialScene, setHasLoadedInitialScene] = useState(false)
// A failed `onLoad` is shown as an error with a retry, never as an empty
// scene: an editor that renders the default scaffold after a failed load
// autosaves that scaffold over the real project.
const [sceneLoadError, setSceneLoadError] = useState<unknown>(null)
const [sceneLoadAttempt, setSceneLoadAttempt] = useState(0)
const [sceneReadyKey, setSceneReadyKey] = useState(0)
const [isViewerSceneReady, setIsViewerSceneReady] = useState(false)
const [previewStageMode, setPreviewStageMode] = useState<ViewerStageMode>('3d')
Expand Down Expand Up @@ -1296,6 +1301,7 @@ function EditorContent({

async function load() {
isLoadingSceneRef.current = true
setSceneLoadError(null)
setHasLoadedInitialScene(false)
setIsViewerSceneReady(false)
setIsSceneLoading(true)
Expand All @@ -1304,26 +1310,31 @@ function EditorContent({
// Session groups are not scene-graph state — clear on every load/switch.
useSessionGroups.getState().clearGroups()

let failed = false
try {
const sceneGraph = onLoad ? await onLoad() : loadSceneFromLocalStorage()
if (!cancelled) {
applySceneGraphToEditor(sceneGraph)
setIsViewerSceneReady(false)
setSceneReadyKey((key) => key + 1)
}
} catch {
} catch (error) {
// Leave the store unloaded and the autosave loop in its loading
// state: nothing may be written until a load actually succeeds.
failed = true
if (!cancelled) {
applySceneGraphToEditor(null)
setIsViewerSceneReady(false)
setSceneReadyKey((key) => key + 1)
console.error('[editor] scene load failed', error)
setSceneLoadError(error ?? new Error('Scene load failed'))
}
} finally {
if (!cancelled) {
setIsSceneLoading(false)
setHasLoadedInitialScene(true)
requestAnimationFrame(() => {
isLoadingSceneRef.current = false
})
if (!failed) {
setHasLoadedInitialScene(true)
requestAnimationFrame(() => {
isLoadingSceneRef.current = false
})
}
}
}
}
Expand All @@ -1333,7 +1344,11 @@ function EditorContent({
return () => {
cancelled = true
}
}, [onLoad, isLoadingSceneRef])
}, [onLoad, isLoadingSceneRef, sceneLoadAttempt])

const retrySceneLoad = useCallback(() => {
setSceneLoadAttempt((attempt) => attempt + 1)
}, [])

// Apply preview scene when version preview mode changes
useEffect(() => {
Expand Down Expand Up @@ -1522,7 +1537,11 @@ function EditorContent({
<FloorplanModeCoordinator />
{visibleLoader && (
<div className="fixed inset-0 z-60">
<SceneLoader className="bg-background" />
{sceneLoadError ? (
<SceneLoadFailed className="bg-background" onRetry={retrySceneLoad} />
) : (
<SceneLoader className="bg-background" />
)}
</div>
)}

Expand Down Expand Up @@ -1598,7 +1617,11 @@ function EditorContent({
<FloorplanModeCoordinator />
{visibleLoader && (
<div className="fixed inset-0 z-60">
<SceneLoader className="bg-background" />
{sceneLoadError ? (
<SceneLoadFailed className="bg-background" onRetry={retrySceneLoad} />
) : (
<SceneLoader className="bg-background" />
)}
</div>
)}

Expand Down
35 changes: 35 additions & 0 deletions packages/editor/src/components/ui/scene-loader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { useEffect, useState } from 'react'
import { cn } from '../../lib/utils'
import { Button } from './primitives/button'

const LOADERS = [
'pascal-loader-1',
Expand Down Expand Up @@ -36,3 +37,37 @@ export function SceneLoader({ className, fullScreen = false }: SceneLoaderProps)
</div>
)
}

interface SceneLoadFailedProps {
className?: string
onRetry: () => void
}

/**
* Replaces the loader when the host could not deliver the scene. Rendered
* INSTEAD of falling back to an empty default scene: a session that shows
* scaffold nodes after a failed load autosaves that scaffold over the real
* project (prod scene-wipe class, 2026-09-02).
*/
export function SceneLoadFailed({ className, onRetry }: SceneLoadFailedProps) {
return (
<div
className={cn(
'z-100 flex flex-col items-center justify-center gap-4 bg-background/90 px-6 text-center backdrop-blur-md',
'absolute inset-0',
className,
)}
role="alert"
>
<div className="flex flex-col gap-1">
<p className="font-medium text-foreground text-sm">This project couldn't be loaded</p>
<p className="text-muted-foreground text-sm">
Nothing was changed. Check your connection and try again.
</p>
</div>
<Button className="rounded-full" onClick={onRetry} size="sm" type="button">
Try again
</Button>
</div>
)
}
Loading