From 953c9781d27f705f8c459f5f6364cdd8f4df9e79 Mon Sep 17 00:00:00 2001
From: fullsizemalt <106900403+fullsizemalt@users.noreply.github.com>
Date: Fri, 19 Dec 2025 12:43:22 -0800
Subject: [PATCH] feat: Sprint 1 - Plant icons and grid overlay
Part of 3D Viewer Enhancement:
- PlantIcon.tsx: Stage-specific 2D shapes (leaf, flower, diamond)
- GridOverlay.tsx: Row/column/tier grid lines and labels
- PlantSystem.tsx: Refactored to use icons by default
- SmartRack.tsx: Integrated grid overlay
Icons: circle (clone), leaf (veg), flower (flowering), diamond (dry), square (cure)
---
docs/3D-VIEWER-ENHANCEMENT-PLAN.md | 251 ++++++++++++++++++
.../src/components/facility3d/GridOverlay.tsx | 235 ++++++++++++++++
.../src/components/facility3d/PlantIcon.tsx | 216 +++++++++++++++
.../src/components/facility3d/PlantSystem.tsx | 78 ++++--
.../src/components/facility3d/SmartRack.tsx | 19 +-
5 files changed, 782 insertions(+), 17 deletions(-)
create mode 100644 docs/3D-VIEWER-ENHANCEMENT-PLAN.md
create mode 100644 frontend/src/components/facility3d/GridOverlay.tsx
create mode 100644 frontend/src/components/facility3d/PlantIcon.tsx
diff --git a/docs/3D-VIEWER-ENHANCEMENT-PLAN.md b/docs/3D-VIEWER-ENHANCEMENT-PLAN.md
new file mode 100644
index 0000000..d2a36c8
--- /dev/null
+++ b/docs/3D-VIEWER-ENHANCEMENT-PLAN.md
@@ -0,0 +1,251 @@
+# 3D Facility Viewer Enhancement Plan
+
+## Overview
+
+Two major enhancements to transform the 3D facility viewer into a comprehensive, investor-ready data visualization platform.
+
+---
+
+## Part 1: Visual Style Refactor
+
+### Goal
+
+Replace 3D spheres with informative 2D icons/shapes that convey plant stage and health at a glance, while adding grid overlays for human readability.
+
+### Current State
+
+- Plants are 3D spheres with color coding
+- No visible grid structure
+- Stage indicated only by color
+
+### Target State
+
+- Flat 2D icons/polygons per growth stage
+- Visible row/column/tier grid lines
+- Labels for navigation (R1, R2, C1, C2, T1, T2)
+- Information-dense schematic view
+
+### Technical Approach
+
+#### Phase 1.1: Plant Icon System
+
+Replace sphereGeometry with stage-specific 2D shapes:
+
+| Stage | Shape | Color | Icon Concept |
+|-------|-------|-------|--------------|
+| CLONE_IN | Small circle | #3b82f6 Blue | Seedling dot |
+| VEGETATIVE | Leaf polygon | #22c55e Green | 3-point leaf |
+| FLOWERING | Flower polygon | #a855f7 Purple | 5-petal flower |
+| DRYING | Diamond | #f97316 Orange | Hanging diamond |
+| CURING | Square | #92400e Brown | Jar square |
+| HARVESTED | Circle outline | #6b7280 Gray | Empty circle |
+
+Implementation:
+
+- Use @react-three/drei Billboard for camera-facing icons
+- Create SVG paths for each stage
+- Scale based on zoom level
+- Add pulse animation for highlighted plants
+
+#### Phase 1.2: Grid Visualization
+
+Add visible structure lines:
+
+- Horizontal lines for rows (labeled R1-R8)
+- Vertical lines for columns (labeled C1-C10)
+- Tier indicators (T1-T3 vertical markers)
+- Semi-transparent to not obscure content
+- Toggle on/off via UI
+
+#### Phase 1.3: Section Improvements
+
+- Section boundaries with labeled borders
+- Section name prominently displayed
+- Row/column count indicator
+- Plant count badge
+
+### Files to Modify
+
+- PlantSystem.tsx - Replace sphere with icon system
+- SmartRack.tsx - Add grid overlay
+- types.ts - Add icon type definitions
+- New: PlantIcon.tsx, GridOverlay.tsx, SectionLabel.tsx
+
+---
+
+## Part 2: Plant Data Visualization System
+
+### Goal
+
+Create a comprehensive data card with lifecycle timeline, sensor data, and hierarchical drill-down capabilities.
+
+### Components
+
+#### 2.1: PlantDataCard (Main Component)
+
+A glassmorphic panel with:
+
+1. Header - Entity name, METRC tag, quick status badges
+2. Timeline - Visual lifecycle from clone to cure
+3. Vitals Grid - Health, temp, VPD, humidity gauges
+4. Activity Feed - Recent events/actions
+5. Actions - Quick action buttons
+
+#### 2.2: Lifecycle Timeline Component
+
+SVG-animated timeline showing plant journey:
+
+- Horizontal timeline with stage nodes
+- Filled nodes for completed stages
+- Animated current stage indicator
+- Day count labels
+- Estimated completion
+- Click stages to see historical data
+
+SVG Animation Ideas:
+
+- Pulse effect on current stage
+- Growth animation between stages
+- Color gradient transitions
+- Hover: expand stage details
+
+#### 2.3: VitalGauge Component
+
+Radial or bar gauge for sensor readings:
+
+- Animated fill based on value
+- Color coding: Green (optimal), Yellow (warning), Red (critical)
+- Sparkline showing 24h/7d trend
+- Threshold markers
+- Animated value counter
+
+#### 2.4: Drill-Down Aggregation
+
+Levels:
+
+| Level | Data Shown | Aggregation |
+|-------|-----------|-------------|
+| Plant | Individual readings | Raw values |
+| Tier | All plants on tier | Avg/Min/Max |
+| Section | All tiers in section | Avg/Min/Max |
+| Room | All sections in room | Avg/Range |
+| Floor | All rooms | Summary counts |
+
+Implementation:
+
+- useAggregatedData(level, entityId) hook
+- Calculates averages, ranges, outliers
+- Returns unified data shape for PlantDataCard
+
+#### 2.5: Mock Data Generator
+
+Rich, realistic demo data including:
+
+- Stage history with dates
+- Vitals: health, temperature, humidity, VPD with history
+- Touch points, photos, notes, alerts
+- Position data
+
+Data Generation:
+
+- Realistic stage duration distributions
+- Seasonal temperature variations
+- Circadian VPD patterns
+- Random but plausible health events
+- IPM schedule adherence
+- Growth curve progression
+
+---
+
+## Part 3: Implementation Phases
+
+### Sprint 1: Icon System & Grid (2-3 days)
+
+1. Create PlantIcon component with stage shapes
+2. Implement GridOverlay with row/column lines
+3. Add section labels with plant counts
+4. Toggle for grid visibility
+5. Test rendering performance
+
+### Sprint 2: Data Card Foundation (2-3 days)
+
+1. Create PlantDataCard shell component
+2. Implement glassmorphic styling
+3. Add header with entity info
+4. Wire up to plant selection
+5. Position card in UI (side panel or overlay)
+
+### Sprint 3: Lifecycle Timeline (1-2 days)
+
+1. Create LifecycleTimeline component
+2. SVG path animations
+3. Stage click handlers
+4. Day calculation logic
+5. Integration with data card
+
+### Sprint 4: Vital Gauges (1-2 days)
+
+1. Create VitalGauge component
+2. Animated value display
+3. Color thresholds
+4. Sparkline mini-charts
+5. Mock sensor data hookup
+
+### Sprint 5: Aggregation & Drill-down (2 days)
+
+1. Implement useAggregatedData hook
+2. Level switcher UI
+3. Data roll-up calculations
+4. Update card content per level
+5. Breadcrumb navigation integration
+
+### Sprint 6: Mock Data & Polish (1-2 days)
+
+1. Build comprehensive mock data generator
+2. Performance optimization
+3. Animation polish
+4. Mobile responsive adjustments
+5. Accessibility review
+
+---
+
+## File Structure
+
+frontend/src/components/facility3d/
+
+- coordinates.ts (existing)
+- PlantSystem.tsx (refactor to use icons)
+- PlantIcon.tsx (NEW - stage icons)
+- GridOverlay.tsx (NEW - grid lines)
+- SectionLabel.tsx (NEW - section info)
+- PlantDataCard/
+ - index.tsx
+ - LifecycleTimeline.tsx
+ - VitalGauge.tsx
+ - ActivityFeed.tsx
+ - ActionButtons.tsx
+- hooks/
+ - useAggregatedData.ts
+ - usePlantLifecycle.ts
+- mocks/
+ - plantDataGenerator.ts
+
+---
+
+## Success Criteria
+
+1. Visual Clarity - Investors understand facility layout in <5 seconds
+2. Stage Recognition - Growth stages identifiable without color dependency
+3. Data Richness - All relevant plant data visible in single card
+4. Performance - 60fps with 500+ plants rendered
+5. Interactivity - Smooth animations, responsive drill-down
+6. Realism - Mock data feels like real cultivation facility
+
+---
+
+## Open Questions
+
+1. Should the data card be a side panel (persistent) or overlay (modal)?
+2. Preferred icon style: geometric shapes, emoji, or custom SVG illustrations?
+3. Priority between "pretty" vs "informative" if tradeoffs arise?
+4. Any specific Metrc fields that MUST be displayed?
diff --git a/frontend/src/components/facility3d/GridOverlay.tsx b/frontend/src/components/facility3d/GridOverlay.tsx
new file mode 100644
index 0000000..b5ccab8
--- /dev/null
+++ b/frontend/src/components/facility3d/GridOverlay.tsx
@@ -0,0 +1,235 @@
+import { useMemo } from 'react';
+import { Line, Text } from '@react-three/drei';
+import * as THREE from 'three';
+
+interface GridOverlayProps {
+ rows: number;
+ columns: number;
+ width: number; // Scaled width
+ height: number; // Scaled height
+ posX: number; // Section position X
+ posY: number; // Section position Y (maps to Z in 3D)
+ tiers: number;
+ currentTier?: number;
+ showLabels?: boolean;
+ opacity?: number;
+}
+
+// Grid colors
+const GRID_COLORS = {
+ rowLine: '#475569', // Slate-600
+ colLine: '#475569', // Slate-600
+ tierLine: '#64748b', // Slate-500
+ labelText: '#94a3b8', // Slate-400
+ highlight: '#06b6d4', // Cyan-500
+};
+
+export function GridOverlay({
+ rows,
+ columns,
+ width,
+ height,
+ posX,
+ posY,
+ tiers,
+ currentTier = 1,
+ showLabels = true,
+ opacity = 0.6,
+}: GridOverlayProps) {
+ // Calculate spacing
+ const colSpacing = width / (columns + 1);
+ const rowSpacing = height / (rows + 1);
+
+ // Generate grid line points
+ const gridLines = useMemo(() => {
+ const lines: Array<{ points: [number, number, number][]; color: string }> = [];
+
+ // Row lines (horizontal, along Z axis in 3D)
+ for (let r = 1; r <= rows; r++) {
+ const z = posY + r * rowSpacing;
+ lines.push({
+ points: [
+ [posX, 0.02, z],
+ [posX + width, 0.02, z],
+ ],
+ color: GRID_COLORS.rowLine,
+ });
+ }
+
+ // Column lines (vertical, along X axis in 3D)
+ for (let c = 1; c <= columns; c++) {
+ const x = posX + c * colSpacing;
+ lines.push({
+ points: [
+ [x, 0.02, posY],
+ [x, 0.02, posY + height],
+ ],
+ color: GRID_COLORS.colLine,
+ });
+ }
+
+ return lines;
+ }, [rows, columns, width, height, posX, posY, colSpacing, rowSpacing]);
+
+ // Generate row/column labels
+ const labels = useMemo(() => {
+ if (!showLabels) return [];
+
+ const result: Array<{ text: string; position: [number, number, number]; type: 'row' | 'col' }> = [];
+
+ // Row labels (R1, R2, R3...)
+ for (let r = 1; r <= rows; r++) {
+ result.push({
+ text: `R${r}`,
+ position: [posX - 0.3, 0.1, posY + r * rowSpacing],
+ type: 'row',
+ });
+ }
+
+ // Column labels (C1, C2, C3...)
+ for (let c = 1; c <= columns; c++) {
+ result.push({
+ text: `C${c}`,
+ position: [posX + c * colSpacing, 0.1, posY + height + 0.25],
+ type: 'col',
+ });
+ }
+
+ return result;
+ }, [rows, columns, posX, posY, rowSpacing, colSpacing, height, showLabels]);
+
+ // Tier marker (vertical line showing tier level)
+ const tierMarkers = useMemo(() => {
+ if (tiers <= 1) return null;
+
+ const markers: Array<{ tier: number; y: number }> = [];
+ for (let t = 1; t <= tiers; t++) {
+ markers.push({
+ tier: t,
+ y: 0.4 + (t * 0.6),
+ });
+ }
+ return markers;
+ }, [tiers]);
+
+ return (
+
+ {/* Grid lines */}
+ {gridLines.map((line, i) => (
+
+ ))}
+
+ {/* Row/Column labels */}
+ {labels.map((label, i) => (
+
+ {label.text}
+
+ ))}
+
+ {/* Tier indicators (side markers) */}
+ {tierMarkers && (
+
+ {tierMarkers.map(marker => (
+
+ {/* Tier line */}
+
+ {/* Tier label */}
+
+ T{marker.tier}
+
+
+ ))}
+
+ )}
+
+ {/* Section boundary outline */}
+
+
+ );
+}
+
+// Simplified grid for room-level view
+interface RoomGridProps {
+ roomWidth: number;
+ roomHeight: number;
+ posX: number;
+ posY: number;
+ sectionCount: number;
+ totalPlants: number;
+}
+
+export function RoomGrid({ roomWidth, roomHeight, posX, posY, sectionCount, totalPlants }: RoomGridProps) {
+ return (
+
+ {/* Room boundary */}
+
+
+ {/* Room stats in corner */}
+
+ {sectionCount} sections • {totalPlants} plants
+
+
+ );
+}
diff --git a/frontend/src/components/facility3d/PlantIcon.tsx b/frontend/src/components/facility3d/PlantIcon.tsx
new file mode 100644
index 0000000..2433d4a
--- /dev/null
+++ b/frontend/src/components/facility3d/PlantIcon.tsx
@@ -0,0 +1,216 @@
+import { useMemo } from 'react';
+import { Billboard } from '@react-three/drei';
+import * as THREE from 'three';
+
+// Stage-specific icon shapes as SVG paths (camera-facing 2D)
+// These are custom illustrations optimized for clarity at small sizes
+
+export type PlantStage = 'CLONE_IN' | 'VEGETATIVE' | 'FLOWERING' | 'DRYING' | 'CURING' | 'HARVESTED' | 'FINISHED';
+
+interface PlantIconProps {
+ stage: PlantStage;
+ color: string;
+ position: [number, number, number];
+ scale?: number;
+ isHighlighted?: boolean;
+ isDimmed?: boolean;
+ onClick?: () => void;
+}
+
+// Icon configurations per stage
+const STAGE_ICONS: Record = {
+ CLONE_IN: { shape: 'circle' }, // Small seedling dot
+ VEGETATIVE: { shape: 'leaf' }, // 3-point leaf shape
+ FLOWERING: { shape: 'flower', sides: 5 }, // 5-petal flower
+ DRYING: { shape: 'diamond' }, // Hanging diamond
+ CURING: { shape: 'square' }, // Jar/container square
+ HARVESTED: { shape: 'ring' }, // Empty circle
+ FINISHED: { shape: 'ring' }, // Empty circle
+};
+
+// Create geometry for each shape type
+function createShapeGeometry(shape: string, sides?: number): THREE.BufferGeometry {
+ switch (shape) {
+ case 'circle':
+ return new THREE.CircleGeometry(0.15, 16);
+
+ case 'leaf': {
+ // Custom leaf shape - pointed oval
+ const leafShape = new THREE.Shape();
+ leafShape.moveTo(0, 0.2);
+ leafShape.bezierCurveTo(0.12, 0.15, 0.15, 0.05, 0.1, -0.15);
+ leafShape.lineTo(0, -0.2);
+ leafShape.lineTo(-0.1, -0.15);
+ leafShape.bezierCurveTo(-0.15, 0.05, -0.12, 0.15, 0, 0.2);
+ return new THREE.ShapeGeometry(leafShape);
+ }
+
+ case 'flower': {
+ // 5-petal flower shape
+ const petalCount = sides || 5;
+ const flowerShape = new THREE.Shape();
+ const innerRadius = 0.06;
+ const outerRadius = 0.18;
+
+ for (let i = 0; i < petalCount; i++) {
+ const angle = (i / petalCount) * Math.PI * 2 - Math.PI / 2;
+ const nextAngle = ((i + 1) / petalCount) * Math.PI * 2 - Math.PI / 2;
+ const midAngle = (angle + nextAngle) / 2;
+
+ const x1 = Math.cos(angle) * innerRadius;
+ const y1 = Math.sin(angle) * innerRadius;
+ const xMid = Math.cos(midAngle) * outerRadius;
+ const yMid = Math.sin(midAngle) * outerRadius;
+
+ if (i === 0) {
+ flowerShape.moveTo(x1, y1);
+ }
+ flowerShape.quadraticCurveTo(xMid, yMid, Math.cos(nextAngle) * innerRadius, Math.sin(nextAngle) * innerRadius);
+ }
+ flowerShape.closePath();
+ return new THREE.ShapeGeometry(flowerShape);
+ }
+
+ case 'diamond': {
+ // Diamond/rhombus shape
+ const diamondShape = new THREE.Shape();
+ diamondShape.moveTo(0, 0.2);
+ diamondShape.lineTo(0.12, 0);
+ diamondShape.lineTo(0, -0.2);
+ diamondShape.lineTo(-0.12, 0);
+ diamondShape.closePath();
+ return new THREE.ShapeGeometry(diamondShape);
+ }
+
+ case 'square':
+ return new THREE.PlaneGeometry(0.25, 0.25);
+
+ case 'ring': {
+ // Ring/outline circle
+ const ringShape = new THREE.Shape();
+ ringShape.absarc(0, 0, 0.15, 0, Math.PI * 2, false);
+ const hole = new THREE.Path();
+ hole.absarc(0, 0, 0.1, 0, Math.PI * 2, true);
+ ringShape.holes.push(hole);
+ return new THREE.ShapeGeometry(ringShape);
+ }
+
+ default:
+ return new THREE.CircleGeometry(0.15, 16);
+ }
+}
+
+// Cache geometries to avoid recreation
+const geometryCache = new Map();
+
+function getGeometry(shape: string, sides?: number): THREE.BufferGeometry {
+ const key = `${shape}-${sides || 0}`;
+ if (!geometryCache.has(key)) {
+ geometryCache.set(key, createShapeGeometry(shape, sides));
+ }
+ return geometryCache.get(key)!;
+}
+
+export function PlantIcon({
+ stage,
+ color,
+ position,
+ scale = 1,
+ isHighlighted = false,
+ isDimmed = false,
+ onClick
+}: PlantIconProps) {
+ const iconConfig = STAGE_ICONS[stage] || STAGE_ICONS.VEGETATIVE;
+ const geometry = useMemo(() => getGeometry(iconConfig.shape, iconConfig.sides), [iconConfig]);
+
+ // Adjust color for dimming/highlighting
+ const finalColor = useMemo(() => {
+ if (isDimmed) return '#3f3f46'; // Zinc-700
+ return color;
+ }, [color, isDimmed]);
+
+ // Scale adjustments
+ const finalScale = isHighlighted ? scale * 1.5 : scale;
+
+ return (
+
+ {
+ e.stopPropagation();
+ onClick?.();
+ }}
+ >
+
+
+
+ {/* Highlight ring for selected plants */}
+ {isHighlighted && (
+
+
+
+
+ )}
+
+ );
+}
+
+// Batch renderer for performance with many plants
+interface PlantIconBatchProps {
+ plants: Array<{
+ id: string;
+ stage: PlantStage;
+ color: string;
+ position: [number, number, number];
+ isHighlighted?: boolean;
+ isDimmed?: boolean;
+ }>;
+ onPlantClick: (id: string) => void;
+}
+
+export function PlantIconBatch({ plants, onPlantClick }: PlantIconBatchProps) {
+ // Group by stage for instanced rendering
+ const groupedByStage = useMemo(() => {
+ const groups: Record = {
+ CLONE_IN: [],
+ VEGETATIVE: [],
+ FLOWERING: [],
+ DRYING: [],
+ CURING: [],
+ HARVESTED: [],
+ FINISHED: [],
+ };
+
+ for (const plant of plants) {
+ const stage = plant.stage || 'VEGETATIVE';
+ if (groups[stage]) {
+ groups[stage].push(plant);
+ }
+ }
+
+ return groups;
+ }, [plants]);
+
+ // For now, render individually (can optimize to instanced later if needed)
+ return (
+
+ {plants.map(plant => (
+ onPlantClick(plant.id)}
+ />
+ ))}
+
+ );
+}
diff --git a/frontend/src/components/facility3d/PlantSystem.tsx b/frontend/src/components/facility3d/PlantSystem.tsx
index 46c92b2..b059ced 100644
--- a/frontend/src/components/facility3d/PlantSystem.tsx
+++ b/frontend/src/components/facility3d/PlantSystem.tsx
@@ -2,6 +2,7 @@ import { useMemo } from 'react';
import { Instances, Instance } from '@react-three/drei';
import * as THREE from 'three';
import { PlantPosition, VisMode, COLORS } from './types';
+import { PlantIcon, PlantStage } from './PlantIcon';
// Mock helpers
const getMockPlantHealth = (_plantId: string) => {
@@ -17,12 +18,27 @@ const lerpColor = (c1: string, c2: string, t: number) => {
return '#' + c1c.lerp(c2c, Math.min(1, Math.max(0, t))).getHexString();
};
+// Map stage strings to PlantStage type
+const normalizeStage = (stage?: string): PlantStage => {
+ if (!stage) return 'VEGETATIVE';
+ const s = stage.toUpperCase();
+ if (s === 'CLONE_IN') return 'CLONE_IN';
+ if (s === 'VEGETATIVE') return 'VEGETATIVE';
+ if (s === 'FLOWERING') return 'FLOWERING';
+ if (s === 'DRYING') return 'DRYING';
+ if (s === 'CURING') return 'CURING';
+ if (s === 'HARVESTED') return 'HARVESTED';
+ if (s === 'FINISHED') return 'FINISHED';
+ return 'VEGETATIVE';
+};
+
interface PlantSystemProps {
positions: PlantPosition[];
visMode: VisMode;
onPlantClick: (plant: PlantPosition) => void;
highlightedTags?: string[]; // Tags to highlight
dimMode?: boolean; // Whether to dim non-highlighted plants
+ useIcons?: boolean; // Use 2D icons instead of 3D spheres
}
export function PlantSystem({
@@ -30,7 +46,8 @@ export function PlantSystem({
visMode,
onPlantClick,
highlightedTags = [],
- dimMode = false
+ dimMode = false,
+ useIcons = true // Default to new icon system
}: PlantSystemProps) {
if (!positions || positions.length === 0) return null;
@@ -46,17 +63,17 @@ export function PlantSystem({
return plants.map(pos => {
const isHighlighted = pos.plant && highlightSet.has(pos.plant.tagNumber);
const shouldDim = dimMode && hasHighlights && !isHighlighted;
+ const stage = normalizeStage(pos.plant?.stage ?? undefined);
let color: string;
switch (visMode) {
case 'STANDARD':
// Match actual BatchStage enum values from schema
- const stage = pos.plant?.stage;
color = (stage === 'FLOWERING') ? COLORS.FLOWER :
(stage === 'DRYING') ? COLORS.DRY :
(stage === 'CURING') ? COLORS.CURE :
- (stage === 'CLONE_IN' || stage === 'VEGETATIVE') ? COLORS.VEG :
- COLORS.VEG; // Default fallback
+ (stage === 'CLONE_IN') ? '#3b82f6' : // Blue for clones
+ COLORS.VEG;
break;
case 'HEALTH':
const status = getMockPlantHealth(pos.plant!.id);
@@ -70,18 +87,46 @@ export function PlantSystem({
color = '#555';
}
- // Apply dim effect
- if (shouldDim) {
- color = '#3f3f46'; // Zinc-700 - muted gray
- }
-
- // Apply highlight boost
- const scale = isHighlighted ? 2.0 : (shouldDim ? 1.0 : 1.5);
-
- return { pos, color, scale, isHighlighted };
+ return { pos, color, isHighlighted, shouldDim, stage };
});
}, [plants, visMode, highlightSet, dimMode, hasHighlights]);
+ // New icon-based rendering
+ if (useIcons) {
+ return (
+
+ {plantData.map(({ pos, color, isHighlighted, shouldDim, stage }, i) => (
+ onPlantClick(pos)}
+ />
+ ))}
+
+ {/* Empty slot markers - small dots */}
+ {emptySlots.length > 0 && visMode === 'STANDARD' && !dimMode && (
+
+
+
+ {emptySlots.map((pos, i) => (
+
+ ))}
+
+ )}
+
+ );
+ }
+
+ // Legacy sphere-based rendering (fallback)
return (
{plants.length > 0 && (
@@ -97,12 +142,12 @@ export function PlantSystem({
>
- {plantData.map(({ pos, color, scale }, i) => (
+ {plantData.map(({ pos, color, isHighlighted, shouldDim }, i) => (
))}
@@ -124,3 +169,4 @@ export function PlantSystem({
);
}
+
diff --git a/frontend/src/components/facility3d/SmartRack.tsx b/frontend/src/components/facility3d/SmartRack.tsx
index 844c2e8..81ec284 100644
--- a/frontend/src/components/facility3d/SmartRack.tsx
+++ b/frontend/src/components/facility3d/SmartRack.tsx
@@ -5,6 +5,7 @@ import type { Section3D, Position3D } from '../../lib/layoutApi';
import { PlantPosition, VisMode } from './types';
import { PlantSystem } from './PlantSystem';
import { SCALE } from './coordinates';
+import { GridOverlay } from './GridOverlay';
// Hierarchy context passed down from FacilityScene
export interface HierarchyContext {
@@ -21,9 +22,10 @@ interface SmartRackProps {
highlightedTags?: string[];
dimMode?: boolean;
hierarchy?: HierarchyContext;
+ showGrid?: boolean; // Toggle grid visibility
}
-export function SmartRack({ section, visMode, onPlantClick, highlightedTags, dimMode, hierarchy }: SmartRackProps) {
+export function SmartRack({ section, visMode, onPlantClick, highlightedTags, dimMode, hierarchy, showGrid = true }: SmartRackProps) {
// Section positions are absolute (after our fix), scale them
const scaledSection = {
posX: section.posX * SCALE,
@@ -146,6 +148,21 @@ export function SmartRack({ section, visMode, onPlantClick, highlightedTags, dim
))}
+ {/* Grid overlay with row/column labels */}
+ {showGrid && visMode === 'STANDARD' && (
+
+ )}
+