From d75be99748de214ccf28d72d5c8b0fd823dec84e Mon Sep 17 00:00:00 2001 From: fullsizemalt <106900403+fullsizemalt@users.noreply.github.com> Date: Wed, 17 Dec 2025 07:28:51 -0800 Subject: [PATCH] fix(3d-viewer): downgrade three.js/fiber/drei dependencies to stable versions and restore full viewer code - Fixes library crash on startup - Restores interactive facility map --- frontend/package.json | 8 +- frontend/src/pages/Facility3DViewerPage.tsx | 307 +++++++++++++++++++- 2 files changed, 296 insertions(+), 19 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index f8ee7a4..718a7af 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -16,8 +16,8 @@ "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-label": "^2.1.8", "@radix-ui/react-slot": "^1.2.4", - "@react-three/drei": "^10.7.7", - "@react-three/fiber": "^9.4.2", + "@react-three/drei": "9.107.2", + "@react-three/fiber": "8.16.8", "axios": "^1.6.2", "browser-image-compression": "^2.0.2", "class-variance-authority": "^0.7.1", @@ -35,7 +35,7 @@ "react-konva": "^18.2.10", "react-router-dom": "^7.10.1", "tailwind-merge": "^3.4.0", - "three": "^0.182.0", + "three": "0.165.0", "zustand": "^4.5.2" }, "devDependencies": { @@ -58,4 +58,4 @@ "vite": "^5.0.8", "vitest": "^1.0.0" } -} +} \ No newline at end of file diff --git a/frontend/src/pages/Facility3DViewerPage.tsx b/frontend/src/pages/Facility3DViewerPage.tsx index 7aec62d..1a6e14a 100644 --- a/frontend/src/pages/Facility3DViewerPage.tsx +++ b/frontend/src/pages/Facility3DViewerPage.tsx @@ -1,26 +1,303 @@ +import { useEffect, useState, Suspense, useMemo, Component, ReactNode } from 'react'; import { Canvas } from '@react-three/fiber'; +import { OrbitControls, Text, Instances, Instance, Html } from '@react-three/drei'; +import { layoutApi, Floor3DData } from '../lib/layoutApi'; +import { Loader2, ArrowLeft } from 'lucide-react'; import { Link } from 'react-router-dom'; -import { ArrowLeft } from 'lucide-react'; + +// Colors +const COLORS = { + VEG: '#4ade80', // green-400 + FLOWER: '#a855f7', // purple-500 + DRY: '#f59e0b', // amber-500 + CURE: '#78716c', // stone-500 + EMPTY_SLOT: '#374151', // gray-700 + ROOM_FLOOR: '#1f2937', // gray-800 + ROOM_WALL: '#374151', // gray-700 +}; + +// Define State interface +interface ErrorBoundaryState { + hasError: boolean; + error: Error | null; +} + +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; + } +} + +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; // Or return a subset + } + + return ( + + {/* Active Plants */} + {plants.length > 0 && ( + { + e.stopPropagation(); + const index = e.instanceId; + if (index !== undefined && plants[index]) { + onPlantClick(plants[index]); + } + }} + > + + + {plants.map((pos, i) => ( + + ))} + + )} + + {/* Empty Slots */} + {emptySlots.length > 0 && ( + + + + {emptySlots.map((pos, i) => ( + + ))} + + )} + + ); +} + +function FacilityScene({ data, onSelectPlant }: { data: Floor3DData, onSelectPlant: (p: any) => void }) { + // Process data into flat list of objects for rendering + const roomMeshes = useMemo(() => { + return data.rooms.map(room => { + // Room creates a floor area + return ( + + {/* Floor Label */} + + {room.name} + + + {/* Floor Plane */} + + + + + + {/* Render sections and positions */} + {room.sections.map(section => { + // Calculate positions for this section + const positions = section.positions.map(pos => { + // Simple grid layout logic for position coordinates relative to section + // Assuming typical rack dimensions + const spacing = 0.5; + const x = section.posX + (pos.column * spacing); + const z = section.posY + (pos.row * spacing); // Z is depth/row + const y = 0.5 + (pos.tier * 0.5); // Y is height/tier + + return { ...pos, x, y, z }; + }); + + return ( + + {/* Section Label */} + + {section.code} + + + + ); + })} + + ); + }); + }, [data, onSelectPlant]); + + return ( + <> + + + + + + {roomMeshes} + + + + + + ); +} 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 ( -
-
- - Back - -
-

Debug Mode: Red Cube

+
+ {/* 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}

+ {status.includes('Error') && ( + + )} +
+
+ )} + + {/* 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 && ( + + )} + +
);