import { Suspense, useMemo } from 'react'; import { getPhysicalItemPose } from '../../domain/physicalItemMotion'; import { getModelAsset } from '../../data/modelAssets'; import { ITEMS } from '../../data/items'; import type { PlaylistCase } from '../../domain/demoPlaylist'; import { getRenderedItemDimensions } from '../../domain/physicalLayout'; import * as THREE from 'three'; import { useLoader } from '@react-three/fiber'; import { STLLoader } from 'three/examples/jsm/loaders/STLLoader.js'; const COLORS = { B: '#16a34a', C: '#ea580c', D: '#7c3aed', sensorAccent: '#3b82f6', }; const ITEM_MATERIALS: Record = { 'SKU-001': { color: '#b68b58', roughness: 0.82 }, 'SKU-002': { color: '#e8eef6', roughness: 0.5 }, 'SKU-004': { color: '#c49a6c', roughness: 0.82 }, 'SKU-006': { color: '#f8fafc', roughness: 0.42 }, 'SKU-007': { color: '#7dd3fc', roughness: 0.28 }, 'SKU-008': { color: '#cbd5e1', roughness: 0.35, metalness: 0.15 }, }; function STLGeometry({ path, scale, color, accentColor, emissiveIntensity, roughness, metalness }: any) { const geometry = useLoader(STLLoader, path) as THREE.BufferGeometry; useMemo(() => { if (geometry) { geometry.center(); geometry.computeVertexNormals(); } }, [geometry]); return ( ); } function FallbackPrimitive({ type, color, accentColor, emissiveIntensity, roughness, metalness, w, h, d }: any) { if (type === 'cylinder' || type === 'sphere') { return ( ); } return ( ); } export function PhysicalPlaybackItem({ caseData, elapsedMs, slotIndex = 0 }: { caseData: PlaylistCase; elapsedMs: number; slotIndex?: number; }) { const itemId = caseData.itemId.replace('-LC', ''); const itemData = useMemo(() => ITEMS.find(i => i.id === itemId) ?? ITEMS[0], [itemId]); const asset = getModelAsset(itemId); const dims = getRenderedItemDimensions(itemData.dimensionsMm); const pose = getPhysicalItemPose({ caseId: caseData.id, slotIndex, dimensionsMm: itemData.dimensionsMm, targetCategory: caseData.expectedCategory, elapsedMs }); const { position, rotation, isSettled, phase } = pose; const isRouting = phase === 'routing'; // Do not render B items that are settled (they exit the factory) if (isSettled && caseData.expectedCategory === 'B') { return null; } // Before spawn, don't show if (elapsedMs < 0) return null; const accentColor = COLORS[caseData.expectedCategory] ?? COLORS.sensorAccent; const material = ITEM_MATERIALS[itemId] ?? { color: '#d8c3a5', roughness: 0.75 }; const emissiveIntensity = isRouting ? 0.16 : 0.03; const useSTL = asset?.loaderType === 'stl' && asset?.frontendAssetPath; const stlPath = asset?.frontendAssetPath ?? ''; const fallbackType = asset?.fallbackPrimitive ?? 'box'; return ( {useSTL ? ( }> ) : ( )} {/* Shadow on belt (only if on belt or chute) */} {!isSettled && ( )} ); }