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
This commit is contained in:
parent
9194335dd7
commit
ddaf67ab1e
3 changed files with 95 additions and 48 deletions
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue