Files
ozone-tech_owl_prime/src/components/ProductDemoSection.tsx

174 lines
6.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { lazy, Suspense, useEffect, useMemo, useState } from 'react';
import CurrentProofCard from './CurrentProofCard';
import ThreeFallback from './ThreeD/ThreeFallback';
import { prefer3DByDefault, useWebGLSupport } from './ThreeD/useWebGL';
import type { SimulationState } from '../domain/types';
const SorterDigitalTwin = lazy(() => import('./ThreeD/SorterDigitalTwin'));
type ViewMode = '3d' | '2d';
interface Props {
simulation: SimulationState;
demoStepTitle: string;
onStartDemo: () => void;
onNext: () => void;
onReset: () => void;
onOpenEngineering: () => void;
}
export default function ProductDemoSection({
simulation,
demoStepTitle,
onStartDemo,
onNext,
onReset,
onOpenEngineering,
}: Props) {
const webgl = useWebGLSupport();
const [width, setWidth] = useState(() => (typeof window === 'undefined' ? 1200 : window.innerWidth));
const [viewMode, setViewMode] = useState<ViewMode>('3d');
const [contextLost, setContextLost] = useState(false);
useEffect(() => {
const onResize = () => setWidth(window.innerWidth);
window.addEventListener('resize', onResize);
return () => window.removeEventListener('resize', onResize);
}, []);
useEffect(() => {
setViewMode(prefer3DByDefault(width, webgl && !contextLost) ? '3d' : '2d');
}, [webgl, width, contextLost]);
const item = simulation.currentItem;
const category = item?.classification.category;
const command = simulation.machineState.startsWith('ROUTE_TO_')
? simulation.machineState
: item?.classification
? `ROUTE_TO_${item.classification.category}`
: '—';
const fallbackReason = useMemo(() => {
if (!webgl || contextLost) return 'webgl' as const;
if (width < 640) return 'mobile' as const;
return 'user' as const;
}, [webgl, contextLost, width]);
const show3D = viewMode === '3d' && webgl && !contextLost;
const simplified = width < 900;
return (
<section className="product-demo-section" id="demo" aria-labelledby="demo-title">
<div className="demo-header">
<div>
<p className="section-eyebrow">Live product demo</p>
<h2 id="demo-title">Главное демо</h2>
<p className="demo-lead">
3D digital twin показывает физическую маршрутизацию: A → конвейер → CV → накопитель → gate → B/C/D.
Справа — товар, категория, причина и команда ROUTE_TO_*.
</p>
</div>
<div className="demo-status">
<span>Шаг:</span>
<strong>{demoStepTitle}</strong>
<span className={`status-dot ${simulation.systemStatus.toLowerCase()}`} aria-hidden="true" />
<span className="demo-status-text">{simulation.systemStatus}</span>
</div>
</div>
<div className="view-mode-toggle" role="group" aria-label="Режим сцены">
<button
type="button"
className={show3D ? 'btn-primary' : 'btn-secondary'}
onClick={() => {
setContextLost(false);
setViewMode('3d');
}}
disabled={!webgl}
>
3D Digital Twin
</button>
<button
type="button"
className={!show3D ? 'btn-primary' : 'btn-secondary'}
onClick={() => setViewMode('2d')}
>
2D fallback
</button>
</div>
<div className="demo-layout">
<div className="demo-scene-column">
<p className="demo-mobile-hint">
Товар проходит камеру, накопитель и gate, затем уходит в зону B, C или D.
</p>
<div className="demo-scene-container demo-scene-3d">
{show3D ? (
<Suspense fallback={<div className="three-loading">Загрузка 3D digital twin…</div>}>
<SorterDigitalTwin
simulation={simulation}
simplified={simplified}
onContextLost={() => {
setContextLost(true);
setViewMode('2d');
}}
/>
</Suspense>
) : (
<ThreeFallback simulation={simulation} reason={fallbackReason} />
)}
</div>
</div>
<aside className="demo-result-column" aria-label="Результат классификации">
<CurrentProofCard simulation={simulation} />
<div className="demo-result-summary">
<div className="summary-item">
<span>Товар</span>
<strong>{item?.item.name ?? 'Ожидание'}</strong>
</div>
<div className="summary-item">
<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>Command</span>
<strong>{command}</strong>
</div>
<div className="summary-item">
<span>Target zone</span>
<strong>{category ? `Zone ${category}` : '—'}</strong>
</div>
</div>
<div className="demo-controls">
<button type="button" className="btn-primary" onClick={onStartDemo}>
Start demo
</button>
<button type="button" className="btn-primary" onClick={onNext}>
Next step
</button>
<button type="button" className="btn-secondary" onClick={onReset}>
Reset
</button>
<button type="button" className="btn-secondary" onClick={onOpenEngineering}>
Engineering details
</button>
</div>
</aside>
</div>
</section>
);
}