fix: harden editor scene loading
This commit is contained in:
@@ -72,6 +72,7 @@ const PAINT_CURSOR_BADGE_COLOR = '#818cf8'
|
|||||||
const PAINT_CURSOR_BADGE_DISABLED_COLOR = '#94a3b8'
|
const PAINT_CURSOR_BADGE_DISABLED_COLOR = '#94a3b8'
|
||||||
const PAINT_CURSOR_BADGE_OFFSET_X = 14
|
const PAINT_CURSOR_BADGE_OFFSET_X = 14
|
||||||
const PAINT_CURSOR_BADGE_OFFSET_Y = 14
|
const PAINT_CURSOR_BADGE_OFFSET_Y = 14
|
||||||
|
const SCENE_READY_FALLBACK_MS = 8000
|
||||||
const EDITOR_HOVER_STYLES: HoverStyles = {
|
const EDITOR_HOVER_STYLES: HoverStyles = {
|
||||||
default: { visibleColor: 0x00_aa_ff, hiddenColor: 0xf3_ff_47, strength: 5, pulse: true },
|
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 },
|
delete: { visibleColor: 0xef_44_44, hiddenColor: 0x99_1b_1b, strength: 6, pulse: false },
|
||||||
@@ -1059,6 +1060,19 @@ export default function Editor({
|
|||||||
setIsViewerSceneReady(ready)
|
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 showLoader = isLoading || isSceneLoading || !hasLoadedInitialScene || !isViewerSceneReady
|
||||||
|
|
||||||
const firstPersonPreviousLevelRef = useRef(useViewer.getState().selection.levelId)
|
const firstPersonPreviousLevelRef = useRef(useViewer.getState().selection.levelId)
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ export function resolveSurfaceColor(
|
|||||||
// The active scene theme may tint individual roles (e.g. Mediterranean's blue
|
// 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.
|
// roof); fall back to the chosen colour preset's palette when it doesn't.
|
||||||
const tints = sceneThemeId ? getSceneTheme(sceneThemeId).clayTints : undefined
|
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 /
|
// DoubleSide on any NodeMaterial inside the MRT scenePass (SSGI's output /
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { create } from 'zustand'
|
|||||||
import { persist } from 'zustand/middleware'
|
import { persist } from 'zustand/middleware'
|
||||||
import type { EdgeMode } from '../lib/edge-style'
|
import type { EdgeMode } from '../lib/edge-style'
|
||||||
import type { ColorPreset, RenderShading } from '../lib/materials'
|
import type { ColorPreset, RenderShading } from '../lib/materials'
|
||||||
|
import { SCENE_THEME_IDS } from '../lib/scene-themes'
|
||||||
|
|
||||||
export type RenderContext = 'editor' | 'viewer'
|
export type RenderContext = 'editor' | 'viewer'
|
||||||
|
|
||||||
@@ -114,6 +115,85 @@ type ViewerState = {
|
|||||||
setInputDragging: (dragging: boolean) => void
|
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>()(
|
const useViewer = create<ViewerState>()(
|
||||||
persist(
|
persist(
|
||||||
(set) => ({
|
(set) => ({
|
||||||
@@ -267,6 +347,10 @@ const useViewer = create<ViewerState>()(
|
|||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: 'viewer-preferences',
|
name: 'viewer-preferences',
|
||||||
|
merge: (persistedState, currentState) => ({
|
||||||
|
...currentState,
|
||||||
|
...normalizePersistedViewerState(persistedState),
|
||||||
|
}),
|
||||||
partialize: (state) => ({
|
partialize: (state) => ({
|
||||||
cameraMode: state.cameraMode,
|
cameraMode: state.cameraMode,
|
||||||
sceneTheme: state.sceneTheme,
|
sceneTheme: state.sceneTheme,
|
||||||
|
|||||||
Reference in New Issue
Block a user