/** * One machine, built from primitives. * * Geometry is boxes and cylinders on purpose. Sourcing or modelling CAD-accurate * machines is a day of work that adds nothing to the argument being made, and it * is the classic way a two-day demo becomes a four-day demo. * * What the geometry DOES carry is state, three ways: a status-coloured status bar * on the machine, a pulse when it is stopped on a fault, and - the detail that * lands with maintenance engineers - a physical shake on CNC-02 that scales with * measured bearing vibration. */ import { useMemo, useRef } from 'react'; import { useFrame } from '@react-three/fiber'; import { Html } from '@react-three/drei'; import { stateMeta } from '../lib/format.js'; function Body({ w, h, d, y, color, roughness = 0.6, metalness = 0.35 }) { return ( ); } /** Per-kind machine shape. */ function Machine({ kind, station, spinRef }) { const shell = '#4a4941'; const dark = '#2f2e28'; switch (kind) { case 'conveyor': return ( {[-0.5, 0, 0.5].map((x) => ( ))} ); case 'cnc': return ( {/* enclosure window */} {/* spindle, rotates while cutting */} ); case 'oven': { // Zone glow follows burner duty, so a saturated burner visibly runs hot. const duty = (station.signals.burnerDuty || 0) / 100; return ( {/* tunnel mouth */} {/* three zone strips on the roof */} {[-0.75, 0, 0.75].map((x, i) => ( ))} ); } case 'inspection': return ( {/* gantry */} {[-0.6, 0.6].map((x) => ( ))} {/* camera + light cone */} ); case 'packer': return ( {/* film roll */} ); default: return ; } } export function Station({ station, spec, x, selected, onSelect }) { const group = useRef(null); const spinRef = useRef(null); const barRef = useRef(null); const sm = stateMeta(station.state); const stopped = station.state === 'fault' || station.state === 'down'; // Shake amplitude from measured vibration, above the healthy baseline. const shake = useMemo(() => { if (station.kind !== 'cnc') return 0; const v = station.signals.vibration || 0; return Math.max(0, v - 2.0) * 0.011; }, [station.kind, station.signals.vibration]); const running = station.state === 'running'; useFrame((state, dt) => { const t = state.clock.elapsedTime; if (group.current) { group.current.position.x = x + (shake ? Math.sin(t * 47) * shake : 0); group.current.position.z = shake ? Math.cos(t * 61) * shake : 0; } if (spinRef.current && running) { spinRef.current.rotation.y += dt * 14; } // A stopped station pulses, so it draws the eye from across the room. if (barRef.current) { const mat = barRef.current.material; mat.emissiveIntensity = stopped ? 0.7 + Math.sin(t * 5) * 0.6 : 0.65; } }); return ( { e.stopPropagation(); onSelect(station.id); }}> {/* status bar: the machine's own state light */} {/* selection ring */} {selected && ( )} {/* Labels are DOM, not 3D text: drei's fetches a font from a CDN, which would fail in an offline demo room. */}
{station.id}
{station.online ? sm.label : 'No data'}
); }