Files
ozone-tech_owl_prime/src/domain/physicsWorldLayout.ts
Даня Архипов acc59ec912 feat: finish Stage 2B continuous sorter realism and local deploy
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>
2026-07-30 12:00:09 +00:00

132 lines
6.8 KiB
TypeScript

/**
* Stage 2 — single source of truth for static physics colliders.
*
* Pure data (no React, no Rapier): consumed by
* - SorterStaticColliders (runtime, @react-three/rapier CuboidColliders);
* - physicsDropSim (headless Node validation with rapier3d-compat).
*
* Geometry contract (visual == collider == domain kinematic surface):
* - C/D gravity chutes follow the domain chute line (conveyorNetwork
* CHUTE_C/D_ENTRY -> EXIT): from belt edge y=0.66 down to cage front edge
* y=0.14 — real gravity-chute pitch ~0.39 rad (smooth coated steel,
* friction 0.2), not a gentle ramp where items would stall.
* - Cage C/D entry side is OPEN (real roll cages are 3-sided + open front)
* with a 40mm sill; the chute deposits items through the open front.
* - B drop chute descends from spur end (2.15, 0.70) into the open front of
* the B receiver bin (past its low entry lip).
* Every collider mirrors VISIBLE scene geometry — no fake guides (§11.6).
*/
import { ZONES, B_RECEIVER, ROLL_CAGE, CONVEYOR_WIDTH_M, BELT_TOP_Y, CAGE_FLOOR_Y } from './physicalLayout';
export interface StaticColliderDef {
id: string;
halfExtents: [number, number, number];
position: [number, number, number];
/** Euler XYZ radians. */
rotation: [number, number, number];
friction: number;
}
/**
* C/D chute: top surface line from (z=±0.24, y≈0.665 — flush UNDER the belt
* edge so even a 12mm pen cannot fall into a lip gap) to (z=±1.6, y≈0.135,
* cage open front). tan ≈ 0.39 > μ_combined for every SKU -> items slide.
*/
export const CHUTE_PITCH = Math.atan2(0.53, 1.36); // ≈ 0.372 rad
export const CHUTE_LENGTH = Math.hypot(1.36, 0.53); // ≈ 1.460 m
export const CHUTE_MID_Y = 0.40;
export const CHUTE_MID_Z = 0.92;
/** Junction transfer plate: SPEC_DERIVED 1000mm wide (x 1.2..2.2) — covers
* the full junction diagonal plus the 435mm cylinder / 900mm carton with
* margin; converging side rails. */
export const CHUTE_X = 1.7;
export const CHUTE_HALF_W = 0.5;
/** B drop chute: line from (2.15, 0.70) to (2.85, 0.13), pitch ≈ 0.684 rad. */
export const B_CHUTE_PITCH = Math.atan2(BELT_TOP_Y - (CAGE_FLOOR_Y + 0.05), 0.7);
function chuteColliders(targetZ: number, label: 'C' | 'D'): StaticColliderDef[] {
const dir = targetZ > 0 ? 1 : -1;
const midZ = dir * CHUTE_MID_Z;
const rot: [number, number, number] = [dir * CHUTE_PITCH, 0, 0];
return [
{
id: `chute-${label}-floor`,
halfExtents: [CHUTE_HALF_W, 0.01, CHUTE_LENGTH / 2],
// top surface: y≈0.665 flush at belt edge (z 0.24) -> 0.135 at cage edge
position: [CHUTE_X, CHUTE_MID_Y - 0.01, midZ],
rotation: rot,
friction: 0.2, // smooth coated steel — gravity chute (VISUAL_PHYSICS_ESTIMATE)
},
{
id: `chute-${label}-rail-left`,
halfExtents: [0.01, 0.035, CHUTE_LENGTH / 2],
position: [CHUTE_X - CHUTE_HALF_W - 0.01, CHUTE_MID_Y + 0.035, midZ],
rotation: rot,
friction: 0.3,
},
{
id: `chute-${label}-rail-right`,
halfExtents: [0.01, 0.035, CHUTE_LENGTH / 2],
position: [CHUTE_X + CHUTE_HALF_W + 0.01, CHUTE_MID_Y + 0.035, midZ],
rotation: rot,
friction: 0.3,
},
];
}
function receiverColliders(): StaticColliderDef[] {
const bin = B_RECEIVER;
const hx = bin.width / 2;
const hz = bin.depth / 2;
const cage = ROLL_CAGE;
const chx = cage.width / 2;
const chz = cage.depth / 2;
const defs: StaticColliderDef[] = [
// B bin: floor + 3 walls + low entry lip (mirrors BReceiverBin visuals —
// open front on the chute side, no hidden wall)
{ id: 'b-bin-floor', halfExtents: [hx, 0.02, hz], position: [bin.centerX, bin.floorY - 0.02, bin.centerZ], rotation: [0, 0, 0], friction: 0.8 },
{ id: 'b-bin-wall-z+', halfExtents: [hx, bin.wallHeight / 2, 0.015], position: [bin.centerX, bin.floorY + bin.wallHeight / 2, bin.centerZ + hz], rotation: [0, 0, 0], friction: 0.6 },
{ id: 'b-bin-wall-z-', halfExtents: [hx, bin.wallHeight / 2, 0.015], position: [bin.centerX, bin.floorY + bin.wallHeight / 2, bin.centerZ - hz], rotation: [0, 0, 0], friction: 0.6 },
{ id: 'b-bin-wall-x+', halfExtents: [0.015, bin.wallHeight / 2, hz], position: [bin.centerX + hx, bin.floorY + bin.wallHeight / 2, bin.centerZ], rotation: [0, 0, 0], friction: 0.6 },
{ id: 'b-bin-entry-lip', halfExtents: [0.015, 0.04, hz], position: [bin.centerX - hx, bin.floorY + 0.04, bin.centerZ], rotation: [0, 0, 0], friction: 0.6 },
];
const entryX = bin.centerX - hx + 0.1;
const chuteLen = Math.hypot(entryX - bin.transferEndX, BELT_TOP_Y - bin.floorY);
defs.push({
id: 'b-drop-chute',
halfExtents: [chuteLen / 2, 0.008, (CONVEYOR_WIDTH_M - 0.08) / 2],
// top surface runs (2.15, 0.696) -> (2.85, 0.126): flush with spur end
position: [(bin.transferEndX + entryX) / 2, 0.395, bin.centerZ],
rotation: [0, 0, -B_CHUTE_PITCH], // descends toward +X (into the bin)
friction: 0.25,
});
for (const [label, zone] of [['C', ZONES.C], ['D', ZONES.D]] as const) {
const entrySide = label === 'C' ? -1 : 1; // open front faces the conveyor
defs.push(
{ id: `cage-${label}-floor`, halfExtents: [chx, 0.04, chz], position: [zone.x, 0.04, zone.z], rotation: [0, 0, 0], friction: 0.8 },
{ id: `cage-${label}-wall-x-`, halfExtents: [0.02, cage.height / 2, chz], position: [zone.x - chx, cage.height / 2, zone.z], rotation: [0, 0, 0], friction: 0.5 },
{ id: `cage-${label}-wall-x+`, halfExtents: [0.02, cage.height / 2, chz], position: [zone.x + chx, cage.height / 2, zone.z], rotation: [0, 0, 0], friction: 0.5 },
// closed back wall (far side from the conveyor)
{ id: `cage-${label}-wall-back`, halfExtents: [chx, cage.height / 2, 0.02], position: [zone.x, cage.height / 2, zone.z - entrySide * chz], rotation: [0, 0, 0], friction: 0.5 },
// 40mm sill on the open entry side (mirrors RollCageMesh openSide sill)
{ id: `cage-${label}-entry-sill`, halfExtents: [chx, 0.02, 0.015], position: [zone.x, CAGE_FLOOR_Y + 0.02, zone.z + entrySide * chz], rotation: [0, 0, 0], friction: 0.5 },
);
}
return defs;
}
export function getStaticColliders(): StaticColliderDef[] {
return [
{ id: 'world-floor', halfExtents: [8, 0.05, 6], position: [0, -0.05, 0], rotation: [0, 0, 0], friction: 0.8 },
// Belt safety slab — items never pass through the belt surface.
// Ends at the B spur end (2.15): beyond it the B drop chute takes over.
{ id: 'belt-slab', halfExtents: [(2.15 + 4.2) / 2, 0.012, CONVEYOR_WIDTH_M / 2], position: [(2.15 - 4.2) / 2, BELT_TOP_Y - 0.014, 0], rotation: [0, 0, 0], friction: 0.7 },
// B transfer spur — top FLUSH with the belt slab (no 2mm trip step)
{ id: 'b-spur', halfExtents: [0.325, 0.02, (CONVEYOR_WIDTH_M - 0.06) / 2], position: [1.825, BELT_TOP_Y - 0.022, 0], rotation: [0, 0, 0], friction: 0.4 },
...chuteColliders(ZONES.C.z, 'C'),
...chuteColliders(ZONES.D.z, 'D'),
...receiverColliders(),
];
}