import { Suspense, lazy, useCallback, useEffect, useRef, useState } from 'react' import { WorldViewer } from './components/WorldViewer' import { WorldSidebar } from './components/WorldSidebar' import { BottomLeftControls, ViewerModeHotkeys } from './components/BottomLeftControls' import { TouchControls } from './components/TouchControls' import { useSceneProject } from './modules/scene/useSceneProject' import { fetchWorlds, loadWorlds } from './utils/worldLoader' import { useDebugStore } from './store/debug' import { isEditableTarget } from './utils/dom' import type { WorldEntry, WorldHoverPreview, WorldObjectAsset, World } from './types/world' import { UploadOverlay } from './components/UploadOverlay' const LevaPanel = import.meta.env.DEV ? lazy(() => import('leva').then((module) => ({ default: module.Leva }))) : null const DebugPanel = import.meta.env.DEV ? lazy(() => import('./components/DebugPanel').then((module) => ({ default: module.DebugPanel }))) : null export function App() { const [worlds, setWorlds] = useState(loadWorlds) const [refreshingWorlds, setRefreshingWorlds] = useState(false) const [uploading, setUploading] = useState(false) const [uploadProgress, setUploadProgress] = useState(0) const refreshTimeoutRef = useRef(undefined) const [activeSlug, setActiveSlug] = useState(worlds[0]?.slug) const refreshWorlds = useCallback(async () => { if (!import.meta.env.DEV) return setRefreshingWorlds(true) try { const fetched = await fetchWorlds() let combined: WorldEntry[] = [] setWorlds((prev) => { const locals = prev.filter(w => w.slug.startsWith('local-')) combined = [...locals, ...fetched] return combined }) setActiveSlug((prevSlug) => { if (combined.length && !combined.find((w) => w.slug === prevSlug)) { return combined[0].slug } return prevSlug }) } catch (error) { console.warn('Could not refresh local world assets.', error) } finally { setRefreshingWorlds(false) } }, []) useEffect(() => { refreshWorlds() }, [refreshWorlds]) useEffect(() => { if (!import.meta.env.DEV) return const refreshSoon = () => { window.clearTimeout(refreshTimeoutRef.current) refreshTimeoutRef.current = window.setTimeout(() => { void refreshWorlds() }, 150) } import.meta.hot?.on('models-changed', refreshSoon) return () => { window.clearTimeout(refreshTimeoutRef.current) import.meta.hot?.off('models-changed', refreshSoon) } }, [refreshWorlds]) const handleDrop = async (e: React.DragEvent) => { e.preventDefault() const files = e.dataTransfer.files if (!files.length) return const file = files[0] const lowerName = file.name.toLowerCase() if (!lowerName.endsWith('.ply') && !lowerName.endsWith('.spz') && !lowerName.endsWith('.splat') && !lowerName.endsWith('.ksplat') && !lowerName.endsWith('.sog')) return // Create a local object URL to view the file instantly without uploading it to the server! const objectUrl = URL.createObjectURL(file) const isSpz = lowerName.endsWith('.spz') const worldId = 'local-' + Date.now() const newWorld: World = { world_id: worldId, display_name: file.name, assets: { splats: { ply_url: isSpz ? undefined : objectUrl, spz_urls: isSpz ? { full_res: objectUrl } : {}, semantics_metadata: { metric_scale_factor: 1, ground_plane_offset: 0, flip_y: true } }, mesh: { collider_mesh_url: '' }, imagery: { pano_url: '' }, thumbnail_url: '', caption: '' }, world_marble_url: '', tags: [], world_prompt: '', created_at: new Date().toISOString(), updated_at: new Date().toISOString() } const newEntry: WorldEntry = { slug: worldId, project: { slug: worldId, display_name: file.name }, world: newWorld, worldVersions: [{ index: 1, label: 'v1', complete: true, world: newWorld }], objectAssets: [], allObjectAssets: [], sourceImageVersions: [], worldSfxUrls: [] } setWorlds(prev => [newEntry, ...prev]) setActiveSlug(worldId) } const handleDragOver = (e: React.DragEvent) => { e.preventDefault() } if (!worlds.length && !uploading) { return (
Drag and drop a .ply, .spz, .splat, .ksplat, or .sog file here to begin
) } return (
{uploading && }
) } function LoadedApp({ worlds, activeSlug, setActiveSlug, refreshingWorlds, onRefreshWorlds, }: { worlds: WorldEntry[] activeSlug: string | undefined setActiveSlug: (slug: string) => void refreshingWorlds: boolean onRefreshWorlds: () => void }) { const [editing, setEditing] = useState(false) const levaCollapsed = useDebugStore((s) => s.levaCollapsed) const setLevaCollapsed = useDebugStore((s) => s.setLevaCollapsed) const [uiHidden, setUiHidden] = useState(false) const [sceneProjectEnabled, setSceneProjectEnabled] = useState(true) const [selectedWorldVersions, setSelectedWorldVersions] = useState>({}) const [hoveredObjectAssetId, setHoveredObjectAssetId] = useState(null) const [hoveredObjectInstanceId, setHoveredObjectInstanceId] = useState(null) const [hoveredWorldPreview, setHoveredWorldPreview] = useState(null) const slug = activeSlug ?? worlds[0]?.slug const entry = worlds.find((w) => w.slug === slug) ?? worlds[0] const showLeva = import.meta.env.VITE_SHOW_LEVA === 'true' const uiVisible = !uiHidden const defaultWorldVersionIndex = entry?.worldVersions[entry.worldVersions.length - 1]?.index const activeWorldVersionIndex = selectedWorldVersions[entry?.slug] ?? defaultWorldVersionIndex const activeWorldVersion = entry?.worldVersions.find((version) => version.index === activeWorldVersionIndex) const activeWorld = activeWorldVersion?.world ?? entry?.world const renderableObjectAssets = entry?.objectAssets.filter((asset) => asset.complete && asset.url) || [] const renderableAllObjectAssets = entry?.allObjectAssets.filter((asset) => asset.complete && asset.url) || [] const { sceneProject, sceneProjectReady, updateSceneProject } = useSceneProject(entry?.slug || '', '', entry?.sceneProject) const sceneProjectActive = Boolean(sceneProject && sceneProjectEnabled) useEffect(() => { setSceneProjectEnabled(true) setHoveredObjectAssetId(null) setHoveredObjectInstanceId(null) setHoveredWorldPreview(null) }, [entry?.slug]) const handleObjectHover = useCallback((asset: WorldObjectAsset, hovering: boolean, instanceId?: string) => { setHoveredObjectAssetId((current) => { if (hovering) return asset.assetId return current === asset.assetId ? null : current }) setHoveredObjectInstanceId((current) => { if (hovering) return instanceId ?? null return current === instanceId ? null : current }) }, []) const handleWorldHover = useCallback((preview: WorldHoverPreview, hovering: boolean) => { setHoveredWorldPreview((current) => { if (hovering) return preview return current?.slug === preview.slug ? null : current }) }, []) useEffect(() => { const onKeyDown = (event: KeyboardEvent) => { if (isEditableTarget(event.target)) return if (event.code !== 'Backquote') return event.preventDefault() setUiHidden((hidden) => !hidden) } window.addEventListener('keydown', onKeyDown) return () => window.removeEventListener('keydown', onKeyDown) }, []) if (!entry) return null return (
{!editing && LevaPanel && DebugPanel && showLeva && uiVisible && (
)} {uiVisible && (
setSceneProjectEnabled((enabled) => !enabled)} activeWorldVersionIndex={activeWorldVersionIndex} hoveredObjectAssetId={hoveredObjectAssetId} hoveredObjectInstanceId={hoveredObjectInstanceId} onObjectHover={handleObjectHover} onWorldHover={handleWorldHover} onActiveWorldVersionChange={(index) => setSelectedWorldVersions((versions) => ({ ...versions, [entry.slug]: index, }))} />
)} {!editing && uiVisible && ( <> )} {uiVisible && (
)}
) }