import React, { useEffect, useRef } from 'react'; import { Canvas } from '@react-three/fiber'; import { OrbitControls } from '@react-three/drei'; import * as THREE from 'three'; import useStore from './store.js'; import wsClient from './WebSocketClient.js'; import OfficeScene from './OfficeScene.jsx'; // ── Camera setup for true isometric ───────────────────────────── // Orthographic with rotation: 45° around Y, then ~35.264° tilt down (atan(1/√2)) // But the React-idiomatic way: position the camera and use lookAt // Simpler: use orbit controls locked to isometric-ish angle function CameraSetup() { // We use OrbitControls with limited rotation for a fixed-ish isometric feel return null; // Controls are in the Canvas } // ── UI Overlay ────────────────────────────────────────────────── function UIOverlay() { const agents = useStore(s => s.agents); const connected = useStore(s => s.wsConnected); return (
Spatial Office | {connected ? 'Live' : 'Offline'} | {Object.keys(agents).length} agents
); } // ── App ───────────────────────────────────────────────────────── export default function App() { const hasConnected = useRef(false); useEffect(() => { if (hasConnected.current) return; hasConnected.current = true; // Try to connect to WebSocket backend wsClient.connect(); // After 2 seconds, if no backend, start mock mode const mockTimer = setTimeout(() => { const state = useStore.getState(); if (!state.wsConnected) { console.log('[App] No backend found, starting mock mode'); wsClient.startMock(); } }, 2000); return () => { clearTimeout(mockTimer); wsClient.disconnect(); }; }, []); return (
{/* Orthographic camera for true isometric */} {/* Lighting */} {/* Limited orbit controls — zoom + pan, but locked rotation for isometric feel */} {/* Office scene */}
); } // ── Ortho camera wrapper ──────────────────────────────────────── function OrthoCamera() { const ref = useRef(); useEffect(() => { if (!ref.current) return; // On mount, set orthographic projection const camera = ref.current; const aspect = window.innerWidth / window.innerHeight; const frustumSize = 18; camera.left = -frustumSize * aspect / 2; camera.right = frustumSize * aspect / 2; camera.top = frustumSize / 2; camera.bottom = -frustumSize / 2; camera.near = 0.1; camera.far = 200; camera.position.set(14, 18, 14); camera.lookAt(0, 0, 5); camera.updateProjectionMatrix(); // Handle resize const onResize = () => { const a = window.innerWidth / window.innerHeight; camera.left = -frustumSize * a / 2; camera.right = frustumSize * a / 2; camera.top = frustumSize / 2; camera.bottom = -frustumSize / 2; camera.updateProjectionMatrix(); }; window.addEventListener('resize', onResize); return () => window.removeEventListener('resize', onResize); }, []); return ; }