fix: 3D viewer position scaling and stage coloring
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

- Add SCALE constant (0.1) to convert pixel coords to world units
- Fix stage coloring: use FLOWERING/DRYING/CURING instead of FLOWER/DRY/CURE
- Apply scaling to FacilityScene, RoomObject, SmartRack consistently
- Plants now render inside their proper rooms
This commit is contained in:
fullsizemalt 2025-12-18 15:52:05 -08:00
parent 71f1e23ff3
commit d56b7f0b11
4 changed files with 62 additions and 26 deletions

View file

@ -6,6 +6,9 @@ import { RoomObject } from './RoomObject';
import { Beacon } from './Beacon'; import { Beacon } from './Beacon';
import { PlantPosition, VisMode } from './types'; import { PlantPosition, VisMode } from './types';
// Convert pixel coordinates to world units (same as RoomObject/SmartRack)
const SCALE = 0.1;
interface FacilitySceneProps { interface FacilitySceneProps {
data: Floor3DData; data: Floor3DData;
visMode: VisMode; visMode: VisMode;
@ -30,6 +33,12 @@ export function FacilityScene({
const controlsRef = useRef<CameraControls>(null); const controlsRef = useRef<CameraControls>(null);
const [keysPressed, setKeysPressed] = useState<Record<string, boolean>>({}); const [keysPressed, setKeysPressed] = useState<Record<string, boolean>>({});
// Scale floor dimensions for centering
const scaledFloor = {
width: data.floor.width * SCALE,
height: data.floor.height * SCALE,
};
useEffect(() => { useEffect(() => {
if (controlsRef.current) onControlsReady(controlsRef.current); if (controlsRef.current) onControlsReady(controlsRef.current);
}, [controlsRef.current, onControlsReady]); }, [controlsRef.current, onControlsReady]);
@ -88,7 +97,7 @@ export function FacilityScene({
shadow-mapSize={[2048, 2048]} shadow-mapSize={[2048, 2048]}
/> />
<group position={[-data.floor.width / 2, 0, -data.floor.height / 2]}> <group position={[-scaledFloor.width / 2, 0, -scaledFloor.height / 2]}>
{data.rooms.map((room: Room3D) => ( {data.rooms.map((room: Room3D) => (
<RoomObject <RoomObject
key={room.id} key={room.id}

View file

@ -50,9 +50,13 @@ export function PlantSystem({
let color: string; let color: string;
switch (visMode) { switch (visMode) {
case 'STANDARD': case 'STANDARD':
color = pos.plant?.stage === 'FLOWER' ? COLORS.FLOWER : // Match actual BatchStage enum values from schema
pos.plant?.stage === 'DRYING' ? COLORS.DRY : const stage = pos.plant?.stage;
pos.plant?.stage === 'CURE' ? COLORS.CURE : COLORS.VEG; color = (stage === 'FLOWERING') ? COLORS.FLOWER :
(stage === 'DRYING') ? COLORS.DRY :
(stage === 'CURING') ? COLORS.CURE :
(stage === 'CLONE_IN' || stage === 'VEGETATIVE') ? COLORS.VEG :
COLORS.VEG; // Default fallback
break; break;
case 'HEALTH': case 'HEALTH':
const status = getMockPlantHealth(pos.plant!.id); const status = getMockPlantHealth(pos.plant!.id);

View file

@ -4,6 +4,9 @@ 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)
const SCALE = 0.1;
// Mock environment data // Mock environment data
const getMockRoomEnv = (roomName: string) => { const getMockRoomEnv = (roomName: string) => {
const hash = roomName.split('').reduce((a, b) => a + b.charCodeAt(0), 0); const hash = roomName.split('').reduce((a, b) => a + b.charCodeAt(0), 0);
@ -30,6 +33,14 @@ interface RoomObjectProps {
export function RoomObject({ room, visMode, onPlantClick, highlightedTags, dimMode }: RoomObjectProps) { export function RoomObject({ room, visMode, onPlantClick, highlightedTags, dimMode }: RoomObjectProps) {
const env = getMockRoomEnv(room.name); const env = getMockRoomEnv(room.name);
// Scale room dimensions to world units
const scaledRoom = {
posX: room.posX * SCALE,
posY: room.posY * SCALE,
width: room.width * SCALE,
height: room.height * SCALE,
};
let floorColor: string = COLORS.ROOM_FLOOR; let floorColor: string = COLORS.ROOM_FLOOR;
if (visMode === 'TEMP') { if (visMode === 'TEMP') {
const t = (env.temp - 65) / 20; const t = (env.temp - 65) / 20;
@ -44,11 +55,11 @@ export function RoomObject({ room, visMode, onPlantClick, highlightedTags, dimMo
} }
return ( return (
<group position={[room.posX, 0, room.posY]}> <group position={[scaledRoom.posX, 0, scaledRoom.posY]}>
<Text <Text
position={[room.width / 2, 0.05, room.height / 2]} position={[scaledRoom.width / 2, 0.05, scaledRoom.height / 2]}
rotation={[-Math.PI / 2, 0, 0]} rotation={[-Math.PI / 2, 0, 0]}
fontSize={Math.min(room.width, room.height) / 6} fontSize={Math.min(scaledRoom.width, scaledRoom.height) / 6}
color="#f8fafc" color="#f8fafc"
fillOpacity={0.7} fillOpacity={0.7}
anchorX="center" anchorX="center"
@ -59,9 +70,9 @@ export function RoomObject({ room, visMode, onPlantClick, highlightedTags, dimMo
{(visMode === 'TEMP' || visMode === 'HUMIDITY') && ( {(visMode === 'TEMP' || visMode === 'HUMIDITY') && (
<Text <Text
position={[room.width / 2, 4, room.height / 2]} position={[scaledRoom.width / 2, 4, scaledRoom.height / 2]}
rotation={[-Math.PI / 4, 0, 0]} rotation={[-Math.PI / 4, 0, 0]}
fontSize={4} fontSize={Math.min(4, scaledRoom.width / 4)}
color="#fff" color="#fff"
outlineColor="#000" outlineColor="#000"
outlineWidth={0.15} outlineWidth={0.15}
@ -71,8 +82,8 @@ export function RoomObject({ room, visMode, onPlantClick, highlightedTags, dimMo
</Text> </Text>
)} )}
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[room.width / 2, 0, room.height / 2]} receiveShadow> <mesh rotation={[-Math.PI / 2, 0, 0]} position={[scaledRoom.width / 2, 0, scaledRoom.height / 2]} receiveShadow>
<planeGeometry args={[room.width, room.height]} /> <planeGeometry args={[scaledRoom.width, scaledRoom.height]} />
<meshStandardMaterial <meshStandardMaterial
color={floorColor} color={floorColor}
roughness={0.95} roughness={0.95}
@ -82,8 +93,8 @@ export function RoomObject({ room, visMode, onPlantClick, highlightedTags, dimMo
/> />
</mesh> </mesh>
<lineSegments position={[room.width / 2, 0.02, room.height / 2]} rotation={[-Math.PI / 2, 0, 0]}> <lineSegments position={[scaledRoom.width / 2, 0.02, scaledRoom.height / 2]} rotation={[-Math.PI / 2, 0, 0]}>
<edgesGeometry args={[new THREE.PlaneGeometry(room.width, room.height)]} /> <edgesGeometry args={[new THREE.PlaneGeometry(scaledRoom.width, scaledRoom.height)]} />
<lineBasicMaterial color="#64748b" /> <lineBasicMaterial color="#64748b" />
</lineSegments> </lineSegments>

View file

@ -5,6 +5,9 @@ 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)
const SCALE = 0.1; // 1 pixel = 0.1 world units (so 700px = 70 units)
interface SmartRackProps { interface SmartRackProps {
section: Section3D; section: Section3D;
visMode: VisMode; visMode: VisMode;
@ -14,27 +17,36 @@ interface SmartRackProps {
} }
export function SmartRack({ section, visMode, onPlantClick, highlightedTags, dimMode }: SmartRackProps) { export function SmartRack({ section, visMode, onPlantClick, highlightedTags, dimMode }: SmartRackProps) {
// Scale section dimensions to world units
const scaledSection = {
posX: section.posX * SCALE,
posY: section.posY * SCALE,
width: section.width * SCALE,
height: section.height * SCALE,
};
const positions: PlantPosition[] = useMemo(() => { const positions: PlantPosition[] = useMemo(() => {
const spacing = 0.5; const plantSpacing = 0.5; // Spacing between plants in world units
return section.positions.map((pos: Position3D) => ({ return section.positions.map((pos: Position3D) => ({
...pos, ...pos,
x: section.posX + (pos.column * spacing), // Position plants within the scaled section bounds
z: section.posY + (pos.row * spacing), x: scaledSection.posX + (pos.column * plantSpacing),
z: scaledSection.posY + (pos.row * plantSpacing),
y: 0.4 + (pos.tier * 0.6), y: 0.4 + (pos.tier * 0.6),
})); }));
}, [section]); }, [section, scaledSection]);
const distinctTiers = [...new Set(positions.map(p => p.tier))].sort((a, b) => a - b); 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 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 spacing = 0.5; const plantSpacing = 0.5;
return ( return (
<group> <group>
{visMode === 'STANDARD' && ( {visMode === 'STANDARD' && (
<Text <Text
position={[section.posX + section.width / 2, 3, section.posY + section.height / 2]} position={[scaledSection.posX + scaledSection.width / 2, 3, scaledSection.posY + scaledSection.height / 2]}
fontSize={0.8} fontSize={0.8}
color="#94a3b8" color="#94a3b8"
anchorX="center" anchorX="center"
@ -50,14 +62,14 @@ export function SmartRack({ section, visMode, onPlantClick, highlightedTags, dim
<mesh <mesh
key={`shelf-${tier}`} key={`shelf-${tier}`}
position={[ position={[
section.posX + section.width / 2, scaledSection.posX + scaledSection.width / 2,
0.35 + (tier * 0.6), 0.35 + (tier * 0.6),
section.posY + section.height / 2 scaledSection.posY + scaledSection.height / 2
]} ]}
rotation={[-Math.PI / 2, 0, 0]} rotation={[-Math.PI / 2, 0, 0]}
receiveShadow receiveShadow
> >
<planeGeometry args={[section.width * 0.95, section.height * 0.95]} /> <planeGeometry args={[scaledSection.width * 0.95, scaledSection.height * 0.95]} />
<meshStandardMaterial <meshStandardMaterial
color="#64748b" color="#64748b"
roughness={0.3} roughness={0.3}
@ -72,9 +84,9 @@ export function SmartRack({ section, visMode, onPlantClick, highlightedTags, dim
<mesh <mesh
key={`support-${xOffset}-${zOffset}`} key={`support-${xOffset}-${zOffset}`}
position={[ position={[
section.posX + (xOffset * section.width), scaledSection.posX + (xOffset * scaledSection.width),
(distinctTiers.length * 0.6) / 2 + 0.2, (distinctTiers.length * 0.6) / 2 + 0.2,
section.posY + (zOffset * section.height) scaledSection.posY + (zOffset * scaledSection.height)
]} ]}
> >
<boxGeometry args={[0.08, distinctTiers.length * 0.6 + 0.3, 0.08]} /> <boxGeometry args={[0.08, distinctTiers.length * 0.6 + 0.3, 0.08]} />
@ -86,7 +98,7 @@ 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={[section.posX - 0.3, 0.8, section.posY + row * spacing]} position={[scaledSection.posX - 0.3, 0.8, scaledSection.posY + row * plantSpacing]}
rotation={[-Math.PI / 2, 0, 0]} rotation={[-Math.PI / 2, 0, 0]}
fontSize={0.18} fontSize={0.18}
color="#64748b" color="#64748b"
@ -99,7 +111,7 @@ 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={[section.posX + col * spacing, 0.15, section.posY + section.height + 0.2]} position={[scaledSection.posX + col * plantSpacing, 0.15, scaledSection.posY + scaledSection.height + 0.2]}
rotation={[-Math.PI / 2, 0, 0]} rotation={[-Math.PI / 2, 0, 0]}
fontSize={0.18} fontSize={0.18}
color="#64748b" color="#64748b"