fix: calculate room bounds from section positions for proper stacking
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

- RoomObject now calculates actual bounds from section positions
- FacilityScene centers content based on actual section bounds
- SmartRack positions plants proportionally within sections
- Fixes plants rendering outside room boxes
This commit is contained in:
fullsizemalt 2025-12-18 19:57:25 -08:00
parent 9194335dd7
commit ddaf67ab1e
3 changed files with 95 additions and 48 deletions

View file

@ -1,4 +1,4 @@
import { useEffect } from 'react';
import { useMemo } from 'react';
import { Environment, ContactShadows } from '@react-three/drei';
import { CameraControls } from '@react-three/drei';
import type { Floor3DData, Room3D } from '../../lib/layoutApi';
@ -7,7 +7,7 @@ import { Beacon } from './Beacon';
import { PlantPosition, VisMode } from './types';
import { CameraPresets, CameraPreset } from './CameraPresets';
// Convert pixel coordinates to world units (same as RoomObject/SmartRack)
// Convert pixel coordinates to world units
const SCALE = 0.1;
interface FacilitySceneProps {
@ -33,11 +33,35 @@ export function FacilityScene({
dimMode = false,
beaconPosition = null,
}: FacilitySceneProps) {
// Scale floor dimensions for centering and camera
const scaledFloor = {
width: data.floor.width * SCALE,
height: data.floor.height * SCALE,
};
// Calculate actual floor bounds from all sections
const floorBounds = useMemo(() => {
let minX = Infinity, minZ = Infinity, maxX = -Infinity, maxZ = -Infinity;
for (const room of data.rooms) {
for (const section of room.sections) {
minX = Math.min(minX, section.posX * SCALE);
minZ = Math.min(minZ, section.posY * SCALE);
maxX = Math.max(maxX, (section.posX + section.width) * SCALE);
maxZ = Math.max(maxZ, (section.posY + section.height) * SCALE);
}
}
// Fallback to floor dimensions if no sections
if (minX === Infinity) {
minX = 0;
minZ = 0;
maxX = data.floor.width * SCALE;
maxZ = data.floor.height * SCALE;
}
return {
minX, minZ, maxX, maxZ,
width: maxX - minX,
height: maxZ - minZ,
centerX: (minX + maxX) / 2,
centerZ: (minZ + maxZ) / 2,
};
}, [data]);
return (
<>
@ -50,7 +74,8 @@ export function FacilityScene({
shadow-mapSize={[2048, 2048]}
/>
<group position={[-scaledFloor.width / 2, 0, -scaledFloor.height / 2]}>
{/* Offset to center the content */}
<group position={[-floorBounds.centerX, 0, -floorBounds.centerZ]}>
{data.rooms.map((room: Room3D) => (
<RoomObject
key={room.id}
@ -69,7 +94,7 @@ export function FacilityScene({
<ContactShadows
position={[0, -0.02, 0]}
opacity={0.35}
scale={120}
scale={floorBounds.width * 1.5}
blur={2}
far={12}
resolution={512}
@ -79,8 +104,8 @@ export function FacilityScene({
{/* Preset-based camera system */}
<CameraPresets
preset={cameraPreset}
floorWidth={scaledFloor.width}
floorHeight={scaledFloor.height}
floorWidth={floorBounds.width}
floorHeight={floorBounds.height}
focusTarget={focusTarget}
onReady={onControlsReady}
/>

View file

@ -4,7 +4,7 @@ import type { Room3D } from '../../lib/layoutApi';
import { PlantPosition, VisMode, COLORS } from './types';
import { SmartRack } from './SmartRack';
// Convert pixel coordinates to world units (same as SmartRack)
// Convert pixel coordinates to world units
const SCALE = 0.1;
// Mock environment data
@ -54,14 +54,32 @@ export function RoomObject({ room, visMode, onPlantClick, highlightedTags, dimMo
: lerpColor(COLORS.HUMIDITY_OPTIMAL, COLORS.HUMIDITY_WET, (t - 0.5) * 2);
}
// Calculate the bounding box of all sections to properly size the room
const sectionBounds = room.sections.reduce((acc, sec) => ({
minX: Math.min(acc.minX, sec.posX * SCALE),
minZ: Math.min(acc.minZ, sec.posY * SCALE),
maxX: Math.max(acc.maxX, (sec.posX + sec.width) * SCALE),
maxZ: Math.max(acc.maxZ, (sec.posY + sec.height) * SCALE),
}), { minX: Infinity, minZ: Infinity, maxX: -Infinity, maxZ: -Infinity });
// If we have sections, use their bounds for the room visualization
const hasSections = room.sections.length > 0 && sectionBounds.minX !== Infinity;
const actualRoom = hasSections ? {
posX: sectionBounds.minX,
posY: sectionBounds.minZ,
width: sectionBounds.maxX - sectionBounds.minX,
height: sectionBounds.maxZ - sectionBounds.minZ,
} : scaledRoom;
return (
<group position={[scaledRoom.posX, 0, scaledRoom.posY]}>
<group>
{/* Room label - positioned above the actual content area */}
<Text
position={[scaledRoom.width / 2, 0.05, scaledRoom.height / 2]}
position={[actualRoom.posX + actualRoom.width / 2, 0.05, actualRoom.posY + actualRoom.height / 2]}
rotation={[-Math.PI / 2, 0, 0]}
fontSize={Math.min(scaledRoom.width, scaledRoom.height) / 6}
fontSize={Math.min(actualRoom.width, actualRoom.height) / 8}
color="#f8fafc"
fillOpacity={0.7}
fillOpacity={0.5}
anchorX="center"
anchorY="middle"
>
@ -70,9 +88,9 @@ export function RoomObject({ room, visMode, onPlantClick, highlightedTags, dimMo
{(visMode === 'TEMP' || visMode === 'HUMIDITY') && (
<Text
position={[scaledRoom.width / 2, 4, scaledRoom.height / 2]}
position={[actualRoom.posX + actualRoom.width / 2, 4, actualRoom.posY + actualRoom.height / 2]}
rotation={[-Math.PI / 4, 0, 0]}
fontSize={Math.min(4, scaledRoom.width / 4)}
fontSize={Math.min(4, actualRoom.width / 4)}
color="#fff"
outlineColor="#000"
outlineWidth={0.15}
@ -82,22 +100,25 @@ export function RoomObject({ room, visMode, onPlantClick, highlightedTags, dimMo
</Text>
)}
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[scaledRoom.width / 2, 0, scaledRoom.height / 2]} receiveShadow>
<planeGeometry args={[scaledRoom.width, scaledRoom.height]} />
{/* Room floor - use calculated bounds */}
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[actualRoom.posX + actualRoom.width / 2, -0.01, actualRoom.posY + actualRoom.height / 2]} receiveShadow>
<planeGeometry args={[actualRoom.width + 1, actualRoom.height + 1]} />
<meshStandardMaterial
color={floorColor}
roughness={0.95}
metalness={0.05}
transparent={visMode !== 'STANDARD'}
opacity={visMode !== 'STANDARD' ? 0.85 : 1}
transparent={true}
opacity={0.6}
/>
</mesh>
<lineSegments position={[scaledRoom.width / 2, 0.02, scaledRoom.height / 2]} rotation={[-Math.PI / 2, 0, 0]}>
<edgesGeometry args={[new THREE.PlaneGeometry(scaledRoom.width, scaledRoom.height)]} />
{/* Room boundary */}
<lineSegments position={[actualRoom.posX + actualRoom.width / 2, 0.02, actualRoom.posY + actualRoom.height / 2]} rotation={[-Math.PI / 2, 0, 0]}>
<edgesGeometry args={[new THREE.PlaneGeometry(actualRoom.width + 1, actualRoom.height + 1)]} />
<lineBasicMaterial color="#64748b" />
</lineSegments>
{/* Sections are now positioned using their own absolute coordinates */}
{room.sections.map(section => (
<SmartRack
key={section.id}

View file

@ -5,8 +5,10 @@ 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)
// Convert pixel coordinates to world units
// NOTE: Section posX/posY are RELATIVE to the room (already in room's local space)
// So we scale them for placement within the room group
const SCALE = 0.1;
interface SmartRackProps {
section: Section3D;
@ -14,12 +16,11 @@ interface SmartRackProps {
onPlantClick: (plant: PlantPosition) => void;
highlightedTags?: string[];
dimMode?: boolean;
roomName?: string; // For breadcrumb
roomName?: string;
}
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
// Section positions are RELATIVE to room, scale them
const scaledSection = {
posX: section.posX * SCALE,
posY: section.posY * SCALE,
@ -27,25 +28,21 @@ export function SmartRack({ section, visMode, onPlantClick, highlightedTags, dim
height: section.height * SCALE,
};
// Calculate plant positions RELATIVE to section position (within room group)
// Calculate how plants should be positioned within the section
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;
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
// Calculate spacing to fit plants within section bounds
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),
// Position plants WITHIN the section, starting from section origin
x: scaledSection.posX + (pos.column * colSpacing),
z: scaledSection.posY + (pos.row * rowSpacing),
y: 0.4 + (pos.tier * 0.6),
// Add breadcrumb data
breadcrumb: {
section: section.code || section.name,
room: roomName,
@ -57,7 +54,11 @@ export function SmartRack({ section, visMode, onPlantClick, highlightedTags, dim
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;
// 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>
@ -65,7 +66,7 @@ export function SmartRack({ section, visMode, onPlantClick, highlightedTags, dim
{visMode === 'STANDARD' && (
<Text
position={[scaledSection.posX + scaledSection.width / 2, 3, scaledSection.posY + scaledSection.height / 2]}
fontSize={0.8}
fontSize={0.6}
color="#94a3b8"
anchorX="center"
anchorY="bottom"
@ -90,9 +91,9 @@ export function SmartRack({ section, visMode, onPlantClick, highlightedTags, dim
>
<planeGeometry args={[scaledSection.width * 0.95, scaledSection.height * 0.95]} />
<meshStandardMaterial
color="#64748b"
color="#475569"
roughness={0.3}
metalness={0.7}
metalness={0.5}
side={THREE.DoubleSide}
/>
</mesh>
@ -119,9 +120,9 @@ export function SmartRack({ section, visMode, onPlantClick, highlightedTags, dim
{visMode === 'STANDARD' && distinctRows.map(row => (
<Text
key={`row-${row}`}
position={[scaledSection.posX - 0.3, 0.8, scaledSection.posY + row * plantSpacing]}
position={[scaledSection.posX - 0.2, 0.8, scaledSection.posY + row * rowSpacing]}
rotation={[-Math.PI / 2, 0, 0]}
fontSize={0.18}
fontSize={0.15}
color="#64748b"
anchorX="right"
>
@ -133,9 +134,9 @@ export function SmartRack({ section, visMode, onPlantClick, highlightedTags, dim
{visMode === 'STANDARD' && distinctCols.map(col => (
<Text
key={`col-${col}`}
position={[scaledSection.posX + col * plantSpacing, 0.15, scaledSection.posY + scaledSection.height + 0.2]}
position={[scaledSection.posX + col * colSpacing, 0.15, scaledSection.posY + scaledSection.height + 0.15]}
rotation={[-Math.PI / 2, 0, 0]}
fontSize={0.18}
fontSize={0.15}
color="#64748b"
anchorX="center"
>