import { Component, Suspense, useRef, useEffect, useState, type ReactNode } from 'react' import { Tooltip } from '@radix-ui/themes' import { ArrowsClockwiseIcon, CaretDownIcon, CaretUpIcon } from '@phosphor-icons/react' import { Canvas, useThree } from '@react-three/fiber' import * as THREE from 'three' import { Html } from '@react-three/drei' import { Physics } from '@react-three/rapier' import { SplatRenderer } from '../modules/splat/SplatRenderer' import { EnvironmentMap } from '../modules/environment/EnvironmentMap' import { WorldCollider } from '../modules/collider/WorldCollider' import { GroundPlane } from '../modules/collider/GroundPlane' import { CharacterController, type CharacterControllerHandle } from '../modules/character/CharacterController' import { FlyController, type FlyControllerHandle } from '../modules/character/FlyController' import { ButterflyScene } from '../modules/butterfly/ButterflyScene' import { ObjectGrid } from '../modules/scene/ObjectGrid' import { OriginHelper } from '../modules/scene/OriginHelper' import { AudioManager } from '../modules/audio/AudioManager' import { PostProcessing } from '../modules/postprocessing/PostProcessing' import { DEFAULT_SHADOW_CATCHER_COLOR, DEFAULT_SHADOW_CATCHER_OPACITY, shadowCatcherColor, shadowCatcherOpacity } from '../modules/scene/shadows' import { getSplatUrl } from '../utils/worldLoader' import { useDebugStore } from '../store/debug' import { WorldRenderMode, ObjectRenderMode, ViewerQuality, type Vec3Tuple, type World, type WorldHoverPreview, type WorldObjectAsset, type WorldSceneProject } from '../types/world' import { AppButton } from './AppButton' import { chrome } from './AppChrome' function ModelLoader() { return (
Downloading Model...

This may take a moment depending on model size and network speed.

) } type CharHandle = CharacterControllerHandle | FlyControllerHandle const DEFAULT_ENVIRONMENT_URL = '/hdri.jpg' const DEFAULT_WORLD_SEMANTICS = { metric_scale_factor: 1, ground_plane_offset: 0, flip_y: true, } function sunPositionFromRotation(rotation: Vec3Tuple): Vec3Tuple { let x = 0 let y = 10 let z = 0 const [rx, ry, rz] = rotation const cx = Math.cos(rx) const sx = Math.sin(rx) const cy = Math.cos(ry) const sy = Math.sin(ry) const cz = Math.cos(rz) const sz = Math.sin(rz) ;[y, z] = [y * cx - z * sx, y * sx + z * cx] ;[x, z] = [x * cy + z * sy, -x * sy + z * cy] ;[x, y] = [x * cz - y * sz, x * sz + y * cz] return [x, y, z] } interface OptionalAssetBoundaryProps { label: string resetKey: string fallback?: ReactNode children: ReactNode } interface OptionalAssetBoundaryState { hasError: boolean } class OptionalAssetBoundary extends Component { state: OptionalAssetBoundaryState = { hasError: false } static getDerivedStateFromError(): OptionalAssetBoundaryState { return { hasError: true } } componentDidCatch(error: unknown) { console.warn(`Skipping optional world asset "${this.props.label}" because it failed to load.`, error) } componentDidUpdate(prevProps: OptionalAssetBoundaryProps) { if (prevProps.resetKey !== this.props.resetKey && this.state.hasError) { this.setState({ hasError: false }) } } render() { if (this.state.hasError) return this.props.fallback ?? null return this.props.children } } function GrayEnvironmentFallback() { return ( <> ) } function DefaultEnvironment({ intensity }: { intensity: number }) { return ( }> ) } interface Props { world?: World slug: string sourceImageUrl?: string hoveredWorldPreview?: WorldHoverPreview | null objectAssets: WorldObjectAsset[] allObjectAssets: WorldObjectAsset[] worldSfxUrls: string[] sceneProject?: WorldSceneProject sceneProjectReady?: boolean hoveredObjectAssetId?: string | null hoveredObjectInstanceId?: string | null editing?: boolean setEditing?: (editing: boolean) => void uiVisible?: boolean onObjectHover?: (asset: WorldObjectAsset, hovering: boolean, instanceId?: string) => void onSceneProjectSaved?: (project: WorldSceneProject) => void onRefreshWorlds?: () => void refreshingWorlds?: boolean } export function WorldViewer({ world: desiredWorld, slug: desiredSlug, sourceImageUrl, hoveredWorldPreview, objectAssets: desiredObjectAssets, allObjectAssets, worldSfxUrls, sceneProject, sceneProjectReady = true, hoveredObjectAssetId, hoveredObjectInstanceId, editing = false, setEditing, uiVisible = true, onObjectHover, onSceneProjectSaved, onRefreshWorlds, refreshingWorlds = false, }: Props) { const charRef = useRef(null) const worldRenderMode = useDebugStore((s) => s.worldRenderMode) const objectRenderMode = useDebugStore((s) => s.objectRenderMode) const viewerQuality = useDebugStore((s) => s.viewerQuality) const controllerMode = useDebugStore((s) => s.controllerMode) const butterfliesEnabled = useDebugStore((s) => s.butterfliesEnabled) const controllerResetToken = useDebugStore((s) => s.controllerResetToken) const environmentIntensity = useDebugStore((s) => s.environmentIntensity) const sunIntensity = useDebugStore((s) => s.sunIntensity) const sunColor = useDebugStore((s) => s.sunColor) const [sourceThumbnailCollapsed, setSourceThumbnailCollapsed] = useState(false) const colliderUrl = desiredWorld?.assets.mesh?.collider_mesh_url?.startsWith('/models/') ? desiredWorld.assets.mesh.collider_mesh_url : '' const panoUrl = desiredWorld?.assets.imagery?.pano_url?.startsWith('/models/') ? desiredWorld.assets.imagery.pano_url : '' useEffect(() => { charRef.current?.reset() }, [desiredSlug]) useEffect(() => { if (controllerResetToken > 0) charRef.current?.reset() }, [controllerResetToken]) const splatUrl = desiredWorld ? getSplatUrl(desiredWorld) : '' const { ground_plane_offset, flip_y, metric_scale_factor } = desiredWorld?.assets.splats.semantics_metadata ?? DEFAULT_WORLD_SEMANTICS const flipY = flip_y ?? true const baseMetricScaleFactor = metric_scale_factor ?? 1 const baseGroundPlaneOffset = ground_plane_offset ?? 0 const isHighQuality = viewerQuality === ViewerQuality.High const showScene = worldRenderMode !== WorldRenderMode.ObjectOnly const showSplat = showScene && objectRenderMode === ObjectRenderMode.Lit const showObjects = worldRenderMode !== WorldRenderMode.SplatOnly const activeSceneSun = sceneProject?.sun const activeSunIntensity = activeSceneSun?.intensity ?? sunIntensity const activeEnvironmentIntensity = activeSceneSun?.environmentIntensity ?? environmentIntensity const activeSunPosition = sunPositionFromRotation(activeSceneSun?.rotation ?? [0, 0, 0]) const activeMetricScaleFactor = sceneProject?.metricScaleFactor ?? baseMetricScaleFactor const defaultGroundPlaneOffset = baseGroundPlaneOffset * (activeMetricScaleFactor / baseMetricScaleFactor) const activeGroundPlaneOffset = sceneProject?.groundPlaneOffset ?? defaultGroundPlaneOffset const sceneGroundPlaneColliderEnabled = sceneProject?.groundPlaneColliderEnabled ?? true const activeGroundPlaneColliderEnabled = worldRenderMode === WorldRenderMode.ObjectOnly ? true : sceneGroundPlaneColliderEnabled const sceneShadowCatcherOpacity = sceneProject?.shadowCatcherOpacity const activeShadowCatcherOpacity = shadowCatcherOpacity(sceneShadowCatcherOpacity ?? DEFAULT_SHADOW_CATCHER_OPACITY) const sceneShadowCatcherColor = sceneProject?.shadowCatcherColor const activeShadowCatcherColor = shadowCatcherColor(sceneShadowCatcherColor ?? DEFAULT_SHADOW_CATCHER_COLOR) const objectPlacements = sceneProject?.instances ?? [] const objectPhysicsAssets = sceneProject?.instances.length ? allObjectAssets : desiredObjectAssets const activeControllerMode = controllerMode const hoveredObjectAsset = hoveredObjectAssetId ? allObjectAssets.find((asset) => asset.assetId === hoveredObjectAssetId) ?? desiredObjectAssets.find((asset) => asset.assetId === hoveredObjectAssetId) : undefined const activePreviewImageUrl = hoveredObjectAsset?.referenceImageUrl ?? hoveredObjectAsset?.thumbnailUrl ?? hoveredWorldPreview?.imageUrl ?? sourceImageUrl const activePreviewAlt = hoveredObjectAsset ? `${hoveredObjectAsset.name} reference image` : hoveredWorldPreview?.imageUrl ? hoveredWorldPreview.alt : 'Original source' return ( <> }> {activeControllerMode === 'fly' ? ( } preserveCameraOnMount={editing} /> ) : ( } /> )} {showScene && colliderUrl && ( )} {showObjects && !editing && ( )} {splatUrl && ( )} 0} color={sunColor} intensity={activeSunIntensity} position={activeSunPosition} shadow-mapSize={[2048, 2048]} shadow-bias={-0.0001} shadow-normalBias={0.02} shadow-camera-near={0.5} shadow-camera-far={30} shadow-camera-left={-20} shadow-camera-right={20} shadow-camera-top={20} shadow-camera-bottom={-20} /> {panoUrl && ( }> )} {!panoUrl && } {butterfliesEnabled && } {isHighQuality && } {uiVisible && ( setSourceThumbnailCollapsed((collapsed) => !collapsed)} /> )} ) } function CameraLogger() { const { camera } = useThree() useEffect(() => { const onKey = (e: KeyboardEvent) => { if (e.key.toLowerCase() === 'p') { const { x, y, z } = camera.position const euler = new THREE.Euler(0, 0, 0, 'YXZ').setFromQuaternion(camera.quaternion, 'YXZ') const yaw = euler.y const pitch = euler.x const params = new URLSearchParams(window.location.search) params.set('spawn', `${x.toFixed(2)},${y.toFixed(2)},${z.toFixed(2)},${yaw.toFixed(2)},${pitch.toFixed(2)}`) const newUrl = `${window.location.origin}${window.location.pathname}?${params.toString()}` console.log(`\n============== [Camera Spawn Logger] ==============`) console.log(`Position: ${x.toFixed(2)}, ${y.toFixed(2)}, ${z.toFixed(2)}`) console.log(`Rotation: Yaw ${yaw.toFixed(2)}, Pitch ${pitch.toFixed(2)}`) console.log(`Spawn URL: ${newUrl}`) console.log(`===================================================\n`) if (navigator.clipboard) { navigator.clipboard.writeText(newUrl).then(() => { console.log('✅ Spawn URL copied to clipboard!') }).catch(() => { console.log('❌ Failed to copy to clipboard') }) } } } window.addEventListener('keydown', onKey) return () => window.removeEventListener('keydown', onKey) }, [camera]) return null } function SourceImageControls({ activeSourceImageUrl, previewAlt, thumbnailCollapsed, refreshingWorlds, onRefreshWorlds, onThumbnailCollapseToggle, }: { activeSourceImageUrl?: string previewAlt: string thumbnailCollapsed: boolean refreshingWorlds: boolean onRefreshWorlds?: () => void onThumbnailCollapseToggle: () => void }) { if (!activeSourceImageUrl && !import.meta.env.DEV) return null return ( ) } function SourceThumbnailCollapseButton({ collapsed, onToggle, className = '', }: { collapsed: boolean onToggle: () => void className?: string }) { const Icon = collapsed ? CaretUpIcon : CaretDownIcon return ( ) } function RefreshWorldsButton({ refreshing, onRefresh, className = '', }: { refreshing: boolean onRefresh: () => void className?: string }) { return ( ) }