ca-grow-ops-manager/frontend/src/components/facility3d/SmartRack.tsx
fullsizemalt eaa32c05fe
Some checks are pending
Deploy to Production / deploy (push) Waiting to run
Test / backend-test (push) Waiting to run
Test / frontend-test (push) Waiting to run
feat: add hierarchy breadcrumb and improve plant positioning
- Create HierarchyBreadcrumb component for navigation
- Fix SmartRack plant positioning within section bounds
- Add breadcrumb data to PlantPosition type
- Display breadcrumb path in plant selection panel
- Pass roomName through component hierarchy
2025-12-18 19:45:57 -08:00

155 lines
6 KiB
TypeScript

import { useMemo } from 'react';
import { Text } from '@react-three/drei';
import * as THREE from 'three';
import type { Section3D, Position3D } from '../../lib/layoutApi';
import { PlantPosition, VisMode } from './types';
import { PlantSystem } from './PlantSystem';
// Convert pixel coordinates to world units (DB stores pixels, 3D uses meters/units)
const SCALE = 0.1; // 1 pixel = 0.1 world units (so 700px = 70 units)
interface SmartRackProps {
section: Section3D;
visMode: VisMode;
onPlantClick: (plant: PlantPosition) => void;
highlightedTags?: string[];
dimMode?: boolean;
roomName?: string; // For breadcrumb
}
export function SmartRack({ section, visMode, onPlantClick, highlightedTags, dimMode, roomName }: SmartRackProps) {
// Scale section dimensions to world units
// Section posX/posY are RELATIVE to room, so we scale them for placement within room group
const scaledSection = {
posX: section.posX * SCALE,
posY: section.posY * SCALE,
width: section.width * SCALE,
height: section.height * SCALE,
};
// Calculate plant positions RELATIVE to section position (within room group)
const positions: PlantPosition[] = useMemo(() => {
const plantSpacing = 0.5; // Spacing between plants in world units
// Calculate how many columns/rows fit based on section dimensions
const maxCols = Math.max(...section.positions.map(p => p.column)) || 1;
const maxRows = Math.max(...section.positions.map(p => p.row)) || 1;
// Calculate actual spacing based on section dimensions
const colSpacing = scaledSection.width / (maxCols + 1);
const rowSpacing = scaledSection.height / (maxRows + 1);
const actualSpacing = Math.min(colSpacing, rowSpacing, plantSpacing);
return section.positions.map((pos: Position3D) => ({
...pos,
// Position plants within the scaled section bounds
x: scaledSection.posX + (pos.column * actualSpacing),
z: scaledSection.posY + (pos.row * actualSpacing),
y: 0.4 + (pos.tier * 0.6),
// Add breadcrumb data
breadcrumb: {
section: section.code || section.name,
room: roomName,
},
}));
}, [section, scaledSection, roomName]);
const distinctTiers = [...new Set(positions.map(p => p.tier))].sort((a, b) => a - b);
const distinctRows = [...new Set(positions.map(p => p.row))].sort((a, b) => a - b);
const distinctCols = [...new Set(positions.map(p => p.column))].sort((a, b) => a - b);
const plantSpacing = 0.5;
return (
<group>
{/* Section/Table Label */}
{visMode === 'STANDARD' && (
<Text
position={[scaledSection.posX + scaledSection.width / 2, 3, scaledSection.posY + scaledSection.height / 2]}
fontSize={0.8}
color="#94a3b8"
anchorX="center"
anchorY="bottom"
outlineColor="#000"
outlineWidth={0.03}
>
{section.code || section.name}
</Text>
)}
{/* Shelf/Tier surfaces */}
{distinctTiers.map(tier => (
<mesh
key={`shelf-${tier}`}
position={[
scaledSection.posX + scaledSection.width / 2,
0.35 + (tier * 0.6),
scaledSection.posY + scaledSection.height / 2
]}
rotation={[-Math.PI / 2, 0, 0]}
receiveShadow
>
<planeGeometry args={[scaledSection.width * 0.95, scaledSection.height * 0.95]} />
<meshStandardMaterial
color="#64748b"
roughness={0.3}
metalness={0.7}
side={THREE.DoubleSide}
/>
</mesh>
))}
{/* Support posts */}
{[0, 1].map(xOffset =>
[0, 1].map(zOffset => (
<mesh
key={`support-${xOffset}-${zOffset}`}
position={[
scaledSection.posX + (xOffset * scaledSection.width),
(distinctTiers.length * 0.6) / 2 + 0.2,
scaledSection.posY + (zOffset * scaledSection.height)
]}
>
<boxGeometry args={[0.08, distinctTiers.length * 0.6 + 0.3, 0.08]} />
<meshStandardMaterial color="#374151" roughness={0.4} metalness={0.8} />
</mesh>
))
)}
{/* Row labels */}
{visMode === 'STANDARD' && distinctRows.map(row => (
<Text
key={`row-${row}`}
position={[scaledSection.posX - 0.3, 0.8, scaledSection.posY + row * plantSpacing]}
rotation={[-Math.PI / 2, 0, 0]}
fontSize={0.18}
color="#64748b"
anchorX="right"
>
R{row}
</Text>
))}
{/* Column labels */}
{visMode === 'STANDARD' && distinctCols.map(col => (
<Text
key={`col-${col}`}
position={[scaledSection.posX + col * plantSpacing, 0.15, scaledSection.posY + scaledSection.height + 0.2]}
rotation={[-Math.PI / 2, 0, 0]}
fontSize={0.18}
color="#64748b"
anchorX="center"
>
C{col}
</Text>
))}
<PlantSystem
positions={positions}
visMode={visMode}
onPlantClick={onPlantClick}
highlightedTags={highlightedTags}
dimMode={dimMode}
/>
</group>
);
}