Spaces:
Sleeping
Sleeping
| 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<number | undefined>(undefined) | |
| const [activeSlug, setActiveSlug] = useState<string | undefined>(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 ( | |
| <div | |
| onDrop={handleDrop} | |
| onDragOver={handleDragOver} | |
| className="flex items-center justify-center h-screen text-white bg-black" | |
| > | |
| Drag and drop a .ply, .spz, .splat, .ksplat, or .sog file here to begin | |
| </div> | |
| ) | |
| } | |
| return ( | |
| <div onDrop={handleDrop} onDragOver={handleDragOver} className="w-screen h-screen relative bg-black"> | |
| <LoadedApp | |
| worlds={worlds} | |
| activeSlug={activeSlug} | |
| setActiveSlug={setActiveSlug} | |
| refreshingWorlds={refreshingWorlds} | |
| onRefreshWorlds={refreshWorlds} | |
| /> | |
| {uploading && <UploadOverlay progress={uploadProgress} />} | |
| </div> | |
| ) | |
| } | |
| 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<Record<string, number>>({}) | |
| const [hoveredObjectAssetId, setHoveredObjectAssetId] = useState<string | null>(null) | |
| const [hoveredObjectInstanceId, setHoveredObjectInstanceId] = useState<string | null>(null) | |
| const [hoveredWorldPreview, setHoveredWorldPreview] = useState<WorldHoverPreview | null>(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 ( | |
| <div className="relative w-screen h-screen bg-black overflow-hidden select-none [&_*]:focus:outline-none [&_*]:focus-visible:outline-none [&_*]:focus:ring-0 [&_*]:focus-visible:ring-0"> | |
| <ViewerModeHotkeys /> | |
| {!editing && LevaPanel && DebugPanel && showLeva && uiVisible && ( | |
| <div className="hidden md:block"> | |
| <Suspense fallback={null}> | |
| <LevaPanel | |
| collapsed={{ collapsed: levaCollapsed, onChange: setLevaCollapsed }} | |
| theme={{ sizes: { rootWidth: '380px', controlWidth: '180px' } }} | |
| /> | |
| <DebugPanel /> | |
| </Suspense> | |
| </div> | |
| )} | |
| <WorldViewer | |
| world={activeWorld} | |
| slug={entry.slug} | |
| sourceImageUrl={entry.sourceImageUrl} | |
| hoveredWorldPreview={hoveredWorldPreview} | |
| objectAssets={renderableObjectAssets} | |
| allObjectAssets={renderableAllObjectAssets} | |
| worldSfxUrls={entry.worldSfxUrls} | |
| sceneProject={editing || sceneProjectEnabled ? sceneProject : undefined} | |
| sceneProjectReady={sceneProjectReady} | |
| hoveredObjectAssetId={hoveredObjectAssetId} | |
| hoveredObjectInstanceId={hoveredObjectInstanceId} | |
| editing={editing} | |
| setEditing={setEditing} | |
| uiVisible={uiVisible} | |
| onObjectHover={handleObjectHover} | |
| onSceneProjectSaved={updateSceneProject} | |
| onRefreshWorlds={onRefreshWorlds} | |
| refreshingWorlds={refreshingWorlds} | |
| /> | |
| {uiVisible && ( | |
| <div className={`fixed inset-x-4 top-4 sm:left-4 sm:right-auto ${editing ? 'z-30' : 'z-10'}`}> | |
| <WorldSidebar | |
| worlds={worlds} | |
| activeSlug={entry.slug} | |
| setActiveSlug={setActiveSlug} | |
| setEditing={setEditing} | |
| compact={editing} | |
| activeSceneProject={sceneProject} | |
| activeSceneProjectEnabled={sceneProjectActive} | |
| onActiveSceneProjectToggle={() => setSceneProjectEnabled((enabled) => !enabled)} | |
| activeWorldVersionIndex={activeWorldVersionIndex} | |
| hoveredObjectAssetId={hoveredObjectAssetId} | |
| hoveredObjectInstanceId={hoveredObjectInstanceId} | |
| onObjectHover={handleObjectHover} | |
| onWorldHover={handleWorldHover} | |
| onActiveWorldVersionChange={(index) => setSelectedWorldVersions((versions) => ({ | |
| ...versions, | |
| [entry.slug]: index, | |
| }))} | |
| /> | |
| </div> | |
| )} | |
| {!editing && uiVisible && ( | |
| <> | |
| <TouchControls /> | |
| </> | |
| )} | |
| {uiVisible && ( | |
| <div className="fixed inset-x-0 bottom-4 z-20 flex justify-center px-4 sm:left-4 sm:right-auto sm:justify-start sm:px-0"> | |
| <BottomLeftControls /> | |
| </div> | |
| )} | |
| </div> | |
| ) | |
| } | |