feat: Stage 2C CAD assembly cleanup, full-height canvas, physics parity

Remove default Measurement overlay and procedural motor duplicate; force
opaque CAD materials; modular extension pitch from CAD; shared runtime/
headless physics config hash; layout e2e and Stage 2C artifacts.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Даня Архипов
2026-07-30 12:35:50 +00:00
parent 0a1be376eb
commit 68327635ce
50 changed files with 3051 additions and 100 deletions

View File

@@ -42,14 +42,9 @@ import {
BELT_TOP_Y,
BELT_THICKNESS_M,
CONVEYOR_WIDTH_M,
ROLLER_RADIUS_M,
ROLLER_SPACING_M,
FRAME_HEIGHT_M,
SIDE_GUARD_HEIGHT_M,
LEG_WIDTH_M,
MOTOR_WIDTH_M,
MOTOR_HEIGHT_M,
MOTOR_DEPTH_M,
DRIVE_ROLLER_RADIUS_M,
ZONES,
CAMERA_RIG,
@@ -61,6 +56,11 @@ import {
CAGE_FLOOR_Y,
CONVEYOR_SPEED_MPS,
} from '../../domain/physicalLayout';
import {
CAD_ROLLER_PITCH_M,
CAD_ROLLER_RADIUS_M,
CAD_SUPPORT_SPACING_M,
} from '../../domain/cadAssemblyParams';
import { CHUTE_PITCH, CHUTE_LENGTH, CHUTE_MID_Y, CHUTE_MID_Z, CHUTE_X, CHUTE_HALF_W } from '../../domain/physicsWorldLayout';
import PerfCollector from './PerfCollector';
import PerfOverlay from './PerfOverlay';
@@ -133,8 +133,8 @@ function getItemMaterial(itemId: string) {
// Physical layout constants
const BELT_Y = BELT_TOP_Y; // 0.7m - top of belt where items ride
const BELT_UNDERSIDE_Y = BELT_TOP_Y - BELT_THICKNESS_M; // 0.685m
const ROLLER_Y = BELT_UNDERSIDE_Y - ROLLER_RADIUS_M; // ~0.645m - roller center
const FRAME_TOP_Y = ROLLER_Y - ROLLER_RADIUS_M - 0.02; // Top of frame structure
const ROLLER_Y = BELT_UNDERSIDE_Y - CAD_ROLLER_RADIUS_M;
const FRAME_TOP_Y = ROLLER_Y - CAD_ROLLER_RADIUS_M - 0.02;
const CONVEYOR_START_X = -4.2;
const CONVEYOR_END_X = B_RECEIVER.transferEndX + 0.15; // main belt ends at short B spur tip
/** Max items rendered simultaneously (perf cap). */
@@ -267,12 +267,19 @@ function MotionTrail({
);
}
/** Static roller cylinder — no per-frame rotation (belt stripes show movement). */
/** Static roller — CAD pitch/radius for modular extensions (no non-uniform scale of CAD module). */
function StaticRoller({ position }: { position: [number, number, number] }) {
return (
<mesh position={position} rotation={[0, 0, Math.PI / 2]}>
<cylinderGeometry args={[ROLLER_RADIUS_M, ROLLER_RADIUS_M, CONVEYOR_WIDTH_M - 0.02, 8]} />
<meshStandardMaterial color={COLORS.rollers} metalness={0.4} roughness={0.5} />
<cylinderGeometry args={[CAD_ROLLER_RADIUS_M, CAD_ROLLER_RADIUS_M, CONVEYOR_WIDTH_M - 0.02, 16]} />
<meshStandardMaterial
color={COLORS.rollers}
metalness={0.85}
roughness={0.32}
transparent={false}
opacity={1}
depthWrite
/>
</mesh>
);
}
@@ -302,56 +309,7 @@ function BeltStripe({ baseOffset, elapsedMs }: { baseOffset: number; elapsedMs:
);
}
/** Stepper motor drive unit with pulse indicator */
function StepperMotor({ position, pulseActive }: { position: [number, number, number]; pulseActive: boolean }) {
const pulseRef = useRef<Mesh>(null);
const rotationRef = useRef(0);
useFrame((_, delta) => {
if (pulseRef.current && pulseActive) {
rotationRef.current += delta * 8;
pulseRef.current.rotation.z = rotationRef.current;
}
});
return (
<group position={position}>
{/* Motor body */}
<mesh position={[0, 0, CONVEYOR_WIDTH_M / 2 + MOTOR_DEPTH_M / 2 + 0.02]}>
<boxGeometry args={[MOTOR_WIDTH_M, MOTOR_HEIGHT_M, MOTOR_DEPTH_M]} />
<meshStandardMaterial color="#475569" metalness={0.6} roughness={0.3} />
</mesh>
{/* Motor shaft - rotates when active */}
<mesh
ref={pulseRef}
position={[0, 0, CONVEYOR_WIDTH_M / 2 + 0.01]}
rotation={[Math.PI / 2, 0, 0]}
>
<cylinderGeometry args={[0.015, 0.015, 0.03, 8]} />
<meshStandardMaterial color="#94a3b8" metalness={0.7} roughness={0.2} />
</mesh>
{/* Drive pulley */}
<mesh position={[0, 0, CONVEYOR_WIDTH_M / 2 - 0.02]} rotation={[Math.PI / 2, 0, 0]}>
<cylinderGeometry args={[0.035, 0.035, 0.025, 12]} />
<meshStandardMaterial color="#64748b" metalness={0.5} roughness={0.4} />
</mesh>
{/* Belt to drive roller */}
<mesh position={[0, DRIVE_ROLLER_RADIUS_M / 2, CONVEYOR_WIDTH_M / 2 - 0.02]}>
<boxGeometry args={[0.01, DRIVE_ROLLER_RADIUS_M + 0.02, 0.015]} />
<meshStandardMaterial color="#1e293b" />
</mesh>
{/* Pulse indicator LED */}
<mesh position={[0, MOTOR_HEIGHT_M / 2 - 0.01, CONVEYOR_WIDTH_M / 2 + MOTOR_DEPTH_M + 0.025]}>
<sphereGeometry args={[0.008, 8, 8]} />
<meshStandardMaterial
color={pulseActive ? '#22d3ee' : '#475569'}
emissive={pulseActive ? '#22d3ee' : '#000'}
emissiveIntensity={pulseActive ? 0.8 : 0}
/>
</mesh>
</group>
);
}
/** StepperMotor removed in Stage 2C — CAD motor-and-drive/NEMA17 is the sole drive unit. */
/** Laser beam from rangefinder to item — static opacity, no useFrame. */
function LaserBeam({ active, itemY }: { active: boolean; itemY: number }) {
@@ -467,26 +425,27 @@ function SupportLeg({ x }: { x: number }) {
* Belt top surface at 0.7m (BELT_TOP_Y)
*/
/**
* Belt extension segment (SPEC_DERIVED) — entry/exit sections flanking the
* CAD conveyor module. Same 500 mm width / 700 mm belt top as the real unit.
* Belt extension segment (SPEC_DERIVED modular) — entry/exit flanking the CAD
* module. Uses CAD roller pitch (500 mm) and radius (25 mm); never scales the GLB.
*/
function BeltSection({ startX, endX, simplified, shadows }: { startX: number; endX: number; simplified: boolean; shadows: boolean }) {
const spacing = simplified ? ROLLER_SPACING_M * 2 : ROLLER_SPACING_M;
const spacing = simplified ? CAD_ROLLER_PITCH_M * 2 : CAD_ROLLER_PITCH_M;
const length = endX - startX;
const centerX = (startX + endX) / 2;
const rollerCount = Math.max(1, Math.floor(length / spacing));
const rollerY = BELT_Y - BELT_THICKNESS_M - CAD_ROLLER_RADIUS_M - 0.01;
const rollerPositions = useMemo(() => {
const positions: [number, number, number][] = [];
for (let i = 0; i < rollerCount; i++) {
positions.push([startX + spacing / 2 + i * spacing, ROLLER_Y, 0]);
positions.push([startX + spacing / 2 + i * spacing, rollerY, 0]);
}
return positions;
}, [rollerCount, spacing, startX]);
}, [rollerCount, spacing, startX, rollerY]);
const legPositions = useMemo(() => {
const positions: number[] = [];
for (let x = startX + 0.5; x < endX - 0.3; x += 2.0) {
for (let x = startX + 0.5; x < endX - 0.3; x += CAD_SUPPORT_SPACING_M) {
positions.push(x);
}
return positions;
@@ -496,23 +455,23 @@ function BeltSection({ startX, endX, simplified, shadows }: { startX: number; en
<group>
<mesh position={[centerX, BELT_Y - BELT_THICKNESS_M / 2, 0]} receiveShadow={shadows}>
<boxGeometry args={[length, BELT_THICKNESS_M, CONVEYOR_WIDTH_M]} />
<meshStandardMaterial color={COLORS.belt} roughness={0.85} metalness={0.05} />
<meshStandardMaterial color={COLORS.belt} roughness={0.85} metalness={0.05} transparent={false} opacity={1} depthWrite />
</mesh>
<mesh position={[centerX, BELT_Y + SIDE_GUARD_HEIGHT_M / 2, CONVEYOR_WIDTH_M / 2 + 0.02]}>
<boxGeometry args={[length, SIDE_GUARD_HEIGHT_M, 0.025]} />
<meshStandardMaterial color={COLORS.sideGuards} metalness={0.4} roughness={0.5} />
<meshStandardMaterial color={COLORS.sideGuards} metalness={0.55} roughness={0.45} transparent={false} opacity={1} depthWrite />
</mesh>
<mesh position={[centerX, BELT_Y + SIDE_GUARD_HEIGHT_M / 2, -CONVEYOR_WIDTH_M / 2 - 0.02]}>
<boxGeometry args={[length, SIDE_GUARD_HEIGHT_M, 0.025]} />
<meshStandardMaterial color={COLORS.sideGuards} metalness={0.4} roughness={0.5} />
<meshStandardMaterial color={COLORS.sideGuards} metalness={0.55} roughness={0.45} transparent={false} opacity={1} depthWrite />
</mesh>
<mesh position={[centerX, FRAME_TOP_Y + 0.025, CONVEYOR_WIDTH_M / 2 + 0.01]} castShadow={shadows}>
<boxGeometry args={[length, 0.05, 0.04]} />
<meshStandardMaterial color={COLORS.conveyorFrame} metalness={0.5} roughness={0.4} />
<meshStandardMaterial color={COLORS.conveyorFrame} metalness={0.55} roughness={0.4} transparent={false} opacity={1} depthWrite />
</mesh>
<mesh position={[centerX, FRAME_TOP_Y + 0.025, -CONVEYOR_WIDTH_M / 2 - 0.01]} castShadow={shadows}>
<boxGeometry args={[length, 0.05, 0.04]} />
<meshStandardMaterial color={COLORS.conveyorFrame} metalness={0.5} roughness={0.4} />
<meshStandardMaterial color={COLORS.conveyorFrame} metalness={0.55} roughness={0.4} transparent={false} opacity={1} depthWrite />
</mesh>
{rollerPositions.map((pos, i) => (
<StaticRoller key={i} position={pos} />
@@ -525,17 +484,16 @@ function BeltSection({ startX, endX, simplified, shadows }: { startX: number; en
}
/**
* Conveyor: CAD module (REAL_CAD) + entry/exit belt extensions (SPEC_DERIVED).
* Gate/roller/belt motion is driven by the domain visual-state adapter.
* SorterCadAssembly (inline): sole conveyor geometry source —
* REAL_CAD module + modular SPEC_DERIVED entry/exit at CAD pitch.
* No procedural motor (CAD NEMA17 only).
*/
function ConveyorBelt({ pulseActive, elapsedMs, simplified, shadows = false, gateOpen = false, beltVelocityMps = 0, rollerOmega = 0 }: { pulseActive: boolean; elapsedMs: number; simplified: boolean; shadows?: boolean; gateOpen?: boolean; beltVelocityMps?: number; rollerOmega?: number }) {
function ConveyorBelt({ pulseActive: _pulseActive, elapsedMs, simplified, shadows = false, gateOpen = false, beltVelocityMps = 0, rollerOmega = 0 }: { pulseActive: boolean; elapsedMs: number; simplified: boolean; shadows?: boolean; gateOpen?: boolean; beltVelocityMps?: number; rollerOmega?: number }) {
const stripeOffsets = simplified ? [-2, 0, 2] : [-3, -1, 0.5, 2];
const [cadStart, cadEnd] = CONVEYOR_CAD_SPAN_X;
return (
<group>
{/* CAD-derived conveyor module (real machine: frame, belt, rollers,
drive, metering gates, servo diverters, camera arch) */}
<group name="SorterCadAssembly" userData={{ role: 'single-cad-conveyor' }}>
<Suspense fallback={null}>
<ConveyorCadModel
gateOpen={gateOpen}
@@ -545,39 +503,30 @@ function ConveyorBelt({ pulseActive, elapsedMs, simplified, shadows = false, gat
/>
</Suspense>
{/* Entry extension: zone A feed into the CAD module */}
<BeltSection startX={CONVEYOR_START_X} endX={cadStart} simplified={simplified} shadows={shadows} />
{/* Exit extension: CAD module discharge to the B spur */}
<BeltSection startX={cadEnd} endX={CONVEYOR_END_X} simplified={simplified} shadows={shadows} />
{/* Belt stripes — deterministic movement synced to item (offset = time * 1 m/s) */}
{stripeOffsets.map((offset, i) => (
<BeltStripe key={i} baseOffset={offset} elapsedMs={elapsedMs} />
))}
{/* Drive roller at end (larger) */}
<mesh position={[CONVEYOR_END_X - 0.1, ROLLER_Y, 0]} rotation={[0, 0, Math.PI / 2]}>
<mesh position={[CONVEYOR_END_X - 0.1, BELT_Y - BELT_THICKNESS_M - CAD_ROLLER_RADIUS_M - 0.01, 0]} rotation={[0, 0, Math.PI / 2]}>
<cylinderGeometry args={[DRIVE_ROLLER_RADIUS_M, DRIVE_ROLLER_RADIUS_M, CONVEYOR_WIDTH_M - 0.02, 16]} />
<meshStandardMaterial color="#64748b" metalness={0.5} roughness={0.4} />
<meshStandardMaterial color="#64748b" metalness={0.5} roughness={0.4} transparent={false} opacity={1} depthWrite />
</mesh>
{/* Tension roller at start (larger) */}
<mesh position={[CONVEYOR_START_X + 0.1, ROLLER_Y, 0]} rotation={[0, 0, Math.PI / 2]}>
<mesh position={[CONVEYOR_START_X + 0.1, BELT_Y - BELT_THICKNESS_M - CAD_ROLLER_RADIUS_M - 0.01, 0]} rotation={[0, 0, Math.PI / 2]}>
<cylinderGeometry args={[DRIVE_ROLLER_RADIUS_M * 0.9, DRIVE_ROLLER_RADIUS_M * 0.9, CONVEYOR_WIDTH_M - 0.02, 16]} />
<meshStandardMaterial color="#64748b" metalness={0.5} roughness={0.4} />
<meshStandardMaterial color="#64748b" metalness={0.5} roughness={0.4} transparent={false} opacity={1} depthWrite />
</mesh>
{/* Stepper motor at drive end */}
<StepperMotor position={[CONVEYOR_END_X - 0.1, ROLLER_Y, 0]} pulseActive={pulseActive} />
{/* End caps / guards */}
<mesh position={[CONVEYOR_START_X, BELT_Y - 0.05, 0]}>
<boxGeometry args={[0.05, 0.12, CONVEYOR_WIDTH_M + 0.1]} />
<meshStandardMaterial color={COLORS.conveyorFrame} />
<meshStandardMaterial color={COLORS.conveyorFrame} transparent={false} opacity={1} depthWrite />
</mesh>
<mesh position={[CONVEYOR_END_X, BELT_Y - 0.05, 0]}>
<boxGeometry args={[0.05, 0.12, CONVEYOR_WIDTH_M + 0.1]} />
<meshStandardMaterial color={COLORS.conveyorFrame} />
<meshStandardMaterial color={COLORS.conveyorFrame} transparent={false} opacity={1} depthWrite />
</mesh>
</group>
);