feat: stage 2 real CAD conveyor, hybrid physics drops, mobile policy (WIP)
- CAD pipeline: conveer.FCStd -> FreeCAD headless -> conveyor-web.glb (555KB Draco, 42 named nodes) integrated into main scene; gates and rollers animated from domain state - Domain-driven adapter sorterVisualState.ts; per-SKU visual physics profiles (VISUAL_PHYSICS_ESTIMATE) - Hybrid Rapier physics: kinematic belt travel -> dynamic drop handoff -> verified freeze; shared collider layout (physicsWorldLayout) used by runtime and headless validation sim (physicsDropSim + 13 tests) - Gravity chutes / open roll-cage front redesign (drop tuning in progress: 4/7 routes verified headless) - Premium industrial visual: PBR, ACES, PCFSoft shadows, Lightformer env, UI cleanup behind ?debug=1 - Mobile capability policy: SVG fallback / Mobile Low / full tiers, Telegram WebView forced to SVG, e2e stubs for weak devices - Performance benchmarks cad-light/physics/full pass; stage2 artifacts tsc clean. Physics drop validation for 3 SKU routes still in progress. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
128
src/domain/physicsWorldLayout.ts
Normal file
128
src/domain/physicsWorldLayout.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* 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 pitch: line from (z=±0.35, y=0.66) to (z=±1.6, y=0.14).
|
||||
* tan = 0.52/1.25 ≈ 0.416 > μ_combined for every SKU profile -> items slide.
|
||||
*/
|
||||
export const CHUTE_PITCH = Math.atan2(0.52, 1.25); // ≈ 0.394 rad
|
||||
export const CHUTE_LENGTH = Math.hypot(1.25, 0.52); // ≈ 1.354 m
|
||||
export const CHUTE_MID_Y = 0.40;
|
||||
export const CHUTE_MID_Z = 0.975;
|
||||
/** Chute plate center X (gate exit) and plate half width. */
|
||||
export const CHUTE_X = ZONES.GATE.x + 0.3;
|
||||
export const CHUTE_HALF_W = 0.25;
|
||||
/** 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.655 at belt edge (z 0.35) -> 0.135 at cage edge (z 1.6)
|
||||
position: [CHUTE_X, CHUTE_MID_Y - 0.015, 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.03, 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.03, 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
|
||||
{ id: 'b-spur', halfExtents: [0.325, 0.02, (CONVEYOR_WIDTH_M - 0.06) / 2], position: [1.825, BELT_TOP_Y - 0.02, 0], rotation: [0, 0, 0], friction: 0.4 },
|
||||
...chuteColliders(ZONES.C.z, 'C'),
|
||||
...chuteColliders(ZONES.D.z, 'D'),
|
||||
...receiverColliders(),
|
||||
];
|
||||
}
|
||||
Reference in New Issue
Block a user