import { useEffect, useState, Suspense, Component, ReactNode, useCallback } from 'react'; import { Canvas } from '@react-three/fiber'; import { Html, CameraControls } from '@react-three/drei'; import { layoutApi, Floor3DData, Room3D, Position3D } from '../lib/layoutApi'; import { Loader2, ArrowLeft, Maximize, MousePointer2, Layers, Thermometer, Droplets, Activity, Leaf, Eye, EyeOff, Clock } from 'lucide-react'; import { Link, useSearchParams } from 'react-router-dom'; import { FacilityScene } from '../components/facility3d/FacilityScene'; import { PlantSearch } from '../components/facility3d/PlantSearch'; import { PlantLibrary } from '../components/facility3d/PlantLibrary'; import { HierarchyNav } from '../components/facility3d/HierarchyNav'; import { TimelineSlider } from '../components/facility3d/TimelineSlider'; import { PlantPosition, VisMode, COLORS } from '../components/facility3d/types'; import { CameraPreset, CameraPresetSelector } from '../components/facility3d/CameraPresets'; import { HierarchyBreadcrumb } from '../components/facility3d/HierarchyBreadcrumb'; // --- Error Boundary --- interface ErrorBoundaryState { hasError: boolean; error: Error | null; } class ErrorBoundary extends Component<{ children: ReactNode }, ErrorBoundaryState> { state: ErrorBoundaryState = { hasError: false, error: null }; static getDerivedStateFromError(error: Error): ErrorBoundaryState { return { hasError: true, error }; } render() { if (this.state.hasError) { return (

3D Rendering Error

{this.state.error?.message}

); } return this.props.children; } } // --- Main Component --- export default function Facility3DViewerPage() { const [status, setStatus] = useState('Initializing...'); const [floorData, setFloorData] = useState(null); const [selectedPlant, setSelectedPlant] = useState(null); const [visMode, setVisMode] = useState('STANDARD'); // Phase 2: Search state const [highlightedTags, setHighlightedTags] = useState([]); const [dimMode, setDimMode] = useState(false); const [beaconPosition, setBeaconPosition] = useState<[number, number, number] | null>(null); // Phase 3: Timeline state const [showTimeline, setShowTimeline] = useState(false); const [timelineDate, setTimelineDate] = useState(new Date()); const [isTimelinePlaying, setIsTimelinePlaying] = useState(false); // Floor selection state const [allFloors, setAllFloors] = useState<{ id: string; name: string; buildingName: string }[]>([]); const [selectedFloorId, setSelectedFloorId] = useState(null); // Camera preset state const [cameraPreset, setCameraPreset] = useState('ISOMETRIC'); const [focusTarget, setFocusTarget] = useState<{ x: number; z: number } | null>(null); const [searchParams] = useSearchParams(); const targetedPlantTag = searchParams.get('plant'); useEffect(() => { loadData(); }, []); // Load floor data when selected floor changes useEffect(() => { if (selectedFloorId) { loadFloorData(selectedFloorId); } }, [selectedFloorId]); // Deep link handler useEffect(() => { if (floorData && targetedPlantTag) { for (const room of floorData.rooms) { for (const section of room.sections) { const match = section.positions.find((p: Position3D) => p.plant?.tagNumber === targetedPlantTag); if (match) { const SCALE = 0.1; const x = (room.posX + section.posX + (match.column * 0.5)) * SCALE - (floorData.floor.width * SCALE / 2); const z = (room.posY + section.posY + (match.row * 0.5)) * SCALE - (floorData.floor.height * SCALE / 2); const y = 0.4 + (match.tier * 0.6); const plant: PlantPosition = { ...match, x, y, z }; setSelectedPlant(plant); setFocusTarget({ x, z }); setCameraPreset('ROOM_FOCUS'); setBeaconPosition([x, y, z]); setHighlightedTags([targetedPlantTag]); setDimMode(true); return; } } } } }, [floorData, targetedPlantTag]); async function loadFloorData(floorId: string) { setStatus('Fetching 3D scene...'); try { const data = await layoutApi.getFloor3D(floorId); setFloorData(data); setStatus(''); } catch (err) { setStatus('Error: ' + (err as Error).message); } } async function loadData() { setStatus('Loading layout...'); try { const props = await layoutApi.getProperties(); if (props[0]?.buildings?.length > 0) { // Collect all floors from all buildings const floors: { id: string; name: string; buildingName: string }[] = []; for (const building of props[0].buildings) { for (const floor of building.floors || []) { floors.push({ id: floor.id, name: floor.name, buildingName: building.name }); } } setAllFloors(floors); // Default to second floor if available (Upper Floor has more plants) if (floors.length > 0) { const defaultFloor = floors.length > 1 ? floors[1] : floors[0]; setSelectedFloorId(defaultFloor.id); } } else { setStatus('No floor layout found. Set up a facility first.'); } } catch (err) { setStatus('Error: ' + (err as Error).message); } } const focusRoom = (room: Room3D) => { if (!floorData) return; const SCALE = 0.1; // Calculate focus target in scaled world coordinates const x = (room.posX + room.width / 2) * SCALE - (floorData.floor.width * SCALE / 2); const z = (room.posY + room.height / 2) * SCALE - (floorData.floor.height * SCALE / 2); setFocusTarget({ x, z }); setCameraPreset('ROOM_FOCUS'); // Clear search state when navigating by room setBeaconPosition(null); setHighlightedTags([]); setDimMode(false); }; const resetView = () => { setCameraPreset('ISOMETRIC'); setFocusTarget(null); setBeaconPosition(null); setHighlightedTags([]); setDimMode(false); setSelectedPlant(null); }; // Breadcrumb navigation handler const handleBreadcrumbNavigate = (level: string) => { if (!floorData || !selectedPlant) return; if (level === 'room' && selectedPlant.breadcrumb?.room) { // Find the room by name and focus on it const room = floorData.rooms.find(r => r.name === selectedPlant.breadcrumb?.room); if (room) { focusRoom(room); } } else if (level === 'floor' || level === 'building' || level === 'facility') { // Return to floor overview setCameraPreset('ISOMETRIC'); setFocusTarget(null); } // Section/tier clicks stay on current view but could highlight }; // Search result selection handler const handleSearchSelect = useCallback((result: any) => { if (!floorData) return; const SCALE = 0.1; const x = (result.position.roomX + result.position.sectionX + (result.position.column * 0.5)) * SCALE - (floorData.floor.width * SCALE / 2); const z = (result.position.roomY + result.position.sectionY + (result.position.row * 0.5)) * SCALE - (floorData.floor.height * SCALE / 2); const y = 0.4 + (result.position.tier * 0.6); setFocusTarget({ x, z }); setCameraPreset('ROOM_FOCUS'); setBeaconPosition([x, y, z]); setHighlightedTags([result.label]); setDimMode(true); // Include breadcrumb data from search result setSelectedPlant({ ...result.plant, x, y, z, breadcrumb: { room: result.roomName, section: result.sectionCode, }, }); }, [floorData]); // Highlight handler for search typing const handleHighlightResults = useCallback((tags: string[]) => { setHighlightedTags(tags); if (tags.length > 0) { setDimMode(true); } else { setDimMode(false); setBeaconPosition(null); } }, []); return (
{/* Header */}
Back

Facility 3D BETA

{allFloors.length > 1 ? ( ) : ( {floorData?.floor.name || 'Loading...'} )} • {floorData?.stats.occupiedPositions || 0} Plants
{/* Search Bar */} {floorData && (
)} {/* Camera Preset Selector */}
{ setCameraPreset(preset); if (preset !== 'ROOM_FOCUS') { setFocusTarget(null); } }} />
{/* Legend */}
{visMode === 'STANDARD' && ( <>
Veg
Flower
Dry
)} {visMode === 'HEALTH' && ( <>
Good
Check
Critical
)} {visMode === 'TEMP' && ( <>
<65°F
75°F
>85°F
)} {visMode === 'HUMIDITY' && ( <>
<40%
60%
>80%
)}
{/* Room Navigation Sidebar */} {floorData && (
{/* Hierarchical Navigation */} {/* Focus Mode Toggle */} {highlightedTags.length > 0 && (
)} {/* Plant Library Browser */}
{ // Calculate position and select plant const room = floorData.rooms.find(r => r.name === roomName); const section = room?.sections.find(s => (s.code || s.name) === sectionCode); if (room && section) { const SCALE = 0.1; const x = (room.posX + section.posX + (plant.column * 0.5)) * SCALE - (floorData.floor.width * SCALE / 2); const z = (room.posY + section.posY + (plant.row * 0.5)) * SCALE - (floorData.floor.height * SCALE / 2); const y = 0.4 + (plant.tier * 0.6); setFocusTarget({ x, z }); setCameraPreset('ROOM_FOCUS'); setBeaconPosition([x, y, z]); setDimMode(false); setHighlightedTags([plant.plant?.tagNumber || '']); setSelectedPlant({ ...plant, x, y, z, breadcrumb: { room: roomName, section: sectionCode, }, }); } }} />
)} {/* Visualization Mode Buttons */}
{[ { mode: 'STANDARD' as VisMode, icon: Layers, label: 'Layout', active: 'bg-primary' }, { mode: 'HEALTH' as VisMode, icon: Activity, label: 'Health', active: 'bg-red-500' }, { mode: 'TEMP' as VisMode, icon: Thermometer, label: 'Temp', active: 'bg-orange-500' }, { mode: 'HUMIDITY' as VisMode, icon: Droplets, label: 'VPD', active: 'bg-blue-500' }, { mode: 'YIELD' as VisMode, icon: Leaf, label: 'Yield', active: 'bg-green-600' }, ].map(({ mode, icon: Icon, label, active }) => ( ))} {/* Timeline Toggle */}
{/* Loading/Error Overlay */} {status && (

{status}

{status.includes('Error') && ( )}
)} {/* Plant Selection Panel */} {selectedPlant?.plant && (
{/* Breadcrumb */} {selectedPlant.breadcrumb && (
)}
{selectedPlant.plant.batchId ? 'METRC TAG' : 'PLANT ID'}

{selectedPlant.plant.tagNumber}

Strain {selectedPlant.plant.strain || 'Unknown'}
Stage {selectedPlant.plant.stage || 'N/A'}
Batch {selectedPlant.plant.batchName || '—'}
Position R{selectedPlant.row} • C{selectedPlant.column} • T{selectedPlant.tier}
)} {/* 3D Canvas */} Loading 3D Scene...}> {floorData && ( )} {/* Timeline Slider - Phase 3 */} {showTimeline && (
setIsTimelinePlaying(!isTimelinePlaying)} />
)}
); }