Files
WebUITest-RealProjecT/FrontEnd/components/Machine3D.tsx
arDTDev 86fe466b55 feat: Add VisionData panel, HW error display, and reel handler 3D model
- Add hardware error banner with priority system (motion > i/o > emergency)
- Add DIO status to HW status display with backend integration
- Remove status text from HW status, keep only LED indicators
- Add VisionDataPanel showing real-time recognized data for L/C/R ports
- Add GetVisionData API in MachineBridge with batch field support
- Add BroadcastVisionData function (250ms interval)
- Replace 3D model with detailed reel handler equipment
- Use OrthographicCamera with front view for distortion-free display
- Fix ProcessedDataPanel layout to avoid right sidebar overlap
- Show log viewer filename in error message when file not found

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-27 23:22:56 +09:00

564 lines
25 KiB
TypeScript

import React, { useRef } from 'react';
import { Canvas, useFrame } from '@react-three/fiber';
import { OrbitControls, Grid, OrthographicCamera, Text, Box, Environment, RoundedBox, Cylinder, Plane } from '@react-three/drei';
import * as THREE from 'three';
import { RobotTarget, IOPoint } from '../types';
interface Machine3DProps {
target: RobotTarget;
ioState: IOPoint[];
doorStates: {
front: boolean;
right: boolean;
left: boolean;
back: boolean;
};
}
// Reusable Aluminum Profile Component
const Profile = ({ position, size, rotation = [0, 0, 0] }: { position: [number, number, number], size: [number, number, number], rotation?: [number, number, number] }) => (
<RoundedBox position={position} args={size} radius={0.05} smoothness={4} rotation={rotation as any}>
<meshStandardMaterial color="#b0b5be" roughness={0.3} metalness={0.8} />
</RoundedBox>
);
// Industrial Linear Actuator Rail (Black body + Silver guides)
const RobotRail = ({
length,
orientation = 'horizontal',
profile = [0.25, 0.25]
}: {
length: number,
orientation?: 'horizontal' | 'vertical',
profile?: [number, number]
}) => {
const isHoriz = orientation === 'horizontal';
const [dim1, dim2] = profile;
const size = isHoriz ? [length, dim1, dim2] : [dim1, length, dim2];
const railW = isHoriz ? length : dim1 * 0.4;
const railH = isHoriz ? dim1 * 0.4 : length;
const railD = 0.02;
const guideZ = dim2 / 2 + 0.005;
return (
<group>
<RoundedBox args={size as [number, number, number]} radius={0.02}>
<meshStandardMaterial color="#1e293b" roughness={0.4} metalness={0.5} />
</RoundedBox>
<group position={[0, 0, guideZ]}>
<Box args={[railW, railH, railD]} position={isHoriz ? [0, dim1 * 0.25, 0] : [dim1 * 0.25, 0, 0]}>
<meshStandardMaterial color="#e2e8f0" metalness={0.8} roughness={0.2} />
</Box>
<Box args={[railW, railH, railD]} position={isHoriz ? [0, -dim1 * 0.25, 0] : [-dim1 * 0.25, 0, 0]}>
<meshStandardMaterial color="#e2e8f0" metalness={0.8} roughness={0.2} />
</Box>
</group>
</group>
);
};
// Detailed Industrial Camera Component
const IndustrialCamera = ({ position, rotation = [0, 0, 0], isActive = false }: { position: [number, number, number], rotation?: [number, number, number], isActive?: boolean }) => {
return (
<group position={position} rotation={rotation as any}>
<group rotation={[0, 0, 0]}>
<Cylinder args={[0.14, 0.14, 0.1, 32]} position={[0, 0.45, 0]}>
<meshStandardMaterial color="#111" />
</Cylinder>
<Cylinder args={[0.135, 0.135, 0.15, 32]} position={[0, 0.32, 0]}>
<meshStandardMaterial color="#111" roughness={0.4} />
</Cylinder>
<Cylinder args={[0.13, 0.13, 0.05, 32]} position={[0, 0.20, 0]}>
<meshStandardMaterial color="#e2e8f0" metalness={0.9} roughness={0.1} />
</Cylinder>
<Cylinder args={[0.125, 0.125, 0.02, 32]} position={[0, 0.15, 0]}>
<meshStandardMaterial color="#f97316" />
</Cylinder>
<Cylinder args={[0.13, 0.13, 0.05, 32]} position={[0, 0.10, 0]}>
<meshStandardMaterial color="#e2e8f0" metalness={0.9} roughness={0.1} />
</Cylinder>
<Cylinder args={[0.15, 0.15, 0.4, 32]} position={[0, -0.15, 0]}>
<meshStandardMaterial color="#0f172a" metalness={0.5} roughness={0.5} />
</Cylinder>
<Box args={[0.32, 0.4, 0.32]} position={[0, -0.45, 0]}>
<meshStandardMaterial color="#111" />
</Box>
<Plane args={[0.25, 0.3]} position={[0, -0.45, 0.161]} rotation={[0, 0, Math.PI]}>
<meshStandardMaterial color="#f1f5f9" />
</Plane>
<Text position={[0, -0.4, 0.162]} rotation={[0, 0, Math.PI]} fontSize={0.05} color="black">MA-D200B</Text>
<Text position={[0, -0.5, 0.162]} rotation={[0, 0, Math.PI]} fontSize={0.03} color="black">DC 12V</Text>
<Cylinder args={[0.08, 0.08, 0.1, 16]} position={[0, -0.7, 0]}>
<meshStandardMaterial color="#94a3b8" metalness={0.8} />
</Cylinder>
</group>
{isActive && (
<Cylinder args={[0.05, 0.6, 2.5, 16]} position={[0, 1.7, 0]} rotation={[0, 0, 0]}>
<meshBasicMaterial color="#22c55e" opacity={0.15} transparent depthWrite={false} />
</Cylinder>
)}
</group>
);
};
// SATO CL4NX Plus Printer Component
const SatoPrinter = ({ position, rotation = [0, 0, 0] }: { position: [number, number, number], rotation?: [number, number, number] }) => {
return (
<group position={position} rotation={rotation as any}>
<RoundedBox args={[0.8, 1.0, 1.5]} position={[0.2, 0, 0]} radius={0.05}>
<meshStandardMaterial color="#c0c0c0" roughness={0.4} metalness={0.6} />
</RoundedBox>
<RoundedBox args={[0.05, 0.5, 0.8]} position={[0.6, 0.1, 0]} radius={0.02}>
<meshStandardMaterial color="#111" roughness={0.2} metalness={0.8} opacity={0.8} transparent />
</RoundedBox>
<RoundedBox args={[0.4, 1.0, 1.5]} position={[-0.4, 0, 0]} radius={0.05}>
<meshStandardMaterial color="#1e1e1e" roughness={0.6} />
</RoundedBox>
<group position={[-0.4, 0, 0.76]}>
<Plane args={[0.25, 0.2]} position={[0, 0.2, 0]} rotation={[0, 0, 0]}>
<meshBasicMaterial color="#0ea5e9" />
</Plane>
<Plane args={[0.25, 0.3]} position={[0, -0.15, 0]} rotation={[0, 0, 0]}>
<meshStandardMaterial color="#333" />
</Plane>
<Box args={[0.3, 0.02, 0.01]} position={[0, 0.35, 0]}>
<meshStandardMaterial color="#0044ff" emissive="#0044ff" emissiveIntensity={4} toneMapped={false} />
</Box>
<Text position={[0, 0.42, 0]} fontSize={0.04} color="white">SATO</Text>
<Text position={[0, 0.39, 0]} fontSize={0.03} color="white">CL4NX Plus</Text>
</group>
<Box args={[0.01, 1.0, 1.51]} position={[-0.2, 0, 0]}>
<meshStandardMaterial color="#000" />
</Box>
</group>
);
};
// Industrial Cart Component
const Cart = ({ position, label }: { position: [number, number, number], label?: string }) => {
return (
<group position={position}>
<group position={[0, 0.15, 0]}>
<Cylinder position={[-0.7, 0, -0.7]} rotation={[0,0,Math.PI/2]} args={[0.15, 0.15, 0.1]}><meshStandardMaterial color="#111" /></Cylinder>
<Cylinder position={[0.7, 0, -0.7]} rotation={[0,0,Math.PI/2]} args={[0.15, 0.15, 0.1]}><meshStandardMaterial color="#111" /></Cylinder>
<Cylinder position={[-0.7, 0, 0.7]} rotation={[0,0,Math.PI/2]} args={[0.15, 0.15, 0.1]}><meshStandardMaterial color="#111" /></Cylinder>
<Cylinder position={[0.7, 0, 0.7]} rotation={[0,0,Math.PI/2]} args={[0.15, 0.15, 0.1]}><meshStandardMaterial color="#111" /></Cylinder>
</group>
<RoundedBox position={[0, 0.4, 0]} args={[1.8, 0.1, 1.8]} radius={0.05}>
<meshStandardMaterial color="#cbd5e1" metalness={0.5} />
</RoundedBox>
<Profile position={[-0.85, 1.4, -0.85]} size={[0.05, 2.0, 0.05]} />
<Profile position={[0.85, 1.4, -0.85]} size={[0.05, 2.0, 0.05]} />
<Profile position={[-0.85, 1.4, 0.85]} size={[0.05, 2.0, 0.05]} />
<Profile position={[0.85, 1.4, 0.85]} size={[0.05, 2.0, 0.05]} />
<Box position={[0, 2.4, 0]} args={[1.8, 0.05, 1.8]}>
<meshStandardMaterial color="#94a3b8" transparent opacity={0.2} />
</Box>
<group position={[0, 0.45, 0]}>
{Array.from({ length: 15 }).map((_, i) => (
<group key={i} position={[0, 0.1 + (i * 0.12), 0]}>
<Cylinder args={[0.6, 0.6, 0.1, 32]}>
<meshStandardMaterial color="#1e293b" />
</Cylinder>
<Cylinder args={[0.61, 0.61, 0.02, 32]}>
<meshStandardMaterial color="#f8fafc" />
</Cylinder>
</group>
))}
{[0, 120, 240].map((angle, idx) => {
const rad = 0.65;
const x = Math.cos(angle * Math.PI / 180) * rad;
const z = Math.sin(angle * Math.PI / 180) * rad;
return (
<Cylinder key={idx} args={[0.02, 0.02, 2.2, 8]} position={[x, 1.1, z]}>
<meshStandardMaterial color="#cbd5e1" metalness={0.6} roughness={0.3} />
</Cylinder>
);
})}
</group>
{label && (
<Text position={[0, 2.6, 0]} fontSize={0.2} color="white" anchorY="bottom">
{label}
</Text>
)}
</group>
);
};
// Glass Door Component
const Door = ({ position, size, isOpen, hingeDirection = 1 }: { position: [number, number, number], size: [number, number, number], isOpen: boolean, hingeDirection?: number }) => {
const meshRef = useRef<THREE.Group>(null);
useFrame(() => {
if (meshRef.current) {
const targetRotation = isOpen ? Math.PI / 2 * hingeDirection : 0;
meshRef.current.rotation.y = THREE.MathUtils.lerp(meshRef.current.rotation.y, targetRotation, 0.1);
}
});
return (
<group ref={meshRef} position={[position[0] - (size[0]/2 * hingeDirection), position[1], position[2]]}>
<group position={[(size[0]/2 * hingeDirection), 0, 0]}>
<Box args={[size[0], size[1], 0.05]} position={[0, 0, 0]}>
<meshStandardMaterial color="#334155" />
</Box>
<Box args={[size[0] - 0.1, size[1] - 0.1, 0.04]} position={[0, 0, 0]}>
<meshPhysicalMaterial
color="#ffffff"
transmission={0.99}
opacity={0.1}
transparent={true}
roughness={0}
metalness={0.1}
clearcoat={1}
side={THREE.DoubleSide}
depthWrite={false}
/>
</Box>
<Box args={[0.05, 0.4, 0.1]} position={[-size[0]/2 * hingeDirection * 0.8, 0, 0.05]}>
<meshStandardMaterial color="black" />
</Box>
</group>
</group>
);
};
// Reusable Labeler Unit Component
const LabelerUnit = ({ side, posY, posZ }: { side: 'left' | 'right', posY: number, posZ: number }) => {
const isLeft = side === 'left';
const wallOffsetX = isLeft ? -1.4 : 1.4;
const printerOffset = isLeft ? 0.3 : -0.3;
return (
<group>
<Box args={[1.0, 0.1, 1.6]} position={[printerOffset, 0.05, -1.2]}>
<meshStandardMaterial color="#1e293b" />
</Box>
<SatoPrinter position={[printerOffset, 0.6, -1.2]} rotation={[0, 0, 0]} />
<group position={[wallOffsetX, 0, 0]}>
<Profile position={[0, 0.75, -1.45]} size={[0.1, 1.5, 0.1]} />
<Profile position={[0, 0.75, 1.45]} size={[0.1, 1.5, 0.1]} />
<Profile position={[0, 1.5, 0]} size={[0.1, 0.1, 3.0]} />
<group position={[0, 1.4, 0]} rotation={[0, Math.PI/2, 0]}>
<RobotRail length={3.0} orientation="horizontal" profile={[0.2, 0.2]} />
</group>
<group position={[0, 1.3, -1.0 + posY]}>
<Box args={[0.3, 0.15, 0.3]} position={[0, 0, 0]}>
<meshStandardMaterial color="#f97316" />
</Box>
<Box args={[Math.abs(wallOffsetX), 0.1, 0.15]} position={[-wallOffsetX / 2, -0.05, 0]}>
<meshStandardMaterial color="#334155" />
</Box>
<group position={[-wallOffsetX, -0.1, 0]}>
<Box args={[0.15, 0.3, 0.15]} position={[0, 0, 0]}><meshStandardMaterial color="#111" /></Box>
<Cylinder args={[0.04, 0.04, posZ, 8]} position={[0, -posZ/2, 0]}>
<meshStandardMaterial color="#ccc" />
</Cylinder>
<Cylinder args={[0.12, 0.12, 0.05, 16]} position={[0, -posZ, 0]}>
<meshStandardMaterial color="black" />
</Cylinder>
<Cylinder args={[0.15, 0.15, 0.02, 16]} position={[0, -posZ - 0.02, 0]}>
<meshStandardMaterial color="yellow" transparent opacity={0.8} />
</Cylinder>
</group>
</group>
</group>
</group>
);
};
// Tower Lamp Component
const TowerLamp = ({ ioState }: { ioState: IOPoint[] }) => {
const redOn = ioState.find(io => io.id === 0 && io.type === 'output')?.state;
const yellowOn = ioState.find(io => io.id === 1 && io.type === 'output')?.state;
const greenOn = ioState.find(io => io.id === 2 && io.type === 'output')?.state;
const redMat = useRef<THREE.MeshStandardMaterial>(null);
const yelMat = useRef<THREE.MeshStandardMaterial>(null);
const grnMat = useRef<THREE.MeshStandardMaterial>(null);
useFrame((state) => {
const time = state.clock.elapsedTime;
const pulse = (Math.sin(time * 6) * 0.5 + 0.5);
const intensity = 0.5 + (pulse * 3.0);
if (redMat.current) {
redMat.current.emissiveIntensity = redOn ? intensity : 0;
redMat.current.opacity = redOn ? 1.0 : 0.3;
redMat.current.color.setHex(redOn ? 0xff0000 : 0x550000);
}
if (yelMat.current) {
yelMat.current.emissiveIntensity = yellowOn ? intensity : 0;
yelMat.current.opacity = yellowOn ? 1.0 : 0.3;
yelMat.current.color.setHex(yellowOn ? 0xffff00 : 0x555500);
}
if (grnMat.current) {
grnMat.current.emissiveIntensity = greenOn ? intensity : 0;
grnMat.current.opacity = greenOn ? 1.0 : 0.3;
grnMat.current.color.setHex(greenOn ? 0x00ff00 : 0x005500);
}
});
return (
<group position={[5.2, 6.8, -1.3]}>
<mesh position={[0, 0, 0]}>
<cylinderGeometry args={[0.05, 0.05, 0.5]} />
<meshStandardMaterial color="#333" roughness={0.5} />
</mesh>
<mesh position={[0, 0.3, 0]}>
<cylinderGeometry args={[0.08, 0.08, 0.15]} />
<meshStandardMaterial ref={grnMat} color="#00ff00" emissive="#00ff00" transparent toneMapped={false} />
</mesh>
<mesh position={[0, 0.46, 0]}>
<cylinderGeometry args={[0.08, 0.08, 0.15]} />
<meshStandardMaterial ref={yelMat} color="#ffff00" emissive="#ffff00" transparent toneMapped={false} />
</mesh>
<mesh position={[0, 0.62, 0]}>
<cylinderGeometry args={[0.08, 0.08, 0.15]} />
<meshStandardMaterial ref={redMat} color="#ff0000" emissive="#ff0000" transparent toneMapped={false} />
</mesh>
</group>
);
};
// Main Machine Model
const MachineModel = ({ target, doorStates }: { target: RobotTarget, doorStates: Machine3DProps['doorStates'] }) => {
const FRAME_WIDTH = 10;
const TOTAL_HEIGHT = 6.5;
const DOCK_HEIGHT = 2.5;
const OPERATIONAL_HEIGHT = TOTAL_HEIGHT - DOCK_HEIGHT;
const FRAME_DEPTH = 3.0;
const CONVEYOR_Y = DOCK_HEIGHT + 0.2;
const LABELER_BASE_Y = CONVEYOR_Y;
const PICKER_Y = 4.8;
const minZLength = 0.6;
const zRailLength = Math.max(minZLength, target.z + 0.4);
const Z_XRAIL = -0.6;
const Z_CARRIAGE = -0.45;
const Z_ZRAIL = -0.3;
const Z_HEAD = 0;
// Convert target coordinates
const pickerX = target.x;
const pickerZ = target.z;
const leftLabelY = target.y;
const leftLabelZ = 0.5;
const rightLabelY = target.y;
const rightLabelZ = 0.5;
return (
<group>
{/* --- STRUCTURAL FRAME --- */}
<group>
<Profile position={[-FRAME_WIDTH/2, TOTAL_HEIGHT/2, -FRAME_DEPTH/2]} size={[0.2, TOTAL_HEIGHT, 0.2]} />
<Profile position={[FRAME_WIDTH/2, TOTAL_HEIGHT/2, -FRAME_DEPTH/2]} size={[0.2, TOTAL_HEIGHT, 0.2]} />
<Profile position={[-FRAME_WIDTH/2, TOTAL_HEIGHT/2, FRAME_DEPTH/2]} size={[0.2, TOTAL_HEIGHT, 0.2]} />
<Profile position={[FRAME_WIDTH/2, TOTAL_HEIGHT/2, FRAME_DEPTH/2]} size={[0.2, TOTAL_HEIGHT, 0.2]} />
<Profile position={[0, DOCK_HEIGHT, -FRAME_DEPTH/2]} size={[FRAME_WIDTH, 0.2, 0.2]} />
<Profile position={[0, DOCK_HEIGHT, FRAME_DEPTH/2]} size={[FRAME_WIDTH, 0.2, 0.2]} />
<Profile position={[-FRAME_WIDTH/2, DOCK_HEIGHT, 0]} size={[0.2, 0.2, FRAME_DEPTH]} />
<Profile position={[FRAME_WIDTH/2, DOCK_HEIGHT, 0]} size={[0.2, 0.2, FRAME_DEPTH]} />
<Profile position={[0, TOTAL_HEIGHT, -FRAME_DEPTH/2]} size={[FRAME_WIDTH, 0.2, 0.2]} />
<Profile position={[0, TOTAL_HEIGHT, FRAME_DEPTH/2]} size={[FRAME_WIDTH, 0.2, 0.2]} />
<Profile position={[-FRAME_WIDTH/2, TOTAL_HEIGHT, 0]} size={[0.2, 0.2, FRAME_DEPTH]} />
<Profile position={[FRAME_WIDTH/2, TOTAL_HEIGHT, 0]} size={[0.2, 0.2, FRAME_DEPTH]} />
<group position={[0, PICKER_Y + 0.4, Z_XRAIL]}>
<RobotRail length={FRAME_WIDTH - 0.4} orientation="horizontal" profile={[0.6, 0.2]} />
</group>
</group>
{/* --- DOCKING AREA (LOWER LEVEL) --- */}
<group position={[0, 0, 0]}>
<Profile position={[-1.75, DOCK_HEIGHT/2, 0]} size={[0.1, DOCK_HEIGHT, FRAME_DEPTH]} />
<Profile position={[1.75, DOCK_HEIGHT/2, 0]} size={[0.1, DOCK_HEIGHT, FRAME_DEPTH]} />
{[-3.5, 0, 3.5].map((xPos, idx) => (
<group key={idx} position={[xPos, 0.02, 0]}>
<Plane args={[2.2, FRAME_DEPTH - 0.5]} rotation={[-Math.PI/2, 0, 0]} position={[0, 0, 0]}>
<meshStandardMaterial color="#0f172a" />
</Plane>
<Box args={[0.1, 0.05, FRAME_DEPTH - 0.5]} position={[-1, 0, 0]}><meshStandardMaterial color="yellow" /></Box>
<Box args={[0.1, 0.05, FRAME_DEPTH - 0.5]} position={[1, 0, 0]}><meshStandardMaterial color="yellow" /></Box>
<Text position={[0, 0.1, FRAME_DEPTH/2 - 0.5]} fontSize={0.3} rotation={[-Math.PI/2, 0, 0]} color="white">
{idx === 0 ? "SPARE L" : idx === 1 ? "LOADING" : "SPARE R"}
</Text>
</group>
))}
<Cart position={[-3.5, 0, 0]} label="Cart A" />
<Cart position={[0, 0, 0]} label="Cart B" />
<Cart position={[3.5, 0, 0]} label="Cart C" />
</group>
{/* --- DOORS (UPPER LEVEL ONLY) --- */}
<group position={[0, DOCK_HEIGHT + OPERATIONAL_HEIGHT/2, FRAME_DEPTH/2]}>
<Door position={[-3.3, 0, 0]} size={[3.3, OPERATIONAL_HEIGHT, 0.1]} isOpen={doorStates.front || doorStates.left} hingeDirection={-1} />
<Door position={[0, 0, 0]} size={[3.3, OPERATIONAL_HEIGHT, 0.1]} isOpen={doorStates.front} hingeDirection={1} />
<Door position={[3.3, 0, 0]} size={[3.3, OPERATIONAL_HEIGHT, 0.1]} isOpen={doorStates.front || doorStates.right} hingeDirection={1} />
</group>
{/* --- TOP COVER (ROOF) --- */}
<group position={[0, TOTAL_HEIGHT, 0]}>
<Box args={[FRAME_WIDTH, 0.1, FRAME_DEPTH]}>
<meshPhysicalMaterial
color="#ffffff"
transmission={0.99}
opacity={0.1}
transparent={true}
roughness={0}
metalness={0.1}
clearcoat={1}
thickness={0.02}
side={THREE.DoubleSide}
depthWrite={false}
/>
</Box>
<Box args={[FRAME_WIDTH, 0.12, 0.1]} position={[0, 0, FRAME_DEPTH/2]}><meshStandardMaterial color="#334155" /></Box>
<Box args={[FRAME_WIDTH, 0.12, 0.1]} position={[0, 0, -FRAME_DEPTH/2]}><meshStandardMaterial color="#334155" /></Box>
<Box args={[0.1, 0.12, FRAME_DEPTH]} position={[FRAME_WIDTH/2, 0, 0]}><meshStandardMaterial color="#334155" /></Box>
<Box args={[0.1, 0.12, FRAME_DEPTH]} position={[-FRAME_WIDTH/2, 0, 0]}><meshStandardMaterial color="#334155" /></Box>
</group>
{/* --- CONVEYORS (Left & Right - UPPER LEVEL) --- */}
<group position={[-3.5, CONVEYOR_Y, 0]}>
<Box args={[2.5, 0.2, 3.0]} receiveShadow>
<meshStandardMaterial color="#334155" />
</Box>
<Box args={[2.3, 0.05, 3.0]} position={[0, 0.11, 0]}>
<meshStandardMaterial color="#020617" roughness={0.8} />
</Box>
<Text position={[0, 0.5, 1.5]} fontSize={0.3} color="white" rotation={[-Math.PI/4, 0, 0]}>Left Conveyor</Text>
<Cylinder args={[0.6, 0.6, 0.1, 32]} position={[0, 0.16, 0]}>
<meshStandardMaterial color="#38bdf8" transparent opacity={0.3} />
</Cylinder>
<Text position={[0, 0.3, 0]} fontSize={0.2} color="#38bdf8" rotation={[-Math.PI/2, 0, 0]}>PLACE REEL</Text>
</group>
<group position={[3.5, CONVEYOR_Y, 0]}>
<Box args={[2.5, 0.2, 3.0]} receiveShadow>
<meshStandardMaterial color="#334155" />
</Box>
<Box args={[2.3, 0.05, 3.0]} position={[0, 0.11, 0]}>
<meshStandardMaterial color="#020617" roughness={0.8} />
</Box>
<Text position={[0, 0.5, 1.5]} fontSize={0.3} color="white" rotation={[-Math.PI/4, 0, 0]}>Right Conveyor</Text>
<Cylinder args={[0.6, 0.6, 0.1, 32]} position={[0, 0.16, 0]}>
<meshStandardMaterial color="#38bdf8" transparent opacity={0.3} />
</Cylinder>
<Text position={[0, 0.3, 0]} fontSize={0.2} color="#38bdf8" rotation={[-Math.PI/2, 0, 0]}>PLACE REEL</Text>
</group>
{/* --- MAIN PICKER (Robot Design) --- */}
<group position={[pickerX, PICKER_Y, 0]}>
<group position={[0, 0.4, Z_CARRIAGE]}>
<RoundedBox args={[0.4, 0.7, 0.1]} radius={0.02}>
<meshStandardMaterial color="#ef4444" />
</RoundedBox>
<Box args={[0.3, 0.15, 0.2]} position={[0, 0.4, 0.1]}>
<meshStandardMaterial color="#1e1e1e" />
</Box>
</group>
<group position={[0, -zRailLength/2 + 0.5, Z_ZRAIL]}>
<RobotRail length={zRailLength} orientation="vertical" profile={[0.2, 0.15]} />
</group>
<group position={[0, -pickerZ, Z_HEAD]}>
<Box args={[0.2, 0.15, 0.3]} position={[0, 0, -0.15]}>
<meshStandardMaterial color="#333" />
</Box>
<RoundedBox args={[0.5, 0.2, 0.4]} radius={0.02} position={[0, -0.1, 0]}>
<meshStandardMaterial color="#ef4444" />
</RoundedBox>
<group rotation={[0, 0, 0]} position={[0, -0.2, 0]}>
<Cylinder args={[0.25, 0.25, 0.2, 32]}>
<meshStandardMaterial color="#111" />
</Cylinder>
<Cylinder args={[0.5, 0.5, 0.05, 32]} position={[0, -0.12, 0]}>
<meshStandardMaterial color="#cbd5e1" metalness={0.6} roughness={0.2} />
</Cylinder>
{Array.from({ length: 8 }).map((_, i) => {
const angle = (i / 8) * Math.PI * 2;
const radius = 0.4;
const x = Math.cos(angle) * radius;
const z = Math.sin(angle) * radius;
return (
<group key={i} position={[x, -0.15, z]}>
<Cylinder args={[0.02, 0.02, 0.1, 8]} position={[0, 0.03, 0]}>
<meshStandardMaterial color="#64748b" />
</Cylinder>
<Cylinder args={[0.06, 0.04, 0.04, 16]} position={[0, -0.04, 0]}>
<meshStandardMaterial color="#fca5a5" transparent opacity={0.9} />
</Cylinder>
</group>
);
})}
</group>
</group>
</group>
{/* --- LABELER UNITS --- */}
<group position={[-3.5, LABELER_BASE_Y, 0]}>
<LabelerUnit side="left" posY={leftLabelY} posZ={leftLabelZ} />
</group>
<group position={[3.5, LABELER_BASE_Y, 0]}>
<LabelerUnit side="right" posY={rightLabelY} posZ={rightLabelZ} />
</group>
{/* --- CAMERAS (Roof Mounted) --- */}
<Profile position={[-3.5, TOTAL_HEIGHT - 0.2, 0]} size={[0.2, 0.4, 0.2]} />
<Profile position={[3.5, TOTAL_HEIGHT - 0.2, 0]} size={[0.2, 0.4, 0.2]} />
<IndustrialCamera position={[-3.5, 5.5, 0]} rotation={[Math.PI, 0, 0]} isActive={false} />
<IndustrialCamera position={[3.5, 5.5, 0]} rotation={[Math.PI, 0, 0]} isActive={false} />
{/* --- REAR DISCHARGE CHUTES --- */}
<Box position={[-3.5, CONVEYOR_Y, -FRAME_DEPTH/2 - 0.5]} args={[2.5, 0.2, 1]}>
<meshStandardMaterial color="#334155" />
</Box>
<Box position={[3.5, CONVEYOR_Y, -FRAME_DEPTH/2 - 0.5]} args={[2.5, 0.2, 1]}>
<meshStandardMaterial color="#334155" />
</Box>
{/* Grid Floor */}
<Grid infiniteGrid fadeDistance={25} sectionColor="#475569" cellColor="#1e293b" />
</group>
);
};
export const Machine3D: React.FC<Machine3DProps> = ({ target, ioState, doorStates }) => {
return (
<Canvas shadows className="w-full h-full bg-slate-900" orthographic>
{/* 정면에서 바라보는 직교 카메라 (왜곡 없음) */}
<OrthographicCamera
makeDefault
position={[0, 3.5, 20]} // 정면에서 바라봄
zoom={80} // 줌 레벨 (클수록 가까이)
near={0.1}
far={100}
/>
<OrbitControls
maxPolarAngle={Math.PI}
minPolarAngle={0}
minZoom={30}
maxZoom={200}
target={[0, 3.5, 0]}
enableRotate={true}
/>
<Environment preset="city" />
<ambientLight intensity={0.5} />
<directionalLight position={[10, 15, 10]} intensity={1} castShadow />
<directionalLight position={[-10, 10, -10]} intensity={0.3} />
<pointLight position={[0, 5, 5]} intensity={0.3} color="#bae6fd" distance={15} />
<MachineModel target={target} doorStates={doorStates} />
<TowerLamp ioState={ioState} />
</Canvas>
);
};