fix: harden editor scene loading

This commit is contained in:
Aymeric Rabot
2026-06-09 19:54:42 -04:00
parent e81dc63b28
commit eb00f9f477
3 changed files with 99 additions and 1 deletions
@@ -72,6 +72,7 @@ const PAINT_CURSOR_BADGE_COLOR = '#818cf8'
const PAINT_CURSOR_BADGE_DISABLED_COLOR = '#94a3b8'
const PAINT_CURSOR_BADGE_OFFSET_X = 14
const PAINT_CURSOR_BADGE_OFFSET_Y = 14
const SCENE_READY_FALLBACK_MS = 8000
const EDITOR_HOVER_STYLES: HoverStyles = {
default: { visibleColor: 0x00_aa_ff, hiddenColor: 0xf3_ff_47, strength: 5, pulse: true },
delete: { visibleColor: 0xef_44_44, hiddenColor: 0x99_1b_1b, strength: 6, pulse: false },
@@ -1059,6 +1060,19 @@ export default function Editor({
setIsViewerSceneReady(ready)
}, [])
useEffect(() => {
if (isLoading || isSceneLoading || !hasLoadedInitialScene || isViewerSceneReady) return
const timer = window.setTimeout(() => {
console.warn('[editor] viewer scene readiness timed out; showing editor shell anyway', {
sceneReadyKey,
})
setIsViewerSceneReady(true)
}, SCENE_READY_FALLBACK_MS)
return () => window.clearTimeout(timer)
}, [hasLoadedInitialScene, isLoading, isSceneLoading, isViewerSceneReady, sceneReadyKey])
const showLoader = isLoading || isSceneLoading || !hasLoadedInitialScene || !isViewerSceneReady
const firstPersonPreviousLevelRef = useRef(useViewer.getState().selection.levelId)
+1 -1
View File
@@ -71,7 +71,7 @@ export function resolveSurfaceColor(
// The active scene theme may tint individual roles (e.g. Mediterranean's blue
// roof); fall back to the chosen colour preset's palette when it doesn't.
const tints = sceneThemeId ? getSceneTheme(sceneThemeId).clayTints : undefined
return tints?.[role] ?? PRESET_PALETTES[preset][role]
return tints?.[role] ?? (PRESET_PALETTES[preset] ?? CLAY_PALETTE)[role]
}
// DoubleSide on any NodeMaterial inside the MRT scenePass (SSGI's output /
+84
View File
@@ -7,6 +7,7 @@ import { create } from 'zustand'
import { persist } from 'zustand/middleware'
import type { EdgeMode } from '../lib/edge-style'
import type { ColorPreset, RenderShading } from '../lib/materials'
import { SCENE_THEME_IDS } from '../lib/scene-themes'
export type RenderContext = 'editor' | 'viewer'
@@ -114,6 +115,85 @@ type ViewerState = {
setInputDragging: (dragging: boolean) => void
}
type PersistedViewerState = Partial<
Pick<
ViewerState,
| 'cameraMode'
| 'sceneTheme'
| 'shadingByContext'
| 'textures'
| 'colorPreset'
| 'edges'
| 'shadows'
| 'unit'
| 'levelMode'
| 'wallMode'
| 'projectPreferences'
>
>
const CAMERA_MODES = ['perspective', 'orthographic'] as const
const RENDER_SHADINGS = ['solid', 'rendered'] as const
const COLOR_PRESETS = ['clay', 'white', 'mono', 'blueprint'] as const
const EDGE_MODES = ['off', 'soft', 'strong'] as const
const UNITS = ['metric', 'imperial'] as const
const LEVEL_MODES = ['stacked', 'exploded', 'solo', 'manual'] as const
const WALL_MODES = ['up', 'cutaway', 'down'] as const
function pickString<T extends string>(value: unknown, allowed: readonly T[], fallback: T): T {
return typeof value === 'string' && allowed.includes(value as T) ? (value as T) : fallback
}
function normalizeShadingByContext(value: unknown): ViewerState['shadingByContext'] {
if (!value || typeof value !== 'object' || Array.isArray(value)) return {}
const next: ViewerState['shadingByContext'] = {}
for (const [context, shading] of Object.entries(value)) {
if (context !== 'editor' && context !== 'viewer') continue
next[context] = pickString<RenderShading>(shading, RENDER_SHADINGS, 'rendered')
}
return next
}
function normalizeProjectPreferences(value: unknown): ViewerState['projectPreferences'] {
if (!value || typeof value !== 'object' || Array.isArray(value)) return {}
const next: ViewerState['projectPreferences'] = {}
for (const [projectId, preferences] of Object.entries(value)) {
if (!preferences || typeof preferences !== 'object' || Array.isArray(preferences)) continue
const record = preferences as Record<string, unknown>
next[projectId] = {
...(typeof record.showScans === 'boolean' ? { showScans: record.showScans } : {}),
...(typeof record.showGuides === 'boolean' ? { showGuides: record.showGuides } : {}),
...(typeof record.showGrid === 'boolean' ? { showGrid: record.showGrid } : {}),
}
}
return next
}
function normalizePersistedViewerState(value: unknown): PersistedViewerState {
if (!value || typeof value !== 'object' || Array.isArray(value)) return {}
const state = value as Record<string, unknown>
return {
cameraMode: pickString<ViewerState['cameraMode']>(
state.cameraMode,
CAMERA_MODES,
'perspective',
),
sceneTheme: pickString(state.sceneTheme, SCENE_THEME_IDS, 'studio'),
shadingByContext: normalizeShadingByContext(state.shadingByContext),
textures: typeof state.textures === 'boolean' ? state.textures : true,
colorPreset: pickString<ColorPreset>(state.colorPreset, COLOR_PRESETS, 'clay'),
edges: pickString<EdgeMode>(state.edges, EDGE_MODES, 'soft'),
shadows: typeof state.shadows === 'boolean' ? state.shadows : true,
unit: pickString<ViewerState['unit']>(state.unit, UNITS, 'metric'),
levelMode: pickString<ViewerState['levelMode']>(state.levelMode, LEVEL_MODES, 'stacked'),
wallMode: pickString<ViewerState['wallMode']>(state.wallMode, WALL_MODES, 'up'),
projectPreferences: normalizeProjectPreferences(state.projectPreferences),
}
}
const useViewer = create<ViewerState>()(
persist(
(set) => ({
@@ -267,6 +347,10 @@ const useViewer = create<ViewerState>()(
}),
{
name: 'viewer-preferences',
merge: (persistedState, currentState) => ({
...currentState,
...normalizePersistedViewerState(persistedState),
}),
partialize: (state) => ({
cameraMode: state.cameraMode,
sceneTheme: state.sceneTheme,