fix: beacon coordinates now match plant positions exactly
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

- Create coordinates.ts with centralized SCALE constant
- Add calculatePlantCoords() helper matching SmartRack grid logic
- Refactor all beacon position calcs to use helper
- Uses col*colSpacing where colSpacing = width/(maxCols+1)
- This matches how plants are actually rendered in PlantSystem
This commit is contained in:
fullsizemalt 2025-12-19 11:52:24 -08:00
parent e20c618b45
commit 5f774bb873
3 changed files with 150 additions and 34 deletions

View file

@ -4,9 +4,7 @@ import * as THREE from 'three';
import type { Section3D, Position3D } from '../../lib/layoutApi'; import type { Section3D, Position3D } from '../../lib/layoutApi';
import { PlantPosition, VisMode } from './types'; import { PlantPosition, VisMode } from './types';
import { PlantSystem } from './PlantSystem'; import { PlantSystem } from './PlantSystem';
import { SCALE } from './coordinates';
// Convert pixel coordinates to world units
const SCALE = 0.1;
// Hierarchy context passed down from FacilityScene // Hierarchy context passed down from FacilityScene
export interface HierarchyContext { export interface HierarchyContext {
@ -34,18 +32,17 @@ export function SmartRack({ section, visMode, onPlantClick, highlightedTags, dim
height: section.height * SCALE, height: section.height * SCALE,
}; };
// Calculate how plants should be positioned within the section // Calculate grid dimensions and spacing
const maxCols = Math.max(...section.positions.map(p => p.column), 1);
const maxRows = Math.max(...section.positions.map(p => p.row), 1);
const colSpacing = scaledSection.width / (maxCols + 1);
const rowSpacing = scaledSection.height / (maxRows + 1);
// Calculate plant positions using consistent grid logic
const positions: PlantPosition[] = useMemo(() => { const positions: PlantPosition[] = useMemo(() => {
const maxCols = Math.max(...section.positions.map(p => p.column), 1);
const maxRows = Math.max(...section.positions.map(p => p.row), 1);
// Calculate spacing to fit plants within section bounds
const colSpacing = scaledSection.width / (maxCols + 1);
const rowSpacing = scaledSection.height / (maxRows + 1);
return section.positions.map((pos: Position3D) => ({ return section.positions.map((pos: Position3D) => ({
...pos, ...pos,
// Position plants WITHIN the section // Grid-based positioning within section (RAW coords, no floor centering here)
x: scaledSection.posX + (pos.column * colSpacing), x: scaledSection.posX + (pos.column * colSpacing),
z: scaledSection.posY + (pos.row * rowSpacing), z: scaledSection.posY + (pos.row * rowSpacing),
y: 0.4 + (pos.tier * 0.6), y: 0.4 + (pos.tier * 0.6),
@ -59,18 +56,12 @@ export function SmartRack({ section, visMode, onPlantClick, highlightedTags, dim
tier: pos.tier, tier: pos.tier,
}, },
})); }));
}, [section, scaledSection, hierarchy]); }, [section, scaledSection.posX, scaledSection.posY, colSpacing, rowSpacing, hierarchy]);
const distinctTiers = [...new Set(positions.map(p => p.tier))].sort((a, b) => a - b); 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 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 distinctCols = [...new Set(positions.map(p => p.column))].sort((a, b) => a - b);
// Use calculated spacing for labels
const maxCols = Math.max(...section.positions.map(p => p.column), 1);
const maxRows = Math.max(...section.positions.map(p => p.row), 1);
const colSpacing = scaledSection.width / (maxCols + 1);
const rowSpacing = scaledSection.height / (maxRows + 1);
return ( return (
<group> <group>
{/* Section/Table Label */} {/* Section/Table Label */}

View file

@ -0,0 +1,71 @@
import type { Section3D, Position3D } from '../../lib/layoutApi';
// Convert pixel coordinates to world units
export const SCALE = 0.1;
/**
* Calculate the world coordinates (x, y, z) for a plant position within a section.
* This is the SINGLE SOURCE OF TRUTH for plant positioning in 3D space.
*
* @param section - The section containing the position
* @param position - The position within the section (row, column, tier)
* @param floorCenterX - Half of floor width in world units for centering
* @param floorCenterZ - Half of floor height in world units for centering
*/
export function calculatePlantWorldPosition(
section: Section3D,
position: { row: number; column: number; tier: number },
floorCenterX: number = 0,
floorCenterZ: number = 0
): { x: number; y: number; z: number } {
// Scale section to world units
const scaledSection = {
posX: section.posX * SCALE,
posY: section.posY * SCALE,
width: section.width * SCALE,
height: section.height * SCALE,
};
// Calculate grid dimensions from section positions
const maxCols = Math.max(...section.positions.map(p => p.column), 1);
const maxRows = Math.max(...section.positions.map(p => p.row), 1);
// Calculate spacing to fit plants within section bounds
const colSpacing = scaledSection.width / (maxCols + 1);
const rowSpacing = scaledSection.height / (maxRows + 1);
// Calculate raw position (before floor centering)
const rawX = scaledSection.posX + (position.column * colSpacing);
const rawZ = scaledSection.posY + (position.row * rowSpacing);
const y = 0.4 + (position.tier * 0.6);
// Apply floor centering offset (scene is centered around 0,0)
const x = rawX - floorCenterX;
const z = rawZ - floorCenterZ;
return { x, y, z };
}
/**
* Calculate floor center offsets from floor dimensions
*/
export function calculateFloorCenter(floorWidth: number, floorHeight: number): { centerX: number; centerZ: number } {
return {
centerX: (floorWidth * SCALE) / 2,
centerZ: (floorHeight * SCALE) / 2,
};
}
/**
* Find a section within floor data by searching all rooms
*/
export function findSectionInFloorData(
floorData: { rooms: Array<{ sections: Section3D[] }> },
sectionId: string
): Section3D | undefined {
for (const room of floorData.rooms) {
const section = room.sections.find(s => s.id === sectionId);
if (section) return section;
}
return undefined;
}

View file

@ -1,7 +1,7 @@
import { useEffect, useState, Suspense, Component, ReactNode, useCallback } from 'react'; import { useEffect, useState, Suspense, Component, ReactNode, useCallback } from 'react';
import { Canvas } from '@react-three/fiber'; import { Canvas } from '@react-three/fiber';
import { Html, CameraControls } from '@react-three/drei'; import { Html, CameraControls } from '@react-three/drei';
import { layoutApi, Floor3DData, Room3D, Position3D } from '../lib/layoutApi'; import { layoutApi, Floor3DData, Room3D, Position3D, Section3D } from '../lib/layoutApi';
import { Loader2, ArrowLeft, Maximize, MousePointer2, Layers, Thermometer, Droplets, Activity, Leaf, Eye, EyeOff, Clock } from 'lucide-react'; import { Loader2, ArrowLeft, Maximize, MousePointer2, Layers, Thermometer, Droplets, Activity, Leaf, Eye, EyeOff, Clock } from 'lucide-react';
import { Link, useSearchParams } from 'react-router-dom'; import { Link, useSearchParams } from 'react-router-dom';
import { FacilityScene } from '../components/facility3d/FacilityScene'; import { FacilityScene } from '../components/facility3d/FacilityScene';
@ -12,6 +12,49 @@ import { TimelineSlider } from '../components/facility3d/TimelineSlider';
import { PlantPosition, VisMode, COLORS } from '../components/facility3d/types'; import { PlantPosition, VisMode, COLORS } from '../components/facility3d/types';
import { CameraPreset, CameraPresetSelector } from '../components/facility3d/CameraPresets'; import { CameraPreset, CameraPresetSelector } from '../components/facility3d/CameraPresets';
import { HierarchyBreadcrumb } from '../components/facility3d/HierarchyBreadcrumb'; import { HierarchyBreadcrumb } from '../components/facility3d/HierarchyBreadcrumb';
import { SCALE } from '../components/facility3d/coordinates';
/**
* Calculate plant world coordinates matching SmartRack's grid logic exactly.
* This ensures beacon position matches where plants are actually rendered.
*/
function calculatePlantCoords(
section: Section3D,
position: { row: number; column: number; tier: number },
floorWidth: number,
floorHeight: number
): { x: number; y: number; z: number } {
// Scale section to world units (same as SmartRack)
const scaledSection = {
posX: section.posX * SCALE,
posY: section.posY * SCALE,
width: section.width * SCALE,
height: section.height * SCALE,
};
// Calculate grid dimensions (same as SmartRack)
const maxCols = Math.max(...section.positions.map(p => p.column), 1);
const maxRows = Math.max(...section.positions.map(p => p.row), 1);
// Calculate spacing (same as SmartRack)
const colSpacing = scaledSection.width / (maxCols + 1);
const rowSpacing = scaledSection.height / (maxRows + 1);
// Raw position within section (same as SmartRack)
const rawX = scaledSection.posX + (position.column * colSpacing);
const rawZ = scaledSection.posY + (position.row * rowSpacing);
const y = 0.4 + (position.tier * 0.6);
// Apply floor centering (matches FacilityScene offset group)
const floorCenterX = (floorWidth * SCALE) / 2;
const floorCenterZ = (floorHeight * SCALE) / 2;
return {
x: rawX - floorCenterX,
y,
z: rawZ - floorCenterZ,
};
}
// --- Error Boundary --- // --- Error Boundary ---
interface ErrorBoundaryState { hasError: boolean; error: Error | null; } interface ErrorBoundaryState { hasError: boolean; error: Error | null; }
@ -82,10 +125,12 @@ export default function Facility3DViewerPage() {
for (const section of room.sections) { for (const section of room.sections) {
const match = section.positions.find((p: Position3D) => p.plant?.tagNumber === targetedPlantTag); const match = section.positions.find((p: Position3D) => p.plant?.tagNumber === targetedPlantTag);
if (match) { if (match) {
const SCALE = 0.1; const { x, y, z } = calculatePlantCoords(
const x = (room.posX + section.posX + (match.column * 0.5)) * SCALE - (floorData.floor.width * SCALE / 2); section,
const z = (room.posY + section.posY + (match.row * 0.5)) * SCALE - (floorData.floor.height * SCALE / 2); { row: match.row, column: match.column, tier: match.tier },
const y = 0.4 + (match.tier * 0.6); floorData.floor.width,
floorData.floor.height
);
const plant: PlantPosition = { ...match, x, y, z }; const plant: PlantPosition = { ...match, x, y, z };
setSelectedPlant(plant); setSelectedPlant(plant);
@ -188,10 +233,17 @@ export default function Facility3DViewerPage() {
const handleSearchSelect = useCallback((result: any) => { const handleSearchSelect = useCallback((result: any) => {
if (!floorData) return; if (!floorData) return;
const SCALE = 0.1; // Find the actual section to use grid-based position calculation
const x = (result.position.roomX + result.position.sectionX + (result.position.column * 0.5)) * SCALE - (floorData.floor.width * SCALE / 2); const room = floorData.rooms.find(r => r.name === result.roomName);
const z = (result.position.roomY + result.position.sectionY + (result.position.row * 0.5)) * SCALE - (floorData.floor.height * SCALE / 2); const section = room?.sections.find(s => (s.code || s.name) === result.sectionCode);
const y = 0.4 + (result.position.tier * 0.6); if (!section) return;
const { x, y, z } = calculatePlantCoords(
section,
{ row: result.position.row, column: result.position.column, tier: result.position.tier },
floorData.floor.width,
floorData.floor.height
);
setFocusTarget({ x, z }); setFocusTarget({ x, z });
setCameraPreset('ISOMETRIC'); setCameraPreset('ISOMETRIC');
@ -342,14 +394,16 @@ export default function Facility3DViewerPage() {
<PlantLibrary <PlantLibrary
floorData={floorData} floorData={floorData}
onPlantSelect={(plant, roomName, sectionCode) => { onPlantSelect={(plant, roomName, sectionCode) => {
// Calculate position and select plant // Find section and calculate position using same logic as SmartRack
const room = floorData.rooms.find(r => r.name === roomName); const room = floorData.rooms.find(r => r.name === roomName);
const section = room?.sections.find(s => (s.code || s.name) === sectionCode); const section = room?.sections.find(s => (s.code || s.name) === sectionCode);
if (room && section) { if (section) {
const SCALE = 0.1; const { x, y, z } = calculatePlantCoords(
const x = (room.posX + section.posX + (plant.column * 0.5)) * SCALE - (floorData.floor.width * SCALE / 2); section,
const z = (room.posY + section.posY + (plant.row * 0.5)) * SCALE - (floorData.floor.height * SCALE / 2); { row: plant.row, column: plant.column, tier: plant.tier },
const y = 0.4 + (plant.tier * 0.6); floorData.floor.width,
floorData.floor.height
);
setFocusTarget({ x, z }); setFocusTarget({ x, z });
setCameraPreset('ISOMETRIC'); setCameraPreset('ISOMETRIC');