feat: align 3D digital twin with OZON Track 3 requirements
This commit is contained in:
@@ -37,10 +37,10 @@ const PRODUCT_CRITERIA: CriteriaCard[] = [
|
||||
{
|
||||
id: 'rule-priority',
|
||||
title: 'Приоритет правил',
|
||||
evidence: 'Сначала габариты (C), затем форма (D), иначе основной поток B.',
|
||||
evidence: 'Сначала габариты (C), затем форма (D). Негабарит + круглый → только C.',
|
||||
status: 'covered',
|
||||
linkedScenario: 'boundary_dimensions',
|
||||
linkedLabel: 'Пограничные размеры',
|
||||
linkedScenario: 'c_priority',
|
||||
linkedLabel: 'C-priority',
|
||||
},
|
||||
{
|
||||
id: 'routing',
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { DIMENSION_LIMITS } from '../domain/classifier';
|
||||
import { NOMINAL_CONVEYOR_SPEED_MPS } from '../domain/simulation';
|
||||
import type { SimulationState } from '../domain/types';
|
||||
|
||||
export default function CurrentProofCard({ simulation }: { simulation: SimulationState }) {
|
||||
@@ -12,6 +14,7 @@ export default function CurrentProofCard({ simulation }: { simulation: Simulatio
|
||||
: result
|
||||
? `ROUTE_TO_${result.category}`
|
||||
: 'WAITING';
|
||||
const isCPriority = Boolean(result && !result.dimensionsPass && !result.roundnessPass);
|
||||
|
||||
if (!current) {
|
||||
return (
|
||||
@@ -20,6 +23,11 @@ export default function CurrentProofCard({ simulation }: { simulation: Simulatio
|
||||
<p className="eyebrow">Ожидание цикла</p>
|
||||
<h3>Готово к демонстрации</h3>
|
||||
<p>Нажмите Start demo — товар пройдёт Detection → Classification → Command → Routing.</p>
|
||||
<p className="proof-limits">
|
||||
Limits: min {DIMENSION_LIMITS.min.width}×{DIMENSION_LIMITS.min.depth}×{DIMENSION_LIMITS.min.height} mm ·
|
||||
max {DIMENSION_LIMITS.max.width}×{DIMENSION_LIMITS.max.depth}×{DIMENSION_LIMITS.max.height} mm ·
|
||||
conveyor {NOMINAL_CONVEYOR_SPEED_MPS.toFixed(2)} m/s
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -34,38 +42,57 @@ export default function CurrentProofCard({ simulation }: { simulation: Simulatio
|
||||
|
||||
<div className="proof-grid">
|
||||
<div className="proof-item">
|
||||
<span className="proof-label">Категория</span>
|
||||
<span className="proof-label">Dimensions</span>
|
||||
<strong className={result?.dimensionsPass === false ? 'fail-text' : 'pass-text'}>
|
||||
{result?.dimensionsPass ? 'PASS' : 'FAIL'}
|
||||
</strong>
|
||||
<em>
|
||||
{dimensions?.width}×{dimensions?.depth}×{dimensions?.height} мм
|
||||
</em>
|
||||
</div>
|
||||
<div className="proof-item">
|
||||
<span className="proof-label">Roundness K</span>
|
||||
<strong className={result?.roundnessPass === false && result?.dimensionsPass ? 'fail-text' : 'pass-text'}>
|
||||
{item?.roundness.toFixed(2)}
|
||||
</strong>
|
||||
<em>{result?.roundnessPass ? 'PASS' : 'DETECTED (≥0.8)'}</em>
|
||||
</div>
|
||||
<div className="proof-item">
|
||||
<span className="proof-label">Category</span>
|
||||
<strong className={`category-${result?.category ?? 'default'}`}>{result?.category ?? '—'}</strong>
|
||||
</div>
|
||||
<div className="proof-item">
|
||||
<span className="proof-label">Зона</span>
|
||||
<strong>{result?.category ? `Зона ${result.category}` : '—'}</strong>
|
||||
<span className="proof-label">Target zone</span>
|
||||
<strong>{result?.category ? `Zone ${result.category}` : '—'}</strong>
|
||||
</div>
|
||||
<div className="proof-item proof-item-wide">
|
||||
<span className="proof-label">Команда</span>
|
||||
<span className="proof-label">Command</span>
|
||||
<strong>{command}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="proof-details">
|
||||
<div className="detail-row">
|
||||
<span>Габариты</span>
|
||||
<strong className={result?.dimensionsPass === false ? 'fail-text' : ''}>
|
||||
{dimensions?.width} × {dimensions?.depth} × {dimensions?.height} мм
|
||||
<span>Min limits</span>
|
||||
<strong>
|
||||
{DIMENSION_LIMITS.min.width}×{DIMENSION_LIMITS.min.depth}×{DIMENSION_LIMITS.min.height} мм
|
||||
</strong>
|
||||
</div>
|
||||
<div className="detail-row">
|
||||
<span>Roundness</span>
|
||||
<strong className={result?.roundnessPass === false && result?.dimensionsPass ? 'fail-text' : ''}>
|
||||
{item?.roundness.toFixed(2)}
|
||||
</strong>
|
||||
<span>Conveyor target</span>
|
||||
<strong>{NOMINAL_CONVEYOR_SPEED_MPS.toFixed(2)} m/s</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="proof-reason">
|
||||
<strong>Причина решения</strong>
|
||||
<p>{result?.reason}</p>
|
||||
{lowConfidence ? <p className="warning-note">Низкая уверенность CV — rule-based fallback</p> : null}
|
||||
{isCPriority ? (
|
||||
<p className="warning-note">C-priority: негабарит + круглый → только C (габариты важнее формы)</p>
|
||||
) : null}
|
||||
{lowConfidence ? (
|
||||
<p className="warning-note">Низкая уверенность CV — rule-based fallback, класс только B/C/D</p>
|
||||
) : null}
|
||||
{simulation.machineState === 'FAULT' ? <p className="error-note">FAULT: конвейер остановлен</p> : null}
|
||||
{simulation.machineState === 'EMERGENCY_STOP' ? (
|
||||
<p className="error-note">EMERGENCY_STOP: аварийная остановка</p>
|
||||
|
||||
@@ -129,16 +129,26 @@ export default function ProductDemoSection({
|
||||
<strong>{item?.item.name ?? 'Ожидание'}</strong>
|
||||
</div>
|
||||
<div className="summary-item">
|
||||
<span>Категория</span>
|
||||
<span>Dimensions</span>
|
||||
<strong className={item?.classification.dimensionsPass === false ? 'fail-text' : 'pass-text'}>
|
||||
{item ? (item.classification.dimensionsPass ? 'PASS' : 'FAIL') : '—'}
|
||||
</strong>
|
||||
</div>
|
||||
<div className="summary-item">
|
||||
<span>Roundness K</span>
|
||||
<strong>{item ? item.item.roundness.toFixed(2) : '—'}</strong>
|
||||
</div>
|
||||
<div className="summary-item">
|
||||
<span>Category</span>
|
||||
<strong className={category ? `category-${category}` : ''}>{category ?? '—'}</strong>
|
||||
</div>
|
||||
<div className="summary-item">
|
||||
<span>Команда</span>
|
||||
<span>Command</span>
|
||||
<strong>{command}</strong>
|
||||
</div>
|
||||
<div className="summary-item">
|
||||
<span>Зона</span>
|
||||
<strong>{category ? `Зона ${category}` : '—'}</strong>
|
||||
<span>Target zone</span>
|
||||
<strong>{category ? `Zone ${category}` : '—'}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -28,6 +28,12 @@ const SCENARIO_UI: Record<
|
||||
badge: 'Зона D',
|
||||
badgeTone: 'd',
|
||||
},
|
||||
c_priority: {
|
||||
title: 'C-priority',
|
||||
summary: 'Негабарит + круглый: габариты важнее формы, маршрут только в C.',
|
||||
badge: 'C priority',
|
||||
badgeTone: 'c',
|
||||
},
|
||||
boundary_dimensions: {
|
||||
title: 'Пограничные размеры',
|
||||
summary: 'Проверка строгих min/max границ габаритов.',
|
||||
|
||||
@@ -7,6 +7,7 @@ interface Props {
|
||||
export default function Conveyor3D({ stopped }: Props) {
|
||||
const color = stopped ? '#7f1d1d' : '#1e3a54';
|
||||
const beltColor = stopped ? '#991b1b' : '#334155';
|
||||
const accColor = stopped ? '#7f1d1d' : '#0f766e';
|
||||
|
||||
return (
|
||||
<group>
|
||||
@@ -22,11 +23,26 @@ export default function Conveyor3D({ stopped }: Props) {
|
||||
<meshStandardMaterial color={beltColor} roughness={0.9} />
|
||||
</mesh>
|
||||
|
||||
{/* Accumulator / buffer at end of feed conveyor */}
|
||||
<mesh position={[TWIN_LAYOUT.accumulatorX, TWIN_LAYOUT.beltY - 0.05, 0]}>
|
||||
<boxGeometry args={[0.9, 0.28, 0.7]} />
|
||||
<meshStandardMaterial color={stopped ? '#7f1d1d' : '#0f766e'} roughness={0.7} />
|
||||
</mesh>
|
||||
{/* Accumulator / buffer at end of feed conveyor — explicit raised pocket */}
|
||||
<group position={[TWIN_LAYOUT.accumulatorX, TWIN_LAYOUT.beltY, 0]}>
|
||||
<mesh position={[0, -0.02, 0]}>
|
||||
<boxGeometry args={[1.05, 0.12, 0.82]} />
|
||||
<meshStandardMaterial color={accColor} roughness={0.65} emissive={accColor} emissiveIntensity={0.15} />
|
||||
</mesh>
|
||||
{/* Side walls of accumulator */}
|
||||
<mesh position={[0, 0.12, 0.38]}>
|
||||
<boxGeometry args={[1.05, 0.28, 0.06]} />
|
||||
<meshStandardMaterial color="#14b8a6" />
|
||||
</mesh>
|
||||
<mesh position={[0, 0.12, -0.38]}>
|
||||
<boxGeometry args={[1.05, 0.28, 0.06]} />
|
||||
<meshStandardMaterial color="#14b8a6" />
|
||||
</mesh>
|
||||
<mesh position={[-0.48, 0.12, 0]}>
|
||||
<boxGeometry args={[0.06, 0.28, 0.82]} />
|
||||
<meshStandardMaterial color="#14b8a6" />
|
||||
</mesh>
|
||||
</group>
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -29,13 +29,13 @@ function RouteBeam({
|
||||
|
||||
return (
|
||||
<mesh position={mid} rotation={[0, -angle, 0]}>
|
||||
<boxGeometry args={[length, 0.04, active ? 0.12 : 0.06]} />
|
||||
<boxGeometry args={[length, active ? 0.06 : 0.04, active ? 0.14 : 0.08]} />
|
||||
<meshStandardMaterial
|
||||
color={color}
|
||||
transparent
|
||||
opacity={active ? 0.95 : 0.25}
|
||||
opacity={active ? 1 : 0.45}
|
||||
emissive={color}
|
||||
emissiveIntensity={active ? 0.5 : 0.05}
|
||||
emissiveIntensity={active ? 0.55 : 0.18}
|
||||
/>
|
||||
</mesh>
|
||||
);
|
||||
@@ -44,12 +44,13 @@ function RouteBeam({
|
||||
export default function RouteArrows3D({ category, machineState }: Props) {
|
||||
const routing = machineState.startsWith('ROUTE_TO_');
|
||||
const classifying = machineState === 'CLASSIFYING';
|
||||
const preview = classifying || routing;
|
||||
const preview = classifying || routing || Boolean(category);
|
||||
const y = TWIN_LAYOUT.beltY + 0.05;
|
||||
const from: [number, number, number] = [TWIN_LAYOUT.gateX, y, 0];
|
||||
|
||||
return (
|
||||
<group>
|
||||
{/* Always draw B/C/D routes in distinct colors; active route is brighter */}
|
||||
<RouteBeam
|
||||
start={from}
|
||||
end={[TWIN_LAYOUT.zoneBX, y, 0]}
|
||||
|
||||
@@ -1,35 +1,40 @@
|
||||
import { Html } from '@react-three/drei';
|
||||
import type { Category, MachineState, SimulatedItem } from '../../domain/types';
|
||||
import { TWIN_LAYOUT } from './itemMotion';
|
||||
import { ROUTE_COLORS, TWIN_LAYOUT } from './itemMotion';
|
||||
|
||||
interface Props {
|
||||
machineState: MachineState;
|
||||
currentItem?: SimulatedItem;
|
||||
simplified?: boolean;
|
||||
conveyorTargetMps?: number;
|
||||
}
|
||||
|
||||
function Badge({
|
||||
position,
|
||||
text,
|
||||
color = '#e5f2ff',
|
||||
large = false,
|
||||
}: {
|
||||
position: [number, number, number];
|
||||
text: string;
|
||||
color?: string;
|
||||
large?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<Html position={position} center distanceFactor={10} style={{ pointerEvents: 'none' }}>
|
||||
<Html position={position} center distanceFactor={large ? 8 : 10} style={{ pointerEvents: 'none' }}>
|
||||
<div
|
||||
style={{
|
||||
padding: '4px 8px',
|
||||
borderRadius: 8,
|
||||
background: 'rgba(5, 9, 16, 0.82)',
|
||||
border: '1px solid rgba(56, 189, 248, 0.35)',
|
||||
padding: large ? '8px 12px' : '4px 8px',
|
||||
borderRadius: large ? 12 : 8,
|
||||
background: 'rgba(5, 9, 16, 0.9)',
|
||||
border: `1px solid ${color}`,
|
||||
color,
|
||||
fontSize: 11,
|
||||
fontWeight: 700,
|
||||
fontSize: large ? 15 : 12,
|
||||
fontWeight: 800,
|
||||
whiteSpace: 'nowrap',
|
||||
fontFamily: 'Inter, system-ui, sans-serif',
|
||||
letterSpacing: large ? '0.04em' : undefined,
|
||||
boxShadow: '0 8px 20px rgba(0,0,0,0.35)',
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
@@ -38,54 +43,144 @@ function Badge({
|
||||
);
|
||||
}
|
||||
|
||||
export default function SceneLabels3D({ machineState, currentItem, simplified }: Props) {
|
||||
const category = currentItem?.classification.category as Category | undefined;
|
||||
const item = currentItem?.item;
|
||||
const result = currentItem?.classification;
|
||||
function ItemProofPanel({
|
||||
currentItem,
|
||||
machineState,
|
||||
}: {
|
||||
currentItem: SimulatedItem;
|
||||
machineState: MachineState;
|
||||
}) {
|
||||
const item = currentItem.item;
|
||||
const result = currentItem.classification;
|
||||
const category = result.category as Category;
|
||||
const color = ROUTE_COLORS[category];
|
||||
const command = machineState.startsWith('ROUTE_TO_')
|
||||
? machineState
|
||||
: category
|
||||
? `ROUTE_TO_${category}`
|
||||
: null;
|
||||
: `ROUTE_TO_${category}`;
|
||||
const dims = item.dimensionsMm;
|
||||
const dimStatus = result.dimensionsPass ? 'PASS' : 'FAIL';
|
||||
const roundStatus = result.roundnessPass ? 'PASS' : 'DETECTED';
|
||||
|
||||
return (
|
||||
<Html position={[0, 2.05, 0]} center distanceFactor={9} style={{ pointerEvents: 'none' }}>
|
||||
<div
|
||||
style={{
|
||||
minWidth: 260,
|
||||
padding: '12px 14px',
|
||||
borderRadius: 14,
|
||||
background: 'rgba(5, 9, 16, 0.94)',
|
||||
border: `2px solid ${color}`,
|
||||
color: '#e5f2ff',
|
||||
fontFamily: 'Inter, system-ui, sans-serif',
|
||||
boxShadow: `0 0 24px ${color}44`,
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: 11, color: '#91a4b8', fontWeight: 800, letterSpacing: '0.08em', textTransform: 'uppercase' }}>
|
||||
Current item
|
||||
</div>
|
||||
<div style={{ fontSize: 16, fontWeight: 800, margin: '4px 0 10px' }}>{item.name}</div>
|
||||
<div style={{ display: 'grid', gap: 6, fontSize: 14, fontWeight: 700 }}>
|
||||
<div>
|
||||
Dimensions: {dims.width}×{dims.depth}×{dims.height} mm{' '}
|
||||
<span style={{ color: result.dimensionsPass ? '#4ade80' : '#fb3d4e' }}>{dimStatus}</span>
|
||||
</div>
|
||||
<div>
|
||||
Roundness K = {item.roundness.toFixed(2)}{' '}
|
||||
<span style={{ color: result.roundnessPass ? '#4ade80' : '#c084fc' }}>{roundStatus}</span>
|
||||
</div>
|
||||
<div style={{ color }}>
|
||||
Category: {category}
|
||||
</div>
|
||||
<div style={{ color }}>
|
||||
Command: {command}
|
||||
</div>
|
||||
<div style={{ color }}>
|
||||
Target zone: {category}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Html>
|
||||
);
|
||||
}
|
||||
|
||||
export default function SceneLabels3D({
|
||||
machineState,
|
||||
currentItem,
|
||||
conveyorTargetMps = 1,
|
||||
}: Props) {
|
||||
const category = currentItem?.classification.category as Category | undefined;
|
||||
const cage = TWIN_LAYOUT.rollCageSize;
|
||||
|
||||
return (
|
||||
<group>
|
||||
<Badge position={[TWIN_LAYOUT.startX, 0.7, 0]} text="A feed" color="#7dd3fc" />
|
||||
<Badge position={[TWIN_LAYOUT.cameraX, 1.15, -0.55]} text="Camera / CV" color="#38bdf8" />
|
||||
{!simplified ? (
|
||||
<>
|
||||
<Badge position={[TWIN_LAYOUT.laserX, 1.15, -0.55]} text="Laser" color="#22d3ee" />
|
||||
<Badge position={[TWIN_LAYOUT.ultrasonicX, 1.0, -0.55]} text="Ultrasonic" color="#67e8f9" />
|
||||
</>
|
||||
) : null}
|
||||
<Badge position={[TWIN_LAYOUT.accumulatorX, 0.85, 0.55]} text="Накопитель" color="#5eead4" />
|
||||
<Badge position={[TWIN_LAYOUT.gateX, 1.0, 0]} text="Stop-gate" color="#fda4af" />
|
||||
<Badge position={[TWIN_LAYOUT.zoneBX, 0.9, 0]} text="B" color="#4ade80" />
|
||||
<Badge position={[TWIN_LAYOUT.gateX + 0.4, 1.0, TWIN_LAYOUT.zoneCZ]} text="C roll-cage" color="#f59e0b" />
|
||||
<Badge position={[TWIN_LAYOUT.gateX + 0.4, 1.0, TWIN_LAYOUT.zoneDZ]} text="D roll-cage" color="#c084fc" />
|
||||
{/* Zones A/B/C/D — always explicit */}
|
||||
<Badge position={[TWIN_LAYOUT.startX, 0.85, 0]} text="A" color="#38bdf8" large />
|
||||
<Badge position={[TWIN_LAYOUT.zoneBX, 0.95, 0]} text="B" color={ROUTE_COLORS.B} large />
|
||||
<Badge
|
||||
position={[TWIN_LAYOUT.gateX + 0.4, cage.y + 0.35, TWIN_LAYOUT.zoneCZ]}
|
||||
text="C"
|
||||
color={ROUTE_COLORS.C}
|
||||
large
|
||||
/>
|
||||
<Badge
|
||||
position={[TWIN_LAYOUT.gateX + 0.4, cage.y + 0.35, TWIN_LAYOUT.zoneDZ]}
|
||||
text="D"
|
||||
color={ROUTE_COLORS.D}
|
||||
large
|
||||
/>
|
||||
|
||||
{item && result && !simplified ? (
|
||||
<Badge
|
||||
position={[0, 1.6, 0]}
|
||||
text={`${item.dimensionsMm.width}×${item.dimensionsMm.depth}×${item.dimensionsMm.height} · K=${item.roundness.toFixed(2)} · ${result.dimensionsPass ? 'DIM PASS' : 'DIM FAIL'} · ${category}`}
|
||||
color={category === 'B' ? '#4ade80' : category === 'C' ? '#f59e0b' : '#c084fc'}
|
||||
/>
|
||||
) : null}
|
||||
{/* Accumulator at end of feed conveyor */}
|
||||
<Badge
|
||||
position={[TWIN_LAYOUT.accumulatorX, 0.95, 0.65]}
|
||||
text="Накопитель"
|
||||
color="#5eead4"
|
||||
large
|
||||
/>
|
||||
|
||||
{command ? (
|
||||
{/* Sensors & actuators — always visible */}
|
||||
<Badge position={[TWIN_LAYOUT.cameraX, 1.2, -0.55]} text="Camera / CV" color="#38bdf8" />
|
||||
<Badge position={[TWIN_LAYOUT.laserX, 1.2, -0.55]} text="Laser" color="#22d3ee" />
|
||||
<Badge position={[TWIN_LAYOUT.ultrasonicX, 1.05, -0.55]} text="Ultrasonic" color="#67e8f9" />
|
||||
<Badge position={[TWIN_LAYOUT.gateX, 1.15, 0]} text="Stop-gate" color="#fda4af" />
|
||||
<Badge position={[TWIN_LAYOUT.gateX - 0.1, 0.75, 0.75]} text="Pusher C" color={ROUTE_COLORS.C} />
|
||||
<Badge position={[TWIN_LAYOUT.gateX - 0.1, 0.75, -0.75]} text="Pusher D" color={ROUTE_COLORS.D} />
|
||||
|
||||
{/* Route labels */}
|
||||
<Badge position={[(TWIN_LAYOUT.gateX + TWIN_LAYOUT.zoneBX) / 2, 0.55, 0]} text="Route B" color={ROUTE_COLORS.B} />
|
||||
<Badge
|
||||
position={[TWIN_LAYOUT.gateX + 0.2, 0.55, TWIN_LAYOUT.zoneCZ / 2]}
|
||||
text="Route C"
|
||||
color={ROUTE_COLORS.C}
|
||||
/>
|
||||
<Badge
|
||||
position={[TWIN_LAYOUT.gateX + 0.2, 0.55, TWIN_LAYOUT.zoneDZ / 2]}
|
||||
text="Route D"
|
||||
color={ROUTE_COLORS.D}
|
||||
/>
|
||||
|
||||
<Badge
|
||||
position={[-3.2, 1.55, 1.4]}
|
||||
text={`Conveyor ${conveyorTargetMps.toFixed(2)} m/s · min 10×10×10 mm`}
|
||||
color="#91a4b8"
|
||||
/>
|
||||
|
||||
{currentItem ? <ItemProofPanel currentItem={currentItem} machineState={machineState} /> : null}
|
||||
|
||||
{category === 'C' && currentItem && !currentItem.classification.roundnessPass ? (
|
||||
<Badge
|
||||
position={[TWIN_LAYOUT.gateX + 1.2, 1.35, 0]}
|
||||
text={command}
|
||||
color={category === 'B' ? '#4ade80' : category === 'C' ? '#f59e0b' : '#c084fc'}
|
||||
position={[TWIN_LAYOUT.gateX + 0.4, cage.y + 0.7, TWIN_LAYOUT.zoneCZ]}
|
||||
text="C-priority (dims first)"
|
||||
color={ROUTE_COLORS.C}
|
||||
large
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{machineState === 'DETECTING' ? (
|
||||
<Badge position={[TWIN_LAYOUT.cameraX, 1.45, 0]} text="CV detection" color="#38bdf8" />
|
||||
<Badge position={[TWIN_LAYOUT.cameraX, 1.55, 0]} text="CV detection" color="#38bdf8" />
|
||||
) : null}
|
||||
|
||||
{machineState === 'FAULT' || machineState === 'EMERGENCY_STOP' ? (
|
||||
<Badge position={[0, 1.8, 0]} text={machineState} color="#fb3d4e" />
|
||||
<Badge position={[0, 2.5, 0]} text={machineState} color="#fb3d4e" large />
|
||||
) : null}
|
||||
</group>
|
||||
);
|
||||
|
||||
@@ -10,6 +10,7 @@ import Item3D from './Item3D';
|
||||
import RouteArrows3D from './RouteArrows3D';
|
||||
import SceneLabels3D from './SceneLabels3D';
|
||||
import { PHYSICS_ENGINE_ENABLED } from './itemMotion';
|
||||
import { NOMINAL_CONVEYOR_SPEED_MPS } from '../../domain/simulation';
|
||||
|
||||
export interface SorterDigitalTwinProps {
|
||||
simulation: SimulationState;
|
||||
@@ -96,6 +97,7 @@ function TwinScene({
|
||||
machineState={simulation.machineState}
|
||||
currentItem={simulation.currentItem}
|
||||
simplified={simplified}
|
||||
conveyorTargetMps={NOMINAL_CONVEYOR_SPEED_MPS}
|
||||
/>
|
||||
|
||||
{stopped ? (
|
||||
@@ -139,6 +141,8 @@ export default function SorterDigitalTwin({
|
||||
<span className="twin-chip">{simulation.scenario.name}</span>
|
||||
{category ? <span className={`twin-chip category-${category}`}>Zone {category}</span> : null}
|
||||
<span className="twin-chip">{command}</span>
|
||||
<span className="twin-chip">Conveyor {NOMINAL_CONVEYOR_SPEED_MPS.toFixed(2)} m/s</span>
|
||||
<span className="twin-chip">Min 10×10×10 mm</span>
|
||||
{showFps ? <span className="twin-chip">FPS ~{fps}</span> : null}
|
||||
<span className="twin-chip muted-chip">
|
||||
Motion: {PHYSICS_ENGINE_ENABLED ? 'physics' : 'state-machine'}
|
||||
|
||||
@@ -23,9 +23,9 @@ function ZoneBox({
|
||||
<meshStandardMaterial
|
||||
color={color}
|
||||
transparent
|
||||
opacity={active ? 0.45 : 0.18}
|
||||
opacity={active ? 0.5 : 0.22}
|
||||
emissive={color}
|
||||
emissiveIntensity={active ? 0.35 : 0.05}
|
||||
emissiveIntensity={active ? 0.4 : 0.12}
|
||||
/>
|
||||
</mesh>
|
||||
);
|
||||
@@ -37,23 +37,23 @@ export default function SortingZones3D({ activeCategory, activeRoute }: Props) {
|
||||
|
||||
return (
|
||||
<group>
|
||||
{/* Zone A feed */}
|
||||
{/* Zone A feed — always visible cyan */}
|
||||
<ZoneBox
|
||||
position={[TWIN_LAYOUT.startX, 0.2, 0]}
|
||||
size={[0.9, 0.4, 0.9]}
|
||||
position={[TWIN_LAYOUT.startX, 0.22, 0]}
|
||||
size={[1.0, 0.44, 1.0]}
|
||||
color="#38bdf8"
|
||||
active={!active}
|
||||
active={active === undefined}
|
||||
/>
|
||||
|
||||
{/* Zone B main sorter — straight */}
|
||||
{/* Zone B main sorter — green, straight */}
|
||||
<ZoneBox
|
||||
position={[TWIN_LAYOUT.zoneBX, 0.25, 0]}
|
||||
size={[1.1, 0.5, 1.1]}
|
||||
position={[TWIN_LAYOUT.zoneBX, 0.28, 0]}
|
||||
size={[1.2, 0.56, 1.2]}
|
||||
color={ROUTE_COLORS.B}
|
||||
active={active === 'B'}
|
||||
/>
|
||||
|
||||
{/* Roll-cage C 1200×800×800 mm */}
|
||||
{/* Roll-cage C 1200×800×800 mm — orange */}
|
||||
<ZoneBox
|
||||
position={[TWIN_LAYOUT.gateX + 0.4, cage.y / 2, TWIN_LAYOUT.zoneCZ]}
|
||||
size={[cage.x, cage.y, cage.z]}
|
||||
@@ -61,7 +61,7 @@ export default function SortingZones3D({ activeCategory, activeRoute }: Props) {
|
||||
active={active === 'C'}
|
||||
/>
|
||||
|
||||
{/* Roll-cage D 1200×800×800 mm */}
|
||||
{/* Roll-cage D 1200×800×800 mm — purple */}
|
||||
<ZoneBox
|
||||
position={[TWIN_LAYOUT.gateX + 0.4, cage.y / 2, TWIN_LAYOUT.zoneDZ]}
|
||||
size={[cage.x, cage.y, cage.z]}
|
||||
|
||||
Reference in New Issue
Block a user