diff --git a/frontend/src/pages/Facility3DViewerPage.tsx b/frontend/src/pages/Facility3DViewerPage.tsx index 683c519..5921690 100644 --- a/frontend/src/pages/Facility3DViewerPage.tsx +++ b/frontend/src/pages/Facility3DViewerPage.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState, Suspense, useRef, useMemo } from 'react'; +import { useEffect, useState, Suspense, useRef, useMemo, Component, ReactNode } from 'react'; import { Canvas, useFrame } from '@react-three/fiber'; import { OrbitControls, Text, Instances, Instance, Html } from '@react-three/drei'; import { layoutApi, Floor3DData } from '../lib/layoutApi'; @@ -17,49 +17,211 @@ const COLORS = { ROOM_WALL: '#374151', // gray-700 }; -function PlantInstances({ positions, onPlantClick }: { positions: any[], onPlantClick: (p: any) => void }) { - if (!positions || positions.length === 0) return null; +// Define State interface +interface ErrorBoundaryState { + hasError: boolean; + error: Error | null; +} - const plants = positions.filter(p => p.plant); - const emptySlots = positions.filter(p => !p.plant); +class ErrorBoundary extends Component<{ children: ReactNode }, ErrorBoundaryState> { + constructor(props: { children: ReactNode }) { + super(props); + this.state = { hasError: false, error: null }; + } + + static getDerivedStateFromError(error: Error): ErrorBoundaryState { + return { hasError: true, error }; + } + + render() { + if (this.state.hasError) { + console.error("3D Viewer Error Caught:", this.state.error); + return ( + +
+

3D Rendering Error

+

{this.state.error?.message}

+
+ + ); + } + return this.props.children; + } +} + +// ... unchanged parts ... + +// Update Facility3DViewerPage to use ErrorBoundary +export default function Facility3DViewerPage() { + const [status, setStatus] = useState('Initializing...'); + const [floorData, setFloorData] = useState(null); + const [selectedPlant, setSelectedPlant] = useState(null); + + useEffect(() => { + loadData(); + }, []); + + async function loadData() { + setStatus('Loading layout...'); + try { + const props = await layoutApi.getProperties(); + if (props[0]?.buildings[0]?.floors[0]) { + const floorId = props[0].buildings[0].floors[0].id; + setStatus('Fetching 3D assets...'); + const data = await layoutApi.getFloor3D(floorId); + setFloorData(data); + setStatus(''); + } else { + setStatus('No floor layout found'); + } + } catch (err) { + setStatus('Error: ' + (err as Error).message); + } + } + + return ( +
+ {/* Header Overlay */} +
+
+ + Back + +
+

+ Facility Viewer 3D + BETA +

+

+ {floorData ? `${floorData.floor.name} • ${floorData.stats.occupiedPositions} Plants` : 'Loading...'} +

+
+
+ + {/* Legend */} +
+
Veg
+
Flower
+
Dry
+
Empty
+
+
+ + {/* Error/Status Overlay */} + {status && ( +
+
+ +

{status}

+
+
+ )} + + {/* Selection Overlay */} + {selectedPlant && ( +
+
+

{selectedPlant.plant.tagNumber}

+ +
+
+
+ Strain: + {selectedPlant.plant.strain || 'Unknown'} +
+
+ Stage: + {selectedPlant.plant.stage || 'N/A'} +
+
+ Batch: + {selectedPlant.plant.batchName || '-'} +
+
+ Location: + R{selectedPlant.row} T{selectedPlant.tier} S{selectedPlant.slot} +
+
+
+ )} + + + + Loading 3D Scene...}> + {floorData && ( + + )} + + + +
+ ); +} + +function PlantInstances({ positions, onPlantClick }: { positions: any[], onPlantClick: (p: any) => void }) { + if (!positions || !Array.isArray(positions) || positions.length === 0) return null; + + // Safety filter + const safePositions = positions.filter(p => p && typeof p.x === 'number' && typeof p.y === 'number' && typeof p.z === 'number'); + + const plants = safePositions.filter(p => p.plant); + const emptySlots = safePositions.filter(p => !p.plant); + + // Limit instances to prevent crash on huge datasets just in case + if (safePositions.length > 5000) { + console.warn('Too many positions to render:', safePositions.length); + return null; + } return ( {/* Active Plants */} - { - e.stopPropagation(); - const index = e.instanceId; - if (index !== undefined && plants[index]) { - onPlantClick(plants[index]); - } - }} - > - - - {plants.map((pos, i) => ( - - ))} - + {plants.length > 0 && ( + { + e.stopPropagation(); + const index = e.instanceId; + if (index !== undefined && plants[index]) { + onPlantClick(plants[index]); + } + }} + > + + + {plants.map((pos, i) => ( + + ))} + + )} - {/* Empty Slots (Small dots) */} - - - - {emptySlots.map((pos, i) => ( - - ))} - + {/* Empty Slots */} + {emptySlots.length > 0 && ( + + + + {emptySlots.map((pos, i) => ( + + ))} + + )} ); }