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 { Environment, ContactShadows } from '@react-three/drei';
import { CameraControls } from '@react-three/drei'; import { CameraControls } from '@react-three/drei';
import type { Floor3DData, Room3D } from '../../lib/layoutApi'; import type { Floor3DData, Room3D } from '../../lib/layoutApi';
@ -7,7 +7,7 @@ import { Beacon } from './Beacon';
import { PlantPosition, VisMode } from './types'; import { PlantPosition, VisMode } from './types';
import { CameraPresets, CameraPreset } from './CameraPresets'; 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; const SCALE = 0.1;
interface FacilitySceneProps { interface FacilitySceneProps {
@ -33,11 +33,35 @@ export function FacilityScene({
dimMode = false, dimMode = false,
beaconPosition = null, beaconPosition = null,
}: FacilitySceneProps) { }: FacilitySceneProps) {
// Scale floor dimensions for centering and camera // Calculate actual floor bounds from all sections
const scaledFloor = { const floorBounds = useMemo(() => {
width: data.floor.width * SCALE, let minX = Infinity, minZ = Infinity, maxX = -Infinity, maxZ = -Infinity;
height: data.floor.height * SCALE,
}; 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 ( return (
<> <>
@ -50,7 +74,8 @@ export function FacilityScene({
shadow-mapSize={[2048, 2048]} 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) => ( {data.rooms.map((room: Room3D) => (
<RoomObject <RoomObject
key={room.id} key={room.id}
@ -69,7 +94,7 @@ export function FacilityScene({
<ContactShadows <ContactShadows
position={[0, -0.02, 0]} position={[0, -0.02, 0]}
opacity={0.35} opacity={0.35}
scale={120} scale={floorBounds.width * 1.5}
blur={2} blur={2}
far={12} far={12}
resolution={512} resolution={512}
@ -79,8 +104,8 @@ export function FacilityScene({
{/* Preset-based camera system */} {/* Preset-based camera system */}
<CameraPresets <CameraPresets
preset={cameraPreset} preset={cameraPreset}
floorWidth={scaledFloor.width} floorWidth={floorBounds.width}
floorHeight={scaledFloor.height} floorHeight={floorBounds.height}
focusTarget={focusTarget} focusTarget={focusTarget}
onReady={onControlsReady} onReady={onControlsReady}
/> />

View file

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

View file

@ -5,8 +5,10 @@ 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';
// Convert pixel coordinates to world units (DB stores pixels, 3D uses meters/units) // Convert pixel coordinates to world units
const SCALE = 0.1; // 1 pixel = 0.1 world units (so 700px = 70 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 { interface SmartRackProps {
section: Section3D; section: Section3D;
@ -14,12 +16,11 @@ interface SmartRackProps {
onPlantClick: (plant: PlantPosition) => void; onPlantClick: (plant: PlantPosition) => void;
highlightedTags?: string[]; highlightedTags?: string[];
dimMode?: boolean; dimMode?: boolean;
roomName?: string; // For breadcrumb roomName?: string;
} }
export function SmartRack({ section, visMode, onPlantClick, highlightedTags, dimMode, roomName }: SmartRackProps) { export function SmartRack({ section, visMode, onPlantClick, highlightedTags, dimMode, roomName }: SmartRackProps) {
// Scale section dimensions to world units // Section positions are RELATIVE to room, scale them
// Section posX/posY are RELATIVE to room, so we scale them for placement within room group
const scaledSection = { const scaledSection = {
posX: section.posX * SCALE, posX: section.posX * SCALE,
posY: section.posY * SCALE, posY: section.posY * SCALE,
@ -27,25 +28,21 @@ export function SmartRack({ section, visMode, onPlantClick, highlightedTags, dim
height: section.height * SCALE, 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 positions: PlantPosition[] = useMemo(() => {
const plantSpacing = 0.5; // Spacing between plants in world units const maxCols = Math.max(...section.positions.map(p => p.column), 1);
// Calculate how many columns/rows fit based on section dimensions 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 colSpacing = scaledSection.width / (maxCols + 1);
const rowSpacing = scaledSection.height / (maxRows + 1); const rowSpacing = scaledSection.height / (maxRows + 1);
const actualSpacing = Math.min(colSpacing, rowSpacing, plantSpacing);
return section.positions.map((pos: Position3D) => ({ return section.positions.map((pos: Position3D) => ({
...pos, ...pos,
// Position plants within the scaled section bounds // Position plants WITHIN the section, starting from section origin
x: scaledSection.posX + (pos.column * actualSpacing), x: scaledSection.posX + (pos.column * colSpacing),
z: scaledSection.posY + (pos.row * actualSpacing), z: scaledSection.posY + (pos.row * rowSpacing),
y: 0.4 + (pos.tier * 0.6), y: 0.4 + (pos.tier * 0.6),
// Add breadcrumb data
breadcrumb: { breadcrumb: {
section: section.code || section.name, section: section.code || section.name,
room: roomName, 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 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);
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 ( return (
<group> <group>
@ -65,7 +66,7 @@ export function SmartRack({ section, visMode, onPlantClick, highlightedTags, dim
{visMode === 'STANDARD' && ( {visMode === 'STANDARD' && (
<Text <Text
position={[scaledSection.posX + scaledSection.width / 2, 3, scaledSection.posY + scaledSection.height / 2]} position={[scaledSection.posX + scaledSection.width / 2, 3, scaledSection.posY + scaledSection.height / 2]}
fontSize={0.8} fontSize={0.6}
color="#94a3b8" color="#94a3b8"
anchorX="center" anchorX="center"
anchorY="bottom" anchorY="bottom"
@ -90,9 +91,9 @@ export function SmartRack({ section, visMode, onPlantClick, highlightedTags, dim
> >
<planeGeometry args={[scaledSection.width * 0.95, scaledSection.height * 0.95]} /> <planeGeometry args={[scaledSection.width * 0.95, scaledSection.height * 0.95]} />
<meshStandardMaterial <meshStandardMaterial
color="#64748b" color="#475569"
roughness={0.3} roughness={0.3}
metalness={0.7} metalness={0.5}
side={THREE.DoubleSide} side={THREE.DoubleSide}
/> />
</mesh> </mesh>
@ -119,9 +120,9 @@ export function SmartRack({ section, visMode, onPlantClick, highlightedTags, dim
{visMode === 'STANDARD' && distinctRows.map(row => ( {visMode === 'STANDARD' && distinctRows.map(row => (
<Text <Text
key={`row-${row}`} 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]} rotation={[-Math.PI / 2, 0, 0]}
fontSize={0.18} fontSize={0.15}
color="#64748b" color="#64748b"
anchorX="right" anchorX="right"
> >
@ -133,9 +134,9 @@ export function SmartRack({ section, visMode, onPlantClick, highlightedTags, dim
{visMode === 'STANDARD' && distinctCols.map(col => ( {visMode === 'STANDARD' && distinctCols.map(col => (
<Text <Text
key={`col-${col}`} 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]} rotation={[-Math.PI / 2, 0, 0]}
fontSize={0.18} fontSize={0.15}
color="#64748b" color="#64748b"
anchorX="center" anchorX="center"
> >