Initial commit with Dockerfile and demo code
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
/**
|
||||
* The 3D hero view of the line.
|
||||
*
|
||||
* No external assets are loaded anywhere in this scene - no HDRI environment, no
|
||||
* CDN font, no texture. Everything is primitives and analytic lights, because the
|
||||
* demo has to survive a room with no working wifi.
|
||||
*/
|
||||
|
||||
import { Suspense } from 'react';
|
||||
import { Canvas } from '@react-three/fiber';
|
||||
import { OrbitControls, ContactShadows } from '@react-three/drei';
|
||||
import { useStore } from '../store.js';
|
||||
import { Station } from './Station.jsx';
|
||||
import { Belt, QueueBoxes, FlowBoxes } from './Conveyor.jsx';
|
||||
import { stateMeta } from '../lib/format.js';
|
||||
|
||||
/** Station centres, evenly spaced along the line. */
|
||||
const SPACING = 4.3;
|
||||
const stationX = (i, n) => (i - (n - 1) / 2) * SPACING;
|
||||
|
||||
function Floor() {
|
||||
return (
|
||||
<>
|
||||
<mesh position={[0, -0.02, 0]} rotation={[-Math.PI / 2, 0, 0]} receiveShadow>
|
||||
<planeGeometry args={[60, 26]} />
|
||||
<meshStandardMaterial color="#161513" roughness={0.95} metalness={0} />
|
||||
</mesh>
|
||||
{/* aisle markings, purely to give the eye a ground plane to read depth from */}
|
||||
{[-3.2, 3.2].map((z) => (
|
||||
<mesh key={z} position={[0, 0, z]} rotation={[-Math.PI / 2, 0, 0]}>
|
||||
<planeGeometry args={[26, 0.06]} />
|
||||
<meshBasicMaterial color="#3a3830" />
|
||||
</mesh>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function LineContent() {
|
||||
const frame = useStore((s) => s.frame);
|
||||
const meta = useStore((s) => s.meta);
|
||||
const selected = useStore((s) => s.selected);
|
||||
const select = useStore((s) => s.select);
|
||||
|
||||
if (!frame || !meta) return null;
|
||||
const n = frame.stations.length;
|
||||
|
||||
return (
|
||||
<group>
|
||||
{frame.stations.map((st, i) => (
|
||||
<Station
|
||||
key={st.id}
|
||||
station={st}
|
||||
spec={meta.stations.find((x) => x.id === st.id)}
|
||||
x={stationX(i, n)}
|
||||
selected={selected === st.id}
|
||||
onSelect={select}
|
||||
/>
|
||||
))}
|
||||
|
||||
{frame.buffers.map((level, i) => {
|
||||
const x = (stationX(i, n) + stationX(i + 1, n)) / 2;
|
||||
const width = SPACING - 2.15;
|
||||
const upstream = frame.stations[i];
|
||||
return (
|
||||
<group key={i}>
|
||||
<Belt x={x} width={width} />
|
||||
<QueueBoxes
|
||||
x={x}
|
||||
width={width}
|
||||
count={level}
|
||||
full={level >= frame.bufferCapacity}
|
||||
/>
|
||||
<FlowBoxes
|
||||
x={x}
|
||||
width={width}
|
||||
active={upstream.state === 'running'}
|
||||
rate={frame.controls.lineSpeedPct / 100}
|
||||
/>
|
||||
</group>
|
||||
);
|
||||
})}
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
export function Scene() {
|
||||
const frame = useStore((s) => s.frame);
|
||||
const connected = useStore((s) => s.connected);
|
||||
|
||||
const worstState = frame
|
||||
? (frame.stations.find((s) => s.state === 'fault' || s.state === 'down')
|
||||
|| frame.stations.find((s) => s.state === 'blocked'))
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div className="scene">
|
||||
<Canvas
|
||||
shadows
|
||||
dpr={[1, 1.75]}
|
||||
camera={{ position: [11, 8.5, 13], fov: 40 }}
|
||||
gl={{ antialias: true }}
|
||||
>
|
||||
<color attach="background" args={['#121211']} />
|
||||
<fog attach="fog" args={['#121211', 26, 52]} />
|
||||
|
||||
{/* Analytic lights only - an HDRI would be a network fetch. */}
|
||||
<hemisphereLight args={['#8fa4b8', '#2a2620', 0.55]} />
|
||||
<directionalLight
|
||||
position={[9, 14, 8]}
|
||||
intensity={1.7}
|
||||
castShadow
|
||||
shadow-mapSize={[1024, 1024]}
|
||||
shadow-camera-left={-16}
|
||||
shadow-camera-right={16}
|
||||
shadow-camera-top={12}
|
||||
shadow-camera-bottom={-12}
|
||||
/>
|
||||
<directionalLight position={[-10, 6, -8]} intensity={0.45} color="#7fa8d0" />
|
||||
|
||||
<Suspense fallback={null}>
|
||||
<Floor />
|
||||
<LineContent />
|
||||
<ContactShadows
|
||||
position={[0, 0.01, 0]}
|
||||
opacity={0.5}
|
||||
scale={40}
|
||||
blur={2.4}
|
||||
far={5}
|
||||
resolution={512}
|
||||
color="#000000"
|
||||
/>
|
||||
</Suspense>
|
||||
|
||||
<OrbitControls
|
||||
target={[0, 1.1, 0]}
|
||||
enablePan
|
||||
minDistance={7}
|
||||
maxDistance={34}
|
||||
minPolarAngle={0.15}
|
||||
maxPolarAngle={Math.PI / 2.15}
|
||||
/>
|
||||
</Canvas>
|
||||
|
||||
<div className="scene-overlay">
|
||||
<span className="ttl">LINE-1 · Assembly & Cure</span>
|
||||
<span className="sub">
|
||||
{connected ? 'Live twin' : 'Disconnected'}
|
||||
{worstState ? ` · ${worstState.id} ${stateMeta(worstState.state).label.toLowerCase()}` : ' · all stations nominal'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Legend pairs every status colour with its word. */}
|
||||
<div className="scene-legend">
|
||||
{['running', 'microstop', 'blocked', 'fault'].map((s) => {
|
||||
const sm = stateMeta(s);
|
||||
return (
|
||||
<span className="legend-item" key={s}>
|
||||
<span className="dot" style={{
|
||||
width: 7, height: 7, borderRadius: '50%', background: sm.color, display: 'inline-block',
|
||||
}} />
|
||||
{sm.label}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="scene-hint">Drag to orbit · scroll to zoom · click a machine to inspect</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user