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