Initial commit with Dockerfile and demo code

This commit is contained in:
2026-08-28 18:16:30 +05:30
commit cf2575fd4e
53 changed files with 11602 additions and 0 deletions
+226
View File
@@ -0,0 +1,226 @@
/**
* 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 (
<mesh position={[0, y, 0]} castShadow receiveShadow>
<boxGeometry args={[w, h, d]} />
<meshStandardMaterial color={color} roughness={roughness} metalness={metalness} />
</mesh>
);
}
/** Per-kind machine shape. */
function Machine({ kind, station, spinRef }) {
const shell = '#4a4941';
const dark = '#2f2e28';
switch (kind) {
case 'conveyor':
return (
<group>
<Body w={1.9} h={0.5} d={1.3} y={0.25} color={shell} />
{[-0.5, 0, 0.5].map((x) => (
<mesh key={x} position={[x, 0.56, 0]} rotation={[0, 0, Math.PI / 2]}>
<cylinderGeometry args={[0.09, 0.09, 1.15, 14]} />
<meshStandardMaterial color={dark} roughness={0.5} metalness={0.6} />
</mesh>
))}
</group>
);
case 'cnc':
return (
<group>
<Body w={2.0} h={1.5} d={1.5} y={0.75} color={shell} />
{/* enclosure window */}
<mesh position={[0, 0.95, 0.77]}>
<boxGeometry args={[1.3, 0.6, 0.03]} />
<meshStandardMaterial color="#1b2a33" roughness={0.2} metalness={0.5} />
</mesh>
{/* spindle, rotates while cutting */}
<group ref={spinRef} position={[0, 1.62, 0]}>
<mesh castShadow>
<cylinderGeometry args={[0.17, 0.17, 0.42, 18]} />
<meshStandardMaterial color="#6d6a5e" roughness={0.35} metalness={0.8} />
</mesh>
<mesh position={[0, -0.3, 0]}>
<cylinderGeometry args={[0.05, 0.05, 0.24, 12]} />
<meshStandardMaterial color="#9a978c" roughness={0.25} metalness={0.9} />
</mesh>
</group>
</group>
);
case 'oven': {
// Zone glow follows burner duty, so a saturated burner visibly runs hot.
const duty = (station.signals.burnerDuty || 0) / 100;
return (
<group>
<Body w={2.6} h={1.25} d={1.5} y={0.62} color="#514f45" />
{/* tunnel mouth */}
<mesh position={[0, 0.62, 0.76]}>
<boxGeometry args={[2.1, 0.42, 0.04]} />
<meshStandardMaterial color="#120e0b" roughness={0.9} />
</mesh>
{/* three zone strips on the roof */}
{[-0.75, 0, 0.75].map((x, i) => (
<mesh key={x} position={[x, 1.27, 0]}>
<boxGeometry args={[0.62, 0.06, 1.2]} />
<meshStandardMaterial
color="#8a3a10"
emissive="#ff6a1a"
emissiveIntensity={0.15 + duty * 1.5}
roughness={0.6}
/>
</mesh>
))}
</group>
);
}
case 'inspection':
return (
<group>
<Body w={1.7} h={0.45} d={1.3} y={0.22} color={shell} />
{/* gantry */}
{[-0.6, 0.6].map((x) => (
<mesh key={x} position={[x, 1.05, 0]}>
<boxGeometry args={[0.11, 1.25, 0.11]} />
<meshStandardMaterial color={dark} roughness={0.5} metalness={0.6} />
</mesh>
))}
<mesh position={[0, 1.7, 0]}>
<boxGeometry args={[1.35, 0.12, 0.3]} />
<meshStandardMaterial color={dark} roughness={0.5} metalness={0.6} />
</mesh>
{/* camera + light cone */}
<mesh position={[0, 1.5, 0]}>
<cylinderGeometry args={[0.11, 0.11, 0.24, 14]} />
<meshStandardMaterial color="#23221d" roughness={0.4} metalness={0.7} />
</mesh>
<mesh position={[0, 1.05, 0]}>
<coneGeometry args={[0.34, 0.72, 20, 1, true]} />
<meshStandardMaterial
color="#7ec8ff"
emissive="#4aa8ff"
emissiveIntensity={station.online ? 0.5 : 0}
transparent
opacity={station.online ? 0.16 : 0.03}
side={2}
/>
</mesh>
</group>
);
case 'packer':
return (
<group>
<Body w={1.9} h={1.15} d={1.4} y={0.57} color={shell} />
{/* film roll */}
<mesh position={[0.55, 1.35, 0]} rotation={[Math.PI / 2, 0, 0]} castShadow>
<cylinderGeometry args={[0.28, 0.28, 0.85, 20]} />
<meshStandardMaterial color="#b9b2a2" roughness={0.85} />
</mesh>
<mesh position={[-0.5, 1.22, 0]}>
<boxGeometry args={[0.75, 0.14, 1.0]} />
<meshStandardMaterial color={dark} roughness={0.55} metalness={0.5} />
</mesh>
</group>
);
default:
return <Body w={1.8} h={1} d={1.3} y={0.5} color={shell} />;
}
}
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 (
<group ref={group} position={[x, 0, 0]}>
<group onClick={(e) => { e.stopPropagation(); onSelect(station.id); }}>
<Machine kind={station.kind} station={station} spinRef={spinRef} />
{/* status bar: the machine's own state light */}
<mesh ref={barRef} position={[0, 0.06, 0.8]}>
<boxGeometry args={[1.5, 0.09, 0.09]} />
<meshStandardMaterial color={sm.color} emissive={sm.color} emissiveIntensity={0.65} />
</mesh>
{/* selection ring */}
{selected && (
<mesh position={[0, 0.02, 0]} rotation={[-Math.PI / 2, 0, 0]}>
<ringGeometry args={[1.35, 1.5, 48]} />
<meshBasicMaterial color="#ffffff" transparent opacity={0.5} side={2} />
</mesh>
)}
</group>
{/* Labels are DOM, not 3D text: drei's <Text> fetches a font from a CDN,
which would fail in an offline demo room. */}
<Html position={[0, 2.32, 0]} center distanceFactor={12} zIndexRange={[10, 0]} pointerEvents="none">
<div style={{
textAlign: 'center', whiteSpace: 'nowrap', userSelect: 'none',
fontFamily: 'system-ui, sans-serif',
}}>
<div style={{
fontSize: 13, fontWeight: 700, letterSpacing: '0.03em',
color: selected ? '#ffffff' : '#d8d6cc',
textShadow: '0 1px 4px rgba(0,0,0,0.9)',
}}>{station.id}</div>
<div style={{
fontSize: 11, fontWeight: 600, color: sm.color,
textShadow: '0 1px 4px rgba(0,0,0,0.9)',
}}>{station.online ? sm.label : 'No data'}</div>
</div>
</Html>
</group>
);
}