From ddaf67ab1ee51dfc85fd5b321cfff84e1db394dd Mon Sep 17 00:00:00 2001 From: fullsizemalt <106900403+fullsizemalt@users.noreply.github.com> Date: Thu, 18 Dec 2025 19:57:25 -0800 Subject: [PATCH] fix: calculate room bounds from section positions for proper stacking - 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 --- .../components/facility3d/FacilityScene.tsx | 47 +++++++++++++----- .../src/components/facility3d/RoomObject.tsx | 47 +++++++++++++----- .../src/components/facility3d/SmartRack.tsx | 49 ++++++++++--------- 3 files changed, 95 insertions(+), 48 deletions(-) diff --git a/frontend/src/components/facility3d/FacilityScene.tsx b/frontend/src/components/facility3d/FacilityScene.tsx index 2910715..877da9b 100644 --- a/frontend/src/components/facility3d/FacilityScene.tsx +++ b/frontend/src/components/facility3d/FacilityScene.tsx @@ -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]} /> - + {/* Offset to center the content */} + {data.rooms.map((room: Room3D) => ( diff --git a/frontend/src/components/facility3d/RoomObject.tsx b/frontend/src/components/facility3d/RoomObject.tsx index fa74da3..7872044 100644 --- a/frontend/src/components/facility3d/RoomObject.tsx +++ b/frontend/src/components/facility3d/RoomObject.tsx @@ -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 ( - + + {/* Room label - positioned above the actual content area */} @@ -70,9 +88,9 @@ export function RoomObject({ room, visMode, onPlantClick, highlightedTags, dimMo {(visMode === 'TEMP' || visMode === 'HUMIDITY') && ( )} - - + {/* Room floor - use calculated bounds */} + + - - + {/* Room boundary */} + + + {/* Sections are now positioned using their own absolute coordinates */} {room.sections.map(section => ( 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 ( @@ -65,7 +66,7 @@ export function SmartRack({ section, visMode, onPlantClick, highlightedTags, dim {visMode === 'STANDARD' && ( @@ -119,9 +120,9 @@ export function SmartRack({ section, visMode, onPlantClick, highlightedTags, dim {visMode === 'STANDARD' && distinctRows.map(row => ( @@ -133,9 +134,9 @@ export function SmartRack({ section, visMode, onPlantClick, highlightedTags, dim {visMode === 'STANDARD' && distinctCols.map(col => (