Unify CAD conveyor on / and /details, add RealSense D435i + contact paddle physics, keep items moving through scan, lock 7×10 drop validation, and ship Stage 2 artifacts with production on :3100 (permanent HTTPS still blocked). Co-authored-by: Cursor <cursoragent@cursor.com>
93 lines
3.9 KiB
TypeScript
93 lines
3.9 KiB
TypeScript
/**
|
||
* Stage 2B §16 — mandatory drop validation: 7 routes × 10 runs (headless Rapier).
|
||
*
|
||
* Uses the SAME simulateDrop as the unit tests — same colliders, profiles,
|
||
* handoff and pusher as the runtime. Writes docs/stage2_real_sorter/drop-validation.json.
|
||
*
|
||
* Note: the sim is deterministic (fixed dt, no RNG), so the 10 runs per route
|
||
* are identical repetitions validating stability, not a Monte Carlo sample.
|
||
* Runtime variability is covered by the browser e2e (variable frame rate).
|
||
*/
|
||
import { writeFileSync, mkdirSync } from 'node:fs';
|
||
import { dirname, join } from 'node:path';
|
||
import { fileURLToPath } from 'node:url';
|
||
import { initRapier, simulateDrop } from '../src/domain/physicsDropSim';
|
||
|
||
const here = dirname(fileURLToPath(import.meta.url));
|
||
const OUT = join(here, '../docs/stage2_real_sorter/drop-validation.json');
|
||
|
||
const RUNS = 10;
|
||
const ROUTES: { sku: string; zone: 'B' | 'C' | 'D'; label: string }[] = [
|
||
{ sku: 'SKU-001', zone: 'B', label: 'box → B' },
|
||
{ sku: 'SKU-009', zone: 'C', label: 'pen → C' },
|
||
{ sku: 'SKU-005', zone: 'C', label: 'pouf → C' },
|
||
{ sku: 'SKU-004', zone: 'C', label: 'oversized → C' },
|
||
{ sku: 'SKU-007', zone: 'D', label: 'bottle → D' },
|
||
{ sku: 'SKU-006', zone: 'D', label: 'plate → D' },
|
||
{ sku: 'SKU-008', zone: 'D', label: 'cylinder → D' },
|
||
];
|
||
|
||
// Work-area bounds (m) — anything outside is out-of-bounds.
|
||
const BOUNDS = { x: [-2.6, 4.2], y: [-0.05, 2.5], z: [-2.6, 2.6] };
|
||
const SPEED_TELEPORT_CAP = 8; // m/s — above this is a solver blowup/teleport
|
||
const PENETRATION_DEPTH = 0.006; // m below the belt surface = critical penetration
|
||
|
||
await initRapier();
|
||
|
||
const report = {
|
||
generatedAt: new Date().toISOString(),
|
||
sim: 'headless Rapier via simulateDrop (deterministic, dt=1/60, settle 6s)',
|
||
runs: RUNS,
|
||
routes: [] as unknown[],
|
||
summary: { totalRoutes: 0, passedRoutes: 0, result: 'PASS' as 'PASS' | 'FAIL' },
|
||
};
|
||
|
||
for (const route of ROUTES) {
|
||
let correct = 0;
|
||
let penetrations = 0;
|
||
let teleports = 0;
|
||
let outOfBounds = 0;
|
||
let frozenInAir = 0;
|
||
let pusherContacts = 0;
|
||
const finals: string[] = [];
|
||
for (let run = 0; run < RUNS; run += 1) {
|
||
const r = simulateDrop(route.sku, route.zone);
|
||
if (r.insideExpectedReceiver) correct += 1;
|
||
if (r.minClearanceM < -PENETRATION_DEPTH) penetrations += 1;
|
||
if (r.maxSpeedMps > SPEED_TELEPORT_CAP) teleports += 1;
|
||
const [x, y, z] = r.finalPosition;
|
||
if (x < BOUNDS.x[0] || x > BOUNDS.x[1] || y < BOUNDS.y[0] || y > BOUNDS.y[1] || z < BOUNDS.z[0] || z > BOUNDS.z[1]) {
|
||
outOfBounds += 1;
|
||
}
|
||
// Frozen in air: settled above floor level but NOT inside the receiver.
|
||
if (!r.insideExpectedReceiver && y > 0.4) frozenInAir += 1;
|
||
if (r.pusherContactMade) pusherContacts += 1;
|
||
if (run < 3) finals.push(`(${x.toFixed(2)},${y.toFixed(2)},${z.toFixed(2)})`);
|
||
}
|
||
const pass = correct === RUNS && penetrations === 0 && teleports === 0 && outOfBounds === 0 && frozenInAir === 0
|
||
&& (route.zone === 'B' || pusherContacts === RUNS);
|
||
report.routes.push({
|
||
sku: route.sku,
|
||
route: route.label,
|
||
expectedZone: route.zone,
|
||
runs: RUNS,
|
||
correctLandings: correct,
|
||
penetrations,
|
||
teleports,
|
||
outOfBounds,
|
||
frozenInAir,
|
||
pusherContacts: route.zone === 'B' ? 'n/a' : pusherContacts,
|
||
sampleFinalPositions: finals,
|
||
result: pass ? 'PASS' : 'FAIL',
|
||
});
|
||
report.summary.totalRoutes += 1;
|
||
if (pass) report.summary.passedRoutes += 1;
|
||
console.log(`${pass ? 'PASS' : 'FAIL'} ${route.label} (${route.sku}): ${correct}/${RUNS} landings, pen=${penetrations}, tp=${teleports}, oob=${outOfBounds}, air=${frozenInAir}, pusher=${route.zone === 'B' ? 'n/a' : pusherContacts}`);
|
||
}
|
||
|
||
if (report.summary.passedRoutes !== report.summary.totalRoutes) report.summary.result = 'FAIL';
|
||
mkdirSync(dirname(OUT), { recursive: true });
|
||
writeFileSync(OUT, JSON.stringify(report, null, 2));
|
||
console.log(`\n${report.summary.passedRoutes}/${report.summary.totalRoutes} routes PASS -> ${OUT}`);
|
||
process.exit(report.summary.result === 'PASS' ? 0 : 1);
|