fix: beacon coordinates now match plant positions exactly
- 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:
parent
e20c618b45
commit
5f774bb873
3 changed files with 150 additions and 34 deletions
|
|
@ -4,9 +4,7 @@ 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
|
||||
const SCALE = 0.1;
|
||||
import { SCALE } from './coordinates';
|
||||
|
||||
// Hierarchy context passed down from FacilityScene
|
||||
export interface HierarchyContext {
|
||||
|
|
@ -34,18 +32,17 @@ export function SmartRack({ section, visMode, onPlantClick, highlightedTags, dim
|
|||
height: section.height * SCALE,
|
||||
};
|
||||
|
||||
// Calculate how plants should be positioned within the section
|
||||
const positions: PlantPosition[] = useMemo(() => {
|
||||
// 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);
|
||||
|
||||
// Calculate spacing to fit plants within section bounds
|
||||
const colSpacing = scaledSection.width / (maxCols + 1);
|
||||
const rowSpacing = scaledSection.height / (maxRows + 1);
|
||||
|
||||
// Calculate plant positions using consistent grid logic
|
||||
const positions: PlantPosition[] = useMemo(() => {
|
||||
return section.positions.map((pos: Position3D) => ({
|
||||
...pos,
|
||||
// Position plants WITHIN the section
|
||||
// Grid-based positioning within section (RAW coords, no floor centering here)
|
||||
x: scaledSection.posX + (pos.column * colSpacing),
|
||||
z: scaledSection.posY + (pos.row * rowSpacing),
|
||||
y: 0.4 + (pos.tier * 0.6),
|
||||
|
|
@ -59,18 +56,12 @@ export function SmartRack({ section, visMode, onPlantClick, highlightedTags, dim
|
|||
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 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);
|
||||
|
||||
// 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 (
|
||||
<group>
|
||||
{/* Section/Table Label */}
|
||||
|
|
|
|||
71
frontend/src/components/facility3d/coordinates.ts
Normal file
71
frontend/src/components/facility3d/coordinates.ts
Normal 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;
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { useEffect, useState, Suspense, Component, ReactNode, useCallback } from 'react';
|
||||
import { Canvas } from '@react-three/fiber';
|
||||
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 { Link, useSearchParams } from 'react-router-dom';
|
||||
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 { CameraPreset, CameraPresetSelector } from '../components/facility3d/CameraPresets';
|
||||
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 ---
|
||||
interface ErrorBoundaryState { hasError: boolean; error: Error | null; }
|
||||
|
|
@ -82,10 +125,12 @@ export default function Facility3DViewerPage() {
|
|||
for (const section of room.sections) {
|
||||
const match = section.positions.find((p: Position3D) => p.plant?.tagNumber === targetedPlantTag);
|
||||
if (match) {
|
||||
const SCALE = 0.1;
|
||||
const x = (room.posX + section.posX + (match.column * 0.5)) * SCALE - (floorData.floor.width * SCALE / 2);
|
||||
const z = (room.posY + section.posY + (match.row * 0.5)) * SCALE - (floorData.floor.height * SCALE / 2);
|
||||
const y = 0.4 + (match.tier * 0.6);
|
||||
const { x, y, z } = calculatePlantCoords(
|
||||
section,
|
||||
{ row: match.row, column: match.column, tier: match.tier },
|
||||
floorData.floor.width,
|
||||
floorData.floor.height
|
||||
);
|
||||
|
||||
const plant: PlantPosition = { ...match, x, y, z };
|
||||
setSelectedPlant(plant);
|
||||
|
|
@ -188,10 +233,17 @@ export default function Facility3DViewerPage() {
|
|||
const handleSearchSelect = useCallback((result: any) => {
|
||||
if (!floorData) return;
|
||||
|
||||
const SCALE = 0.1;
|
||||
const x = (result.position.roomX + result.position.sectionX + (result.position.column * 0.5)) * SCALE - (floorData.floor.width * SCALE / 2);
|
||||
const z = (result.position.roomY + result.position.sectionY + (result.position.row * 0.5)) * SCALE - (floorData.floor.height * SCALE / 2);
|
||||
const y = 0.4 + (result.position.tier * 0.6);
|
||||
// Find the actual section to use grid-based position calculation
|
||||
const room = floorData.rooms.find(r => r.name === result.roomName);
|
||||
const section = room?.sections.find(s => (s.code || s.name) === result.sectionCode);
|
||||
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 });
|
||||
setCameraPreset('ISOMETRIC');
|
||||
|
|
@ -342,14 +394,16 @@ export default function Facility3DViewerPage() {
|
|||
<PlantLibrary
|
||||
floorData={floorData}
|
||||
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 section = room?.sections.find(s => (s.code || s.name) === sectionCode);
|
||||
if (room && section) {
|
||||
const SCALE = 0.1;
|
||||
const x = (room.posX + section.posX + (plant.column * 0.5)) * SCALE - (floorData.floor.width * SCALE / 2);
|
||||
const z = (room.posY + section.posY + (plant.row * 0.5)) * SCALE - (floorData.floor.height * SCALE / 2);
|
||||
const y = 0.4 + (plant.tier * 0.6);
|
||||
if (section) {
|
||||
const { x, y, z } = calculatePlantCoords(
|
||||
section,
|
||||
{ row: plant.row, column: plant.column, tier: plant.tier },
|
||||
floorData.floor.width,
|
||||
floorData.floor.height
|
||||
);
|
||||
|
||||
setFocusTarget({ x, z });
|
||||
setCameraPreset('ISOMETRIC');
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue