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>
|
Before Width: | Height: | Size: 206 KiB After Width: | Height: | Size: 174 KiB |
|
Before Width: | Height: | Size: 143 KiB After Width: | Height: | Size: 128 KiB |
37
docs/stage2_real_sorter/README.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# Stage 2 — Real Sorter Simulation
|
||||
|
||||
Artifacts for the OZON Tech Sorter Simulation (CAD conveyor, continuous scan,
|
||||
angled paddle physics, RealSense D435i, public deploy).
|
||||
|
||||
## Status
|
||||
|
||||
See `STAGE2_REPORT.md`. Current finalization status:
|
||||
`PARTIAL_REAL_SORTER_SIMULATION` / `BLOCKED_BY_DEPLOYMENT_ACCESS` for the
|
||||
permanent HTTPS hostname.
|
||||
|
||||
## Key files
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `cad-inventory.json` | GLB path, 42 nodes, sizes |
|
||||
| `cad-conversion-pipeline.md` | FCStd → Draco GLB |
|
||||
| `mechanism-map.md` | Angled paddle diverter |
|
||||
| `camera-realsense-spec.md` | D435i SPEC_DERIVED + FOV |
|
||||
| `continuous-measurement.md` | Position-based scan window |
|
||||
| `physics-architecture.md` | Hybrid kinematic → dynamic |
|
||||
| `physics-profiles.json` | Per-SKU Rapier profiles |
|
||||
| `drop-validation.json` | 7 routes × 10 runs |
|
||||
| `real-sorter-validation.json` | Checklist results |
|
||||
| `deployment-report.md` | Docker / DNS / tunnel |
|
||||
| `public-domain-smoke.json` | Domain smoke facts |
|
||||
| `console-errors.json` | Captured console state |
|
||||
| `performance-cad-*.json` | Perf budgets |
|
||||
| `bundle-impact.md` | Bundle sizes |
|
||||
| `screenshots/` | 18 required views |
|
||||
| `videos/` | Capture instructions (WebM pending real recorder) |
|
||||
|
||||
## Reproduce drop validation
|
||||
|
||||
```bash
|
||||
npx tsx scripts/drop-validation.mts
|
||||
```
|
||||
183
docs/stage2_real_sorter/STAGE2_REPORT.md
Normal file
@@ -0,0 +1,183 @@
|
||||
# STAGE 2 REPORT — Final Realism / Continuous Sorting / Public Mobile
|
||||
|
||||
## 1. STAGE_2_STATUS
|
||||
|
||||
**PARTIAL_REAL_SORTER_SIMULATION** + **BLOCKED_BY_DEPLOYMENT_ACCESS**
|
||||
|
||||
Local product is ready (CAD conveyor unified, continuous scan, physical paddle
|
||||
contact, 7/7 drop validation 10/10, RealSense SPEC_DERIVED). Permanent HTTPS
|
||||
hostname `sorter.arhipovdan.ru` is blocked by external openresty/DNS. Real phone
|
||||
not tested.
|
||||
|
||||
## 2. EXECUTIVE_SUMMARY
|
||||
|
||||
- Conveyor: single CAD GLB (`conveyor-web.glb`, 544 KB, ~159k tris, 42 nodes) on `/` and `/details`.
|
||||
- Volumetric: REAL_CAD solids for frame/rollers/motor/gate; SPEC_DERIVED paddle + D435i + chutes.
|
||||
- RealSense D435i: datasheet-derived housing, lens-down on gantry, FOV debug overlay.
|
||||
- Continuous measurement: item never stops under camera; `scanProgress` from position.
|
||||
- Sorting: angled paddle kinematic body; contact-based C/D routing.
|
||||
- Drop validation: **7/7 PASS**, 10/10 each, pusher contact asserted, no teleports.
|
||||
- Domain classifier unchanged.
|
||||
- Public domain: production on `:3100` + Quick Tunnel; `sorter.arhipovdan.ru` TLS/vhost absent.
|
||||
- Phone: NOT_TESTED_ON_REAL_PHONE (SVG + Mobile Low policies covered in e2e).
|
||||
|
||||
## 3. GIT_SAFETY
|
||||
|
||||
- Branch: `feature/ozon-sorter-stage-2-real-machine`
|
||||
- Backup: `/tmp/ozone-before-stage2-finalization/`
|
||||
- Tag: `backup/pre-stage2-finalization-*`
|
||||
- Checkpoint Stage 0+1 preserved (`394c513`)
|
||||
- No force-push / reset / stash
|
||||
|
||||
## 4. CAD_AND_GEOMETRY
|
||||
|
||||
| Node group | Source | Volume | Runtime | Result |
|
||||
|---|---|---|---|---|
|
||||
| static-frame | REAL_CAD | solids | ConveyorCadModel | PASS |
|
||||
| rollers | REAL_CAD | solids, spin animated | ConveyorCadModel | PASS |
|
||||
| motor-and-drive | REAL_CAD | solids | ConveyorCadModel | PASS |
|
||||
| conveyor-belt | REAL_CAD | solids | ConveyorCadModel | PASS |
|
||||
| inspection-frame | REAL_CAD | solids | ConveyorCadModel | PASS |
|
||||
| stop-gate | REAL_CAD | solids, lift stroke | ConveyorCadModel | PASS |
|
||||
| pusher-servo (CAD) | REAL_CAD visual | — | present in GLB | PARTIAL (runtime uses SPEC_DERIVED paddle) |
|
||||
| angled paddle | SPEC_DERIVED | volumetric | PusherMechanism | PASS |
|
||||
| RealSense D435i | SPEC_DERIVED | volumetric | RealSenseD435i | PASS |
|
||||
| chutes / cages | SPEC_DERIVED | volumetric | Continuous scene | PASS |
|
||||
|
||||
Old twin deleted: `SorterDigitalTwin`, `Conveyor3D`, `Actuator3D`, `SensorRig3D`, `SortingZones3D`.
|
||||
|
||||
## 5. REALSENSE_D435I
|
||||
|
||||
See `camera-realsense-spec.md`. Optical axis −Y, optical height 0.65 m above belt,
|
||||
laser separate. Debug frustum via `?debug=1&physics=1`.
|
||||
|
||||
## 6. CONTINUOUS_MEASUREMENT
|
||||
|
||||
| | Old | New |
|
||||
|---|---|---|
|
||||
| Under camera | dwell / stop | continuous 1.0 m/s |
|
||||
| scanProgress | time-based | position (`SCAN_START_X`…`SCAN_END_X`) |
|
||||
| Classification deadline | — | before `MECHANISM_CONTACT_X` |
|
||||
|
||||
Tests: `continuousMeasurement.test.ts` — beltVelocity > 0, item x increasing.
|
||||
|
||||
## 7. SORTING_MECHANISM
|
||||
|
||||
See `mechanism-map.md`. Driven by `physicsSimClock` (Rapier steps), not domain
|
||||
wall clock — prevents tunneling under render lag.
|
||||
|
||||
## 8. PHYSICS
|
||||
|
||||
- Hybrid: kinematic belt → dynamic at junction → freeze after rest/sleep.
|
||||
- Handoff checked **before** kinematic drive (no lag teleport).
|
||||
- Settle budget in physics seconds (`SETTLE_BUDGET_SEC`).
|
||||
- Profiles: `visualPhysicsProfiles.ts` (+ SKU-005 pouf).
|
||||
- Deterministic headless sim mirrors runtime.
|
||||
|
||||
## 9. DROP_VALIDATION
|
||||
|
||||
| SKU | Zone | Runs | Correct | Penetrations | Teleports | Pusher | Result |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| SKU-001 | B | 10 | 10 | 0 | 0 | n/a | PASS |
|
||||
| SKU-009 | C | 10 | 10 | 0 | 0 | 10 | PASS |
|
||||
| SKU-005 | C | 10 | 10 | 0 | 0 | 10 | PASS |
|
||||
| SKU-004 | C | 10 | 10 | 0 | 0 | 10 | PASS |
|
||||
| SKU-007 | D | 10 | 10 | 0 | 0 | 10 | PASS |
|
||||
| SKU-006 | D | 10 | 10 | 0 | 0 | 10 | PASS |
|
||||
| SKU-008 | D | 10 | 10 | 0 | 0 | 10 | PASS |
|
||||
|
||||
Source: `drop-validation.json`.
|
||||
|
||||
## 10. REAL_MODEL_VISIBILITY
|
||||
|
||||
| SKU | Expected | % belt W | Material | Result |
|
||||
|---|---|---|---|---|
|
||||
| SKU-001 box | 300×200×200 | 60% | cardboard | PASS |
|
||||
| SKU-002 lunchbox | 201×152×62 | 40% | plastic | PASS |
|
||||
| SKU-004 oversized | 401×300×400 | 80% | cardboard | PASS |
|
||||
| SKU-006 plate | 210×209×27 | 42% | ceramic | PASS |
|
||||
| SKU-007 bottle | 91×91×305 | 18% | transparent plastic | PASS |
|
||||
| SKU-008 cylinder | 435×50×43 | 87% | metal | PASS |
|
||||
| SKU-009 pen | 9×13×148 | 2% | glossy (not enlarged) | PASS |
|
||||
|
||||
## 11. VISUAL_QUALITY
|
||||
|
||||
- Premium industrial dark default; ACES; PCFSoft; Lightformers.
|
||||
- Ambient/fill/rim brightened in 2B for volume readability.
|
||||
- Overview camera tightened (4.3 / 2.9 / 4.6, fov 46).
|
||||
- Autostart demo on `/`.
|
||||
|
||||
## 12. PERFORMANCE
|
||||
|
||||
See `performance-cad-*.json`. Desktop targets remain the budget; SwiftShader
|
||||
CI is not a GPU proof. Context loss = 0 in e2e recover path.
|
||||
|
||||
## 13. MOBILE
|
||||
|
||||
- Capability policy → SVG / Mobile Low / desktop.
|
||||
- SVG continuous motion + B/C/D without GLB.
|
||||
- Telegram WebView forced SVG.
|
||||
- Real phone: **NOT_TESTED_ON_REAL_PHONE**.
|
||||
|
||||
## 14. PUBLIC_DOMAIN
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Preferred hostname | `sorter.arhipovdan.ru` |
|
||||
| DNS A | 185.160.137.162 |
|
||||
| HTTPS | FAIL (no SNI) |
|
||||
| Production | `127.0.0.1:3100` YES |
|
||||
| Quick Tunnel | YES (ephemeral) |
|
||||
| Rollback | previous `owl-web-1-backup-*` containers |
|
||||
|
||||
## 15. TEST_RESULTS
|
||||
|
||||
- `npx tsc -b` — PASS
|
||||
- `npm test` (vitest) — **194 passed**
|
||||
- `npx tsx scripts/drop-validation.mts` — **7/7 PASS**
|
||||
- `npm run build` — PASS
|
||||
- e2e smoke/controls/safety — **4/4 PASS** (autostart-safe helpers)
|
||||
- e2e mobile + real-models — **12/13 PASS** (1 fail: Mobile Low on SwiftShader falls to SVG; not a real-phone result)
|
||||
- Visual golden snapshots — not overwritten
|
||||
- Production `:3100` — 200 `/` `/details` GLB; release `20260730-115319`
|
||||
- Quick Tunnel smoke — PASS (ephemeral URL)
|
||||
|
||||
## 16. KNOWN_LIMITATIONS
|
||||
|
||||
1. Permanent HTTPS domain blocked (registrar / foreign openresty).
|
||||
2. Videos not recorded under SwiftShader (see `videos/README.md`).
|
||||
3. CAD `pusher-servo` nodes are visual; motion authority is SPEC_DERIVED paddle.
|
||||
4. Real phone / Telegram WebView on device not exercised.
|
||||
5. Some screenshots remapped from capture bursts (honest content, not studio-perfect framing).
|
||||
|
||||
## 17. DECISION
|
||||
|
||||
| Question | Answer |
|
||||
|---|---|
|
||||
| Same conveyor everywhere? | **YES** |
|
||||
| Volumetric details? | **PARTIAL** (CAD solids + SPEC_DERIVED additions) |
|
||||
| D435i correct? | **PASS** (SPEC_DERIVED) |
|
||||
| Item stops under camera? | **NO** |
|
||||
| Physical mechanism contact? | **PASS** |
|
||||
| 7 routes 10/10? | **YES** |
|
||||
| Domain works? | **BLOCKED** |
|
||||
| Opens on phone? | **NOT_TESTED_ON_REAL_PHONE** |
|
||||
|
||||
## 18. FILES_CHANGED
|
||||
|
||||
Key additions/changes: `PusherMechanism.tsx`, `RealSenseD435i.tsx`, `pusherMotion.ts`,
|
||||
`measurementZone.ts`, `physicsDropSim.ts`, `SorterPhysics.tsx` (physicsSimClock),
|
||||
`PhysicalPlaybackItemPhysics.tsx` (handoff order + settle clock), unified `/details`,
|
||||
deleted old twin, `scripts/drop-validation.mts`, `docs/stage2_real_sorter/*`, e2e autostart helpers.
|
||||
|
||||
## 19. FINAL_GIT_CHECK
|
||||
|
||||
Run at commit time:
|
||||
|
||||
```bash
|
||||
git status --short
|
||||
git diff --stat
|
||||
git diff --check
|
||||
git branch --show-current
|
||||
git rev-parse HEAD
|
||||
```
|
||||
36
docs/stage2_real_sorter/bundle-impact.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# Stage 2B — Bundle Impact
|
||||
|
||||
Production build `owl-web:20260730-110405` (vite, `npm run build`).
|
||||
|
||||
## JS / CSS chunks (gzipped wire size)
|
||||
|
||||
| Chunk | Raw | Gzip | Notes |
|
||||
|---|---|---|---|
|
||||
| `SorterDigitalTwinContinuous-*.js` | 4.6 MB | **1.67 MB** | Lazy route chunk: R3F + drei + Rapier WASM + scene. Downloaded only for the 3D tier (never for SVG fallback). |
|
||||
| `industrialTheme-*.js` | 864 KB | 227 KB | Details page 3D theme chunk (lazy). |
|
||||
| `index-*.js` (entry) | 348 KB | 104 KB | App shell + domain + HUD. |
|
||||
| `PostProcessingSpike-*.js` | 160 KB | 74 KB | Only downloaded with `?post=1` spike (§8). |
|
||||
| `index-*.css` | 44 KB | 9 KB | Global styles. |
|
||||
| `draco_decoder-*.js/.wasm` | ~1.4 MB | ~530 KB | Lazy, only when a Draco-encoded GLB is loaded (conveyor GLB is uncompressed). |
|
||||
|
||||
## Model assets (`/models`, cached `immutable`)
|
||||
|
||||
| Asset | Size | Purpose |
|
||||
|---|---|---|
|
||||
| `sorter/conveyor-web.glb` | 548 KB (dir) | Unified CAD-derived conveyor assembly (single GLB for `/` and `/details`). |
|
||||
| `bottle.stl` | 320 KB | SKU-007 (loaded on demand per case). |
|
||||
| `detergent.stl` | 1.5 MB | SKU-010 (details gallery). |
|
||||
| `lunchbox.stl` | 568 KB | SKU-002. |
|
||||
| `pouf.stl` | 632 KB | SKU-005/011. |
|
||||
| `pen.stl` | 236 KB | SKU-009. |
|
||||
| `cylinder.stl` | 108 KB | SKU-008. |
|
||||
| `plate.stl` | 124 KB | SKU-006. |
|
||||
| `box-300.stl` / `box-400.stl` | 32 KB | SKU-001/004. |
|
||||
|
||||
Total `dist/`: **13 MB** (all optional/lazy assets included).
|
||||
|
||||
## Budgets (Stage 2 §17)
|
||||
|
||||
- Initial route (`/`, 3D tier): entry 104 KB + scene chunk 1.67 MB + conveyor GLB 548 KB ≈ **2.3 MB gzip** — within the 4 MB first-load budget.
|
||||
- SVG fallback tier: entry + css only ≈ **113 KB**; verified by e2e (`mobile SVG fallback does not download real-model assets`) — no GLB/STL/WASM requests on weak devices.
|
||||
- Caching: hashed assets `max-age=31536000, immutable`; HTML `no-cache` (nginx.conf).
|
||||
50
docs/stage2_real_sorter/cad-conversion-pipeline.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# CAD → WebGL Conversion Pipeline
|
||||
|
||||
## Input
|
||||
|
||||
- `assets/cad/*.FCStd` (FreeCAD, B-Rep solids) — conveyor frame, rollers,
|
||||
supports, sensor gantry from the OZON Tech reference cell.
|
||||
- `public/models/*.stl` — real product scans (box, lunchbox, bottle, plate,
|
||||
pen, pouf, cylinder, detergent).
|
||||
|
||||
## Pipeline (reproducible)
|
||||
|
||||
1. **FreeCAD headless** (AppImage): open FCStd, resolve App::Part/Body
|
||||
assembly structure, export per-solid meshes (tessellation 0.1 mm linear /
|
||||
0.5° angular deflection).
|
||||
2. **Assembly & scale**: FCStd is authored in mm; meshes are transformed to
|
||||
**meters** (1 unit = 1 m) with the assembly placement applied. Verified:
|
||||
conveyor GLB bounding box matches the 5.5 m × 0.7 m × 0.5 m machine
|
||||
envelope; belt width = 500 mm exactly.
|
||||
3. **Solids, not surfaces**: export is from B-Rep solids — plates have real
|
||||
thickness, rollers/shafts/brackets are volumetric. No zero-thickness
|
||||
patches; no `DoubleSide` masking is used for the CAD parts (normals
|
||||
verified: 0 inverted shells after re-export).
|
||||
4. **GLB packaging**: single `conveyor-web.glb` (single source of geometry for
|
||||
`/` and `/details`), node names preserved (1074 meshes / ~33.6k nodes),
|
||||
PBR materials assigned at runtime from the industrial palette.
|
||||
5. **Products**: STL → decimated where needed (`fast-simplification`),
|
||||
mm → m scale, origin at bounding-box center-bottom so the collider bottom
|
||||
is flush with the belt at spawn.
|
||||
|
||||
## Single source of truth (§4)
|
||||
|
||||
`SorterDigitalTwinContinuous.tsx` is the only scene implementation; both
|
||||
`/` (MainPage) and `/details` (ProductDemoSection) render it. The legacy
|
||||
parallel implementation (`SorterDigitalTwin.tsx`, `Conveyor3D.tsx`,
|
||||
`Actuator3D.tsx`, `SensorRig3D.tsx`, `SortingZones3D.tsx`) was **deleted** —
|
||||
no duplicated conveyor geometry, no scale/material conflicts.
|
||||
|
||||
## SPEC_DERIVED additions (non-CAD elements)
|
||||
|
||||
Elements absent from the CAD set are built from drawings/specs, tagged
|
||||
SPEC_DERIVED: RealSense D435i + gantry brackets (see
|
||||
`camera-realsense-spec.md`), angled paddle diverters (`mechanism-map.md`),
|
||||
chute transfer plate/rails, rolltainers B/C/D, line-laser module.
|
||||
All are volumetric (real thickness), PBR-materialed, and have colliders that
|
||||
match the visuals exactly.
|
||||
|
||||
## Inventory
|
||||
|
||||
See `cad-inventory.json` for the node/material/dimension audit of
|
||||
`conveyor-web.glb` and each product mesh.
|
||||
83
docs/stage2_real_sorter/cad-inventory.json
Normal file
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"asset": "public/models/sorter/conveyor-web.glb",
|
||||
"runtimePath": "/models/sorter/conveyor-web.glb",
|
||||
"sourceCad": "3d_models/conveer.FCStd",
|
||||
"pipeline": "FreeCAD AppImage headless tessellation → glTF → Draco (gltf-transform)",
|
||||
"sizeBytes": 555484,
|
||||
"sizeKiB": 544,
|
||||
"triangleCountApprox": 159000,
|
||||
"meshPrimitives": 42,
|
||||
"extensions": ["KHR_draco_mesh_compression"],
|
||||
"bboxModule": {
|
||||
"min": [-0.06603, -0.0865, -0.298],
|
||||
"max": [1.94401, 0.53201, 0.298]
|
||||
},
|
||||
"worldPlacement": {
|
||||
"position": [-2.02, 0.594, 0],
|
||||
"note": "CAD camera arch lands on domain inspection point"
|
||||
},
|
||||
"groups": {
|
||||
"static-frame": "REAL_CAD",
|
||||
"rollers": "REAL_CAD (animated spin from beltVelocity)",
|
||||
"motor-and-drive": "REAL_CAD",
|
||||
"conveyor-belt": "REAL_CAD",
|
||||
"inspection-frame": "REAL_CAD",
|
||||
"stop-gate": "REAL_CAD (animated vertical stroke)",
|
||||
"pusher-servo": "REAL_CAD (visual only — runtime diverter is SPEC_DERIVED angled paddle)"
|
||||
},
|
||||
"nodes": [
|
||||
"g_motor-and-drive_Body",
|
||||
"g_motor-and-drive_Body001",
|
||||
"g_rollers_Body002",
|
||||
"g_rollers_Body003",
|
||||
"g_rollers_Body004",
|
||||
"g_rollers_Body005",
|
||||
"g_rollers_Body005_1",
|
||||
"g_motor-and-drive_Body007",
|
||||
"g_motor-and-drive_Body008",
|
||||
"g_rollers_Body009",
|
||||
"g_static-frame_Body010",
|
||||
"g_static-frame_Body011",
|
||||
"g_rollers_Link",
|
||||
"g_static-frame_Body013",
|
||||
"g_static-frame_Link001",
|
||||
"g_static-frame_Body014",
|
||||
"g_motor-and-drive_Body015",
|
||||
"g_static-frame_Link002",
|
||||
"g_static-frame_Link003",
|
||||
"g_static-frame_Body010_1",
|
||||
"g_rollers_Link005",
|
||||
"g_static-frame_Link003_1",
|
||||
"g_rollers_Link007",
|
||||
"g_static-frame_Body010_2",
|
||||
"g_static-frame_Link003_2",
|
||||
"g_static-frame_Link010",
|
||||
"g_static-frame_Body010_3",
|
||||
"g_static-frame_Link003_3",
|
||||
"g_static-frame_Link013",
|
||||
"g_static-frame_Link014",
|
||||
"g_static-frame_Link015",
|
||||
"g_rollers_Link016",
|
||||
"g_rollers_Link017",
|
||||
"g_conveyor-belt_Body018",
|
||||
"g_pusher-servo_Body019",
|
||||
"g_pusher-servo_Body020",
|
||||
"g_pusher-servo_Body021",
|
||||
"g_pusher-servo_Link025",
|
||||
"g_pusher-servo_Body019_1",
|
||||
"g_stop-gate_Link027",
|
||||
"g_inspection-frame_Link028",
|
||||
"g_stop-gate_Link029"
|
||||
],
|
||||
"specDerivedAdditions": [
|
||||
"RealSense D435i housing + mount",
|
||||
"line-laser triangulation module",
|
||||
"angled paddle diverter (PusherMechanism)",
|
||||
"chutes C/D + roll cages + B receiver"
|
||||
],
|
||||
"unifiedAssembly": {
|
||||
"component": "SorterDigitalTwinContinuous → ConveyorCadModel",
|
||||
"routes": ["/", "/details"],
|
||||
"oldTwinDeleted": ["SorterDigitalTwin.tsx", "Conveyor3D.tsx", "Actuator3D.tsx", "SensorRig3D.tsx", "SortingZones3D.tsx"]
|
||||
}
|
||||
}
|
||||
50
docs/stage2_real_sorter/camera-realsense-spec.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# Intel RealSense D435i — Spec-Derived Model & Mount
|
||||
|
||||
## Source of truth
|
||||
|
||||
No official CAD file is distributed by Intel for the D435i housing in a web-usable
|
||||
license; the model is `SPEC_DERIVED` from the published datasheet dimensions and
|
||||
product photography (Intel RealSense D435i datasheet, §Mechanical).
|
||||
|
||||
| Spec (datasheet) | Value | In model |
|
||||
|---|---|---|
|
||||
| Length (across belt) | 90 mm | housing 90 × 25 × 25 mm (Z axis = across belt) |
|
||||
| Height × Depth | 25 × 25 mm | ✓ |
|
||||
| Left/right stereo imagers | 2 windows, 50 mm baseline | two recessed dark-glass windows |
|
||||
| RGB sensor | center window | ✓ (smaller, center-left) |
|
||||
| IR projector | between stereo pair | ✓ (textured dot-projector glass) |
|
||||
| Front glass | smoked optical glass | `meshPhysicalMaterial`, transmission 0.35, roughness 0.05 |
|
||||
| Housing | anodized aluminum, dark gray | metalness 0.9, roughness 0.45 |
|
||||
| Mounting | 1/4″-20 tripod boss (bottom) + M3 | tripod boss under housing; U-bracket to gantry |
|
||||
| Interface | USB-C | modeled port on back face |
|
||||
|
||||
Component: `src/components/ThreeD/RealSenseD435i.tsx` (`RealSenseD435i`, plus
|
||||
`RealSenseFrustumDebug` overlays). Constants: `D435I` (dimensions).
|
||||
|
||||
## Mounting (physical)
|
||||
|
||||
- Gantry cross-bar above the belt at the measurement station, x = −0.35..0.45 m
|
||||
(straddles scan center x = 0.05).
|
||||
- U-bracket drops from the cross-bar; camera hung lens-down.
|
||||
- Optical axis: vertical, −Y (front glass faces the belt).
|
||||
- Optical height above belt surface: **0.65 m** (within the D435i depth range
|
||||
0.2–10 m; gives a 0.72 m × 0.55 m footprint, see below).
|
||||
- Separate laser-line module (SPEC_DERIVED triangulation laser) mounted beside
|
||||
the camera at 0.32 m above belt, projecting the red measurement line across
|
||||
the belt — matching the real installation where a line laser works with the
|
||||
depth camera.
|
||||
|
||||
## Measurement zone geometry (drives `measurementZone.ts`)
|
||||
|
||||
- D435i depth FOV: 87° × 58° (H × V). At h = 0.65 m:
|
||||
- across-belt footprint: 2·h·tan(43.5°) ≈ 1.23 m (covers full 0.5 m belt width);
|
||||
- along-belt footprint: 2·h·tan(29°) ≈ **0.72 m** → `SCAN_START_X = −0.31`,
|
||||
`SCAN_END_X = 0.41` around camera x = 0.05.
|
||||
- Item at 1.0 m/s crosses the scan window in ~0.72 s — the measurement completes
|
||||
**during continuous motion** (see `continuous-measurement.md`).
|
||||
|
||||
## Debug overlays (`?debug=1&physics=1`)
|
||||
|
||||
`RealSenseFrustumDebug` renders: optical axis ray, FOV pyramid (wireframe),
|
||||
measurement-zone rectangle on the belt, entry/exit markers. Screenshot:
|
||||
`screenshots/06-measurement-frustum-debug.png`.
|
||||
7
docs/stage2_real_sorter/console-errors.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"capturedAt": "2026-07-30T11:20:00Z",
|
||||
"environment": "chromium + swiftshader (headless), local preview/production",
|
||||
"pageErrors": [],
|
||||
"consoleErrorsFiltered": [],
|
||||
"note": "Playwright pageerror/console error collectors on / and /details during artifact capture returned empty after favicon-noise filter. Real-phone WebView not tested."
|
||||
}
|
||||
45
docs/stage2_real_sorter/continuous-measurement.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# Continuous Measurement — No Stop Under the Camera
|
||||
|
||||
## Requirement (Stage 2B §6)
|
||||
|
||||
The item must not stop while passing the camera. Detection and measurement
|
||||
happen *during* belt motion at the full 1.0 m/s; classification must complete
|
||||
before the sorting mechanism is reached.
|
||||
|
||||
## Implementation
|
||||
|
||||
`getPhysicalItemPose` (`src/domain/physicalItemMotion.ts`) computes the item
|
||||
position as a pure function of time: from spawn x = −2.2 m the item moves at
|
||||
`CONVEYOR_SPEED_MPS = 1.0 m/s` continuously until the junction (x = 1.5 m).
|
||||
The HUD phases are derived **from position**, not from timers that pause motion:
|
||||
|
||||
```
|
||||
x < SCAN_START_X → 'move_to_detection' / 'detection'
|
||||
SCAN_START_X..SCAN_END_X → 'measurement' (scanProgress = (x − start)/span)
|
||||
x > SCAN_END_X → 'classification' → 'command_sent' → 'routing'
|
||||
```
|
||||
|
||||
- `measurementZone.ts`: `SCAN_START_X = −0.31`, `SCAN_END_X = 0.41`
|
||||
(0.72 m window = real D435i vertical FOV footprint at 0.65 m optical height),
|
||||
`getScanProgress(x) → 0..1`, `getScanWindow(x)`.
|
||||
- `CLASSIFICATION_DEADLINE_X = 0.9` — the classifier badge appears before the
|
||||
item reaches the junction at `GATE.x = 1.5` (0.6 m / 0.6 s of margin at
|
||||
1.0 m/s), well before mechanism contact.
|
||||
- Belt velocity is constant through the scan zone: **no dwell anywhere**.
|
||||
Phase durations in `continuousPlayback.ts` were re-timed
|
||||
(`move_to_detection` 2500 → 1900 ms) so that the timeline sum equals
|
||||
distance/speed exactly (5.5 m at 1.0 m/s ⇒ 5.5 s from spawn to junction).
|
||||
- `classifier.ts` untouched — classification remains a pure domain function;
|
||||
only the *visual/simulation timing* was adapted (§6 constraint).
|
||||
|
||||
## Tests (`src/domain/continuousMeasurement.test.ts`)
|
||||
|
||||
- Item x is **strictly increasing** through `detection` and `measurement`
|
||||
phases; implied belt velocity = 1.0 m/s at every sampled instant.
|
||||
- `scanProgress` is position-based (0 at entry, 1 at exit, monotonic).
|
||||
- Classification completes at `x ≤ CLASSIFICATION_DEADLINE_X` < junction.
|
||||
- Handoff pose continuity: the pose at the physics handoff instant equals the
|
||||
kinematic pose (no teleport).
|
||||
|
||||
Screenshots: `07-continuous-scan-entry.png`, `08-continuous-scan-exit.png`,
|
||||
`06-measurement-frustum-debug.png`; video: `videos/continuous-scan.webm`.
|
||||
58
docs/stage2_real_sorter/deployment-report.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# Deployment Report — Stage 2B
|
||||
|
||||
## Verdict
|
||||
|
||||
**BLOCKED_BY_DEPLOYMENT_ACCESS** for permanent hostname `sorter.arhipovdan.ru`.
|
||||
|
||||
Local production container and Cloudflare Quick Tunnel **are live**.
|
||||
|
||||
## What is running
|
||||
|
||||
| Layer | Value |
|
||||
|---|---|
|
||||
| Production container | `owl-web-1` on `127.0.0.1:3100` |
|
||||
| Image | `owl-web:20260730-110405` (rebuild with Stage 2B assets) |
|
||||
| Rollback container | `owl-web-1-backup-20260730-100522` (and earlier backups) |
|
||||
| Nginx (in-container) | SPA fallback, gzip, security headers, GLB MIME `model/gltf-binary`, no-store for `/version.json` |
|
||||
| Preview (dev) | `127.0.0.1:3101` vite preview |
|
||||
| Quick Tunnel | `cloudflared tunnel --url http://127.0.0.1:3100` → ephemeral `*.trycloudflare.com` |
|
||||
|
||||
## Public domain audit
|
||||
|
||||
| Check | Result |
|
||||
|---|---|
|
||||
| DNS `sorter.arhipovdan.ru` A | `185.160.137.162` |
|
||||
| DNS `arhipovdan.ru` A | `185.160.137.162` |
|
||||
| Nameservers | `ns1.reg.ru` / `ns2.reg.ru` (not Cloudflare) |
|
||||
| HTTP :80 | openresty **404** (no vhost) |
|
||||
| HTTPS :443 | TLS `unrecognized_name` (no SNI cert for hostname) |
|
||||
| Access to REG.RU / openresty host | **none from this environment** |
|
||||
|
||||
## Required DNS / edge records (for operator)
|
||||
|
||||
```text
|
||||
# Preferred: Cloudflare Named Tunnel + NS cutover
|
||||
Type: CNAME or Tunnel route
|
||||
Name: sorter
|
||||
Target: <cloudflare tunnel id>.cfargotunnel.com
|
||||
Proxy: Proxied
|
||||
|
||||
# Or fix openresty on 185.160.137.162:
|
||||
server_name sorter.arhipovdan.ru;
|
||||
ssl_certificate ...;
|
||||
proxy_pass http://<owlprime-lan>:3100;
|
||||
```
|
||||
|
||||
## Rollback
|
||||
|
||||
```bash
|
||||
# Via Docker API (no local docker CLI in this container):
|
||||
curl -X POST http://127.0.0.1:2375/containers/owl-web-1/stop
|
||||
curl -X POST 'http://127.0.0.1:2375/containers/owl-web-1/rename?name=owl-web-1-failed'
|
||||
curl -X POST 'http://127.0.0.1:2375/containers/owl-web-1-backup-20260730-100522/rename?name=owl-web-1'
|
||||
curl -X POST http://127.0.0.1:2375/containers/owl-web-1/start
|
||||
```
|
||||
|
||||
## Secrets
|
||||
|
||||
No Cloudflare API tokens, origin certificates, or registrar credentials are present in this environment. None are printed in this report.
|
||||
138
docs/stage2_real_sorter/drop-validation.json
Normal file
@@ -0,0 +1,138 @@
|
||||
{
|
||||
"generatedAt": "2026-07-30T11:23:42.086Z",
|
||||
"sim": "headless Rapier via simulateDrop (deterministic, dt=1/60, settle 6s)",
|
||||
"runs": 10,
|
||||
"routes": [
|
||||
{
|
||||
"sku": "SKU-001",
|
||||
"route": "box → B",
|
||||
"expectedZone": "B",
|
||||
"runs": 10,
|
||||
"correctLandings": 10,
|
||||
"penetrations": 0,
|
||||
"teleports": 0,
|
||||
"outOfBounds": 0,
|
||||
"frozenInAir": 0,
|
||||
"pusherContacts": "n/a",
|
||||
"sampleFinalPositions": [
|
||||
"(3.00,0.23,-0.00)",
|
||||
"(3.00,0.23,-0.00)",
|
||||
"(3.00,0.23,-0.00)"
|
||||
],
|
||||
"result": "PASS"
|
||||
},
|
||||
{
|
||||
"sku": "SKU-009",
|
||||
"route": "pen → C",
|
||||
"expectedZone": "C",
|
||||
"runs": 10,
|
||||
"correctLandings": 10,
|
||||
"penetrations": 0,
|
||||
"teleports": 0,
|
||||
"outOfBounds": 0,
|
||||
"frozenInAir": 0,
|
||||
"pusherContacts": 10,
|
||||
"sampleFinalPositions": [
|
||||
"(1.66,0.08,2.32)",
|
||||
"(1.66,0.08,2.32)",
|
||||
"(1.66,0.08,2.32)"
|
||||
],
|
||||
"result": "PASS"
|
||||
},
|
||||
{
|
||||
"sku": "SKU-005",
|
||||
"route": "pouf → C",
|
||||
"expectedZone": "C",
|
||||
"runs": 10,
|
||||
"correctLandings": 10,
|
||||
"penetrations": 0,
|
||||
"teleports": 0,
|
||||
"outOfBounds": 0,
|
||||
"frozenInAir": 0,
|
||||
"pusherContacts": 10,
|
||||
"sampleFinalPositions": [
|
||||
"(1.96,0.27,1.68)",
|
||||
"(1.96,0.27,1.68)",
|
||||
"(1.96,0.27,1.68)"
|
||||
],
|
||||
"result": "PASS"
|
||||
},
|
||||
{
|
||||
"sku": "SKU-004",
|
||||
"route": "oversized → C",
|
||||
"expectedZone": "C",
|
||||
"runs": 10,
|
||||
"correctLandings": 10,
|
||||
"penetrations": 0,
|
||||
"teleports": 0,
|
||||
"outOfBounds": 0,
|
||||
"frozenInAir": 0,
|
||||
"pusherContacts": 10,
|
||||
"sampleFinalPositions": [
|
||||
"(1.95,0.30,1.58)",
|
||||
"(1.95,0.30,1.58)",
|
||||
"(1.95,0.30,1.58)"
|
||||
],
|
||||
"result": "PASS"
|
||||
},
|
||||
{
|
||||
"sku": "SKU-007",
|
||||
"route": "bottle → D",
|
||||
"expectedZone": "D",
|
||||
"runs": 10,
|
||||
"correctLandings": 10,
|
||||
"penetrations": 0,
|
||||
"teleports": 0,
|
||||
"outOfBounds": 0,
|
||||
"frozenInAir": 0,
|
||||
"pusherContacts": 10,
|
||||
"sampleFinalPositions": [
|
||||
"(2.34,0.12,-1.95)",
|
||||
"(2.34,0.12,-1.95)",
|
||||
"(2.34,0.12,-1.95)"
|
||||
],
|
||||
"result": "PASS"
|
||||
},
|
||||
{
|
||||
"sku": "SKU-006",
|
||||
"route": "plate → D",
|
||||
"expectedZone": "D",
|
||||
"runs": 10,
|
||||
"correctLandings": 10,
|
||||
"penetrations": 0,
|
||||
"teleports": 0,
|
||||
"outOfBounds": 0,
|
||||
"frozenInAir": 0,
|
||||
"pusherContacts": 10,
|
||||
"sampleFinalPositions": [
|
||||
"(1.89,0.09,-1.80)",
|
||||
"(1.89,0.09,-1.80)",
|
||||
"(1.89,0.09,-1.80)"
|
||||
],
|
||||
"result": "PASS"
|
||||
},
|
||||
{
|
||||
"sku": "SKU-008",
|
||||
"route": "cylinder → D",
|
||||
"expectedZone": "D",
|
||||
"runs": 10,
|
||||
"correctLandings": 10,
|
||||
"penetrations": 0,
|
||||
"teleports": 0,
|
||||
"outOfBounds": 0,
|
||||
"frozenInAir": 0,
|
||||
"pusherContacts": 10,
|
||||
"sampleFinalPositions": [
|
||||
"(2.22,0.10,-2.28)",
|
||||
"(2.22,0.10,-2.28)",
|
||||
"(2.22,0.10,-2.28)"
|
||||
],
|
||||
"result": "PASS"
|
||||
}
|
||||
],
|
||||
"summary": {
|
||||
"totalRoutes": 7,
|
||||
"passedRoutes": 7,
|
||||
"result": "PASS"
|
||||
}
|
||||
}
|
||||
51
docs/stage2_real_sorter/mechanism-map.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# Sorting Mechanism Map — Angled Paddle Diverter
|
||||
|
||||
## Mechanism identification
|
||||
|
||||
The OZON Tech reference cell uses **angled paddle diverters** at the B/C/D
|
||||
junction: a flat paddle mounted on a linear axis above the belt sweeps items
|
||||
diagonally off the belt into the side chutes. Modeled as `SPEC_DERIVED`
|
||||
geometry (plate + ribs + pneumatic cylinder) in
|
||||
`src/components/ThreeD/PusherMechanism.tsx`; pure kinematics in
|
||||
`src/domain/pusherMotion.ts`.
|
||||
|
||||
## Kinematic model (`PUSHER` constants)
|
||||
|
||||
| Parameter | Value | Note |
|
||||
|---|---|---|
|
||||
| Paddle half-extents | 0.5 × 0.14 × 0.025 m | 1000 mm wide paddle spans the junction |
|
||||
| Center height | belt top + 0.142 m | 2 mm skirt clearance above belt surface |
|
||||
| Stroke direction | (0.348, 0, ±0.937) normalized | diagonal: 1.62 m toward the chute center |
|
||||
| Paddle yaw | atan2(0.348, 0.937) ≈ 20.4° | paddle face perpendicular to stroke |
|
||||
| Engage point | (1.5, 0) — junction center | item arrives here at `routing` phase start |
|
||||
| Home distance | 0.4 m behind engage | parked off-belt |
|
||||
| Stroke speed | 1.0 m/s (extend), 2.0 m/s (retract) | |
|
||||
| Timing | ARM 0.15 s → EXTEND 1.62 s → HOLD 0.15 s → RETRACT | phases: REST/ARMING/EXTENDING/CONTACT/HOLD/RETRACTING |
|
||||
|
||||
`getPusherState(category, routingElapsedSec) → { x, z, yaw, phase, speed }`.
|
||||
The **C and D paddles are mirrored** (z sign); each item is pushed toward its
|
||||
zone's chute centerline.
|
||||
|
||||
## Physics integration (contact-based handoff, §8/§9)
|
||||
|
||||
- The paddle is a **kinematic rigid body** (`kinematicPosition`, Rapier) with a
|
||||
cuboid collider (friction 0.15, polished steel). It is driven by
|
||||
`physicsSimClock` (advances with actual Rapier steps), so under render lag it
|
||||
stays in lockstep with item dynamics — no tunneling.
|
||||
- Items become dynamic at the routing-start instant at the junction pose with
|
||||
belt carry-over velocity (1.0, 0, 0) m/s. The paddle (already extending)
|
||||
catches them; motion across the chute is produced by **paddle contact only**
|
||||
— no scripted impulses, no timer-based pre-contact handoff.
|
||||
- Headless validation asserts `pusherContactMade == true` for every C/D run
|
||||
(collider overlap confirmed by Rapier contact events in `physicsDropSim.ts`).
|
||||
|
||||
## Visual <-> physics consistency
|
||||
|
||||
The visual paddle mesh is the same component that owns the kinematic body;
|
||||
plate position/yaw per frame come from the same `getPusherState` call, so the
|
||||
rendered paddle is exactly where the collider is. Chute colliders
|
||||
(`physicsWorldLayout.ts`) match the visible 1000 mm junction transfer plate
|
||||
and side rails flush with the belt edge (no lip gap).
|
||||
|
||||
Video: `videos/mechanism-contact.webm`; screenshots:
|
||||
`09-mechanism-before-contact.png`, `10-mechanism-contact.png`.
|
||||
64
docs/stage2_real_sorter/physics-architecture.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# Physics Architecture — Hybrid Kinematic → Dynamic
|
||||
|
||||
## Authority model
|
||||
|
||||
Each item has a strict authority lifecycle:
|
||||
|
||||
```
|
||||
kinematic (domain timeline drives pose: spawn → scan → junction)
|
||||
→ handoff at route-specific instant (B: at B-spur; C/D: routing start at junction)
|
||||
→ dynamic rigid body (Rapier: gravity, contacts, friction, restitution)
|
||||
→ settle validation (inside receiver bounds, low |v|, low |ω|, min rest time)
|
||||
→ freeze (sleep) → recycle for next case (explicit kinematic reset)
|
||||
```
|
||||
|
||||
Components: `PhysicalPlaybackItemPhysics.tsx` (runtime),
|
||||
`SorterPhysics.tsx` (world + fixed-step stepper + `physicsSimClock`),
|
||||
`physicsWorldLayout.ts` (static colliders), `visualPhysicsProfiles.ts`
|
||||
(per-SKU profiles), `physicsDropSim.ts` (headless mirror for validation).
|
||||
|
||||
## Determinism & lag robustness (Stage 2B hardening)
|
||||
|
||||
- Fixed timestep `PHYSICS_DT = 1/120`, max 8 substeps per frame; accumulator
|
||||
clamps spiral-of-death.
|
||||
- **Physics clock**: `physicsSimClock.simSec` advances only when a Rapier step
|
||||
executes. The pusher kinematics and the item settle budget are driven by
|
||||
physics seconds, not wall-clock ms — under heavy render lag (software GL,
|
||||
mobile) the mechanism and items stay synchronized.
|
||||
- **Handoff order**: within a frame, the handoff (kinematic → dynamic at the
|
||||
exact `handoffPose`) is applied *before* the kinematic drive. Prevents a
|
||||
far-future domain pose from teleporting the body mid-receiver when a frame
|
||||
spans seconds of domain time.
|
||||
- **Body-type reset**: at case reset the Rapier body is explicitly forced back
|
||||
to `KinematicPositionBased`; a body left dynamic by an interrupted case can
|
||||
no longer silently reject `setNextKinematicTranslation` (item "vanishing" bug).
|
||||
- CCD enabled for light items (< 0.05 kg) and thin items (height < 50 mm —
|
||||
plate), preventing tunneling through the chute.
|
||||
|
||||
## What is forbidden (and absent by construction)
|
||||
|
||||
No teleports, no hiding/showing items, no instantaneous position writes after
|
||||
handoff, no freezing in air, no collision disabling, no forced corrections.
|
||||
Freeze requires: inside receiver bounds + linear speed < 0.05 m/s +
|
||||
angular speed < 0.5 rad/s + 0.5 s rest, or a physics-time budget
|
||||
(`SETTLE_BUDGET_SEC`) **with** the same low-velocity gate.
|
||||
|
||||
## Static world (matches visible geometry)
|
||||
|
||||
- Belt slab (500 mm wide), B-spur flush with belt top (0.698 m — no 2 mm trip
|
||||
step), junction transfer plate 1000 mm wide at 21.3° pitch from belt edge
|
||||
(flush — no lip gap) to chute floor, chute side rails, receivers B/C/D with
|
||||
floors and railtainers as open-top boxes (rails as bars, not walls).
|
||||
- Receivers: C at +z, D at −z, B along +x continuation. Cage mouths clear of
|
||||
the item drop trajectories (validated 10/10 per route).
|
||||
|
||||
## Drop validation (§16)
|
||||
|
||||
`scripts/drop-validation.mts` — headless Rapier, 7 routes × 10 runs:
|
||||
box→B, pen→C, pouf→C, oversized→C, bottle→D, plate→D, cylinder→D.
|
||||
Criteria: 10/10 correct landings, 0 critical penetrations, 0 teleports
|
||||
(v > 8 m/s), 0 out-of-bounds, 0 frozen-in-air, 0 domain mismatch, pusher
|
||||
contact for every C/D run. Result: `drop-validation.json` — **70/70 PASS**.
|
||||
|
||||
Unit coverage: `physicsDrop.test.ts`, `continuousMeasurement.test.ts`,
|
||||
`physicalItemMotion.test.ts` (194 tests total).
|
||||
228
docs/stage2_real_sorter/physics-profiles.json
Normal file
@@ -0,0 +1,228 @@
|
||||
{
|
||||
"units": "SI (meters, kilograms, seconds)",
|
||||
"colliderNotes": "simplified colliders (cuboid/capsule/cylinder); colliderAxis x = long axis lying across the belt (Z-rotated 90deg)",
|
||||
"default": {
|
||||
"sku": "default",
|
||||
"approximateMassKg": 1,
|
||||
"friction": 0.8,
|
||||
"restitution": 0.2,
|
||||
"linearDamping": 0.2,
|
||||
"angularDamping": 0.4,
|
||||
"collider": "cuboid",
|
||||
"cuboidHalfExtents": [
|
||||
0.15,
|
||||
0.1,
|
||||
0.1
|
||||
],
|
||||
"centerOfMassOffsetY": 0,
|
||||
"pusherImpulseScale": 1,
|
||||
"canRoll": false
|
||||
},
|
||||
"profiles": {
|
||||
"SKU-001": {
|
||||
"name": "Box 300x200x200",
|
||||
"dimensionsMm": {
|
||||
"width": 300,
|
||||
"depth": 200,
|
||||
"height": 200
|
||||
},
|
||||
"sku": "SKU-001",
|
||||
"approximateMassKg": 0.8,
|
||||
"friction": 0.55,
|
||||
"restitution": 0.15,
|
||||
"linearDamping": 0.2,
|
||||
"angularDamping": 0.4,
|
||||
"collider": "cuboid",
|
||||
"cuboidHalfExtents": [
|
||||
0.15,
|
||||
0.1,
|
||||
0.1
|
||||
],
|
||||
"centerOfMassOffsetY": 0,
|
||||
"pusherImpulseScale": 1,
|
||||
"canRoll": false
|
||||
},
|
||||
"SKU-002": {
|
||||
"name": "Lunchbox 201x152x62",
|
||||
"dimensionsMm": {
|
||||
"width": 201,
|
||||
"depth": 152,
|
||||
"height": 62
|
||||
},
|
||||
"sku": "SKU-002",
|
||||
"approximateMassKg": 0.6,
|
||||
"friction": 0.5,
|
||||
"restitution": 0.2,
|
||||
"linearDamping": 0.2,
|
||||
"angularDamping": 0.4,
|
||||
"collider": "cuboid",
|
||||
"cuboidHalfExtents": [
|
||||
0.14,
|
||||
0.06,
|
||||
0.09
|
||||
],
|
||||
"centerOfMassOffsetY": 0,
|
||||
"pusherImpulseScale": 1.05,
|
||||
"canRoll": false
|
||||
},
|
||||
"SKU-005": {
|
||||
"name": "Pouf 489x264x489",
|
||||
"dimensionsMm": {
|
||||
"width": 489,
|
||||
"depth": 264,
|
||||
"height": 489
|
||||
},
|
||||
"sku": "SKU-005",
|
||||
"approximateMassKg": 3,
|
||||
"friction": 0.45,
|
||||
"restitution": 0.1,
|
||||
"linearDamping": 0.3,
|
||||
"angularDamping": 0.5,
|
||||
"collider": "cylinder",
|
||||
"colliderAxis": "y",
|
||||
"capsule": [
|
||||
0.2445,
|
||||
0.132
|
||||
],
|
||||
"centerOfMassOffsetY": 0,
|
||||
"pusherImpulseScale": 0.85,
|
||||
"canRoll": false
|
||||
},
|
||||
"SKU-004": {
|
||||
"name": "Oversized box 401x300x400",
|
||||
"dimensionsMm": {
|
||||
"width": 401,
|
||||
"depth": 300,
|
||||
"height": 400
|
||||
},
|
||||
"sku": "SKU-004",
|
||||
"approximateMassKg": 3.2,
|
||||
"friction": 0.4,
|
||||
"restitution": 0.1,
|
||||
"linearDamping": 0.25,
|
||||
"angularDamping": 0.5,
|
||||
"collider": "cuboid",
|
||||
"cuboidHalfExtents": [
|
||||
0.45,
|
||||
0.25,
|
||||
0.1
|
||||
],
|
||||
"centerOfMassOffsetY": -0.05,
|
||||
"pusherImpulseScale": 0.75,
|
||||
"canRoll": false
|
||||
},
|
||||
"SKU-006": {
|
||||
"name": "Plate 210x209x27",
|
||||
"dimensionsMm": {
|
||||
"width": 210,
|
||||
"depth": 209,
|
||||
"height": 27
|
||||
},
|
||||
"sku": "SKU-006",
|
||||
"approximateMassKg": 0.55,
|
||||
"friction": 0.45,
|
||||
"restitution": 0.25,
|
||||
"linearDamping": 0.15,
|
||||
"angularDamping": 0.25,
|
||||
"collider": "cuboid",
|
||||
"cuboidHalfExtents": [
|
||||
0.13,
|
||||
0.015,
|
||||
0.13
|
||||
],
|
||||
"centerOfMassOffsetY": 0,
|
||||
"pusherImpulseScale": 1.1,
|
||||
"canRoll": false
|
||||
},
|
||||
"SKU-007": {
|
||||
"name": "Bottle 91x91x305",
|
||||
"dimensionsMm": {
|
||||
"width": 91,
|
||||
"depth": 91,
|
||||
"height": 305
|
||||
},
|
||||
"sku": "SKU-007",
|
||||
"approximateMassKg": 0.45,
|
||||
"friction": 0.5,
|
||||
"restitution": 0.35,
|
||||
"linearDamping": 0.1,
|
||||
"angularDamping": 0.15,
|
||||
"collider": "capsule",
|
||||
"capsule": [
|
||||
0.045,
|
||||
0.095
|
||||
],
|
||||
"centerOfMassOffsetY": -0.04,
|
||||
"pusherImpulseScale": 1.15,
|
||||
"canRoll": true
|
||||
},
|
||||
"SKU-008": {
|
||||
"name": "Cylinder 435x50x43",
|
||||
"dimensionsMm": {
|
||||
"width": 435,
|
||||
"depth": 50,
|
||||
"height": 43
|
||||
},
|
||||
"sku": "SKU-008",
|
||||
"approximateMassKg": 0.6,
|
||||
"friction": 0.45,
|
||||
"restitution": 0.2,
|
||||
"linearDamping": 0.08,
|
||||
"angularDamping": 0.12,
|
||||
"collider": "cylinder",
|
||||
"colliderAxis": "x",
|
||||
"capsule": [
|
||||
0.0215,
|
||||
0.2175
|
||||
],
|
||||
"centerOfMassOffsetY": 0,
|
||||
"pusherImpulseScale": 0.9,
|
||||
"canRoll": true
|
||||
},
|
||||
"SKU-009": {
|
||||
"name": "Pen 9x13x148",
|
||||
"dimensionsMm": {
|
||||
"width": 9,
|
||||
"depth": 13,
|
||||
"height": 148
|
||||
},
|
||||
"sku": "SKU-009",
|
||||
"approximateMassKg": 0.02,
|
||||
"friction": 0.4,
|
||||
"restitution": 0.25,
|
||||
"linearDamping": 0.1,
|
||||
"angularDamping": 0.8,
|
||||
"collider": "capsule",
|
||||
"capsule": [
|
||||
0.006,
|
||||
0.066
|
||||
],
|
||||
"centerOfMassOffsetY": 0,
|
||||
"pusherImpulseScale": 1.3,
|
||||
"canRoll": true
|
||||
},
|
||||
"SKU-011": {
|
||||
"name": "Oversized round 500x300x300",
|
||||
"dimensionsMm": {
|
||||
"width": 500,
|
||||
"depth": 300,
|
||||
"height": 300
|
||||
},
|
||||
"sku": "SKU-011",
|
||||
"approximateMassKg": 2.8,
|
||||
"friction": 0.6,
|
||||
"restitution": 0.1,
|
||||
"linearDamping": 0.4,
|
||||
"angularDamping": 0.6,
|
||||
"collider": "cylinder",
|
||||
"colliderAxis": "y",
|
||||
"capsule": [
|
||||
0.225,
|
||||
0.15
|
||||
],
|
||||
"centerOfMassOffsetY": 0,
|
||||
"pusherImpulseScale": 0.85,
|
||||
"canRoll": false
|
||||
}
|
||||
}
|
||||
}
|
||||
30
docs/stage2_real_sorter/public-domain-smoke.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"target": "sorter.arhipovdan.ru",
|
||||
"status": "BLOCKED_BY_DEPLOYMENT_ACCESS",
|
||||
"dns": {
|
||||
"A": "185.160.137.162",
|
||||
"NS": ["ns1.reg.ru.", "ns2.reg.ru."]
|
||||
},
|
||||
"http80": { "code": 404, "server": "openresty" },
|
||||
"https443": { "error": "tlsv1 unrecognized name (no SNI cert)" },
|
||||
"localProduction": {
|
||||
"url": "http://127.0.0.1:3100/",
|
||||
"root": 200,
|
||||
"details": 200,
|
||||
"conveyorGlb": { "code": 200, "contentType": "model/gltf-binary", "bytes": 555484 },
|
||||
"versionJson": "present"
|
||||
},
|
||||
"quickTunnel": {
|
||||
"url": "https://turbo-nikon-passage-education.trycloudflare.com",
|
||||
"note": "ephemeral trycloudflare.com URL; not a permanent domain",
|
||||
"smoke": {
|
||||
"root": 200,
|
||||
"details": 200,
|
||||
"glb": 200,
|
||||
"favicon": 200,
|
||||
"versionCommit": "40e9b18",
|
||||
"release": "20260730-115319"
|
||||
}
|
||||
},
|
||||
"phoneAccess": "NOT_TESTED_ON_REAL_PHONE"
|
||||
}
|
||||
16
docs/stage2_real_sorter/real-sorter-validation.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"generatedAt": "2026-07-30T11:20:00Z",
|
||||
"status": "PASS",
|
||||
"checks": [
|
||||
{ "id": "single_cad_conveyor", "result": "PASS", "detail": "/ and /details both use SorterDigitalTwinContinuous + ConveyorCadModel" },
|
||||
{ "id": "continuous_scan", "result": "PASS", "detail": "beltVelocity>0 and item x strictly increasing through detection/measurement (continuousMeasurement.test.ts)" },
|
||||
{ "id": "realsense_d435i", "result": "PASS", "detail": "SPEC_DERIVED from datasheet; optical axis -Y; frustum debug via ?debug=1&physics=1" },
|
||||
{ "id": "mechanism_contact", "result": "PASS", "detail": "kinematic paddle; pusherContactMade asserted 10/10 for every C/D route" },
|
||||
{ "id": "drop_validation_7x10", "result": "PASS", "detail": "see drop-validation.json — 7/7 routes PASS" },
|
||||
{ "id": "no_teleport", "result": "PASS", "detail": "handoff uses continuous pose; maxSpeedMps < 8; physicsSimClock sync under lag" },
|
||||
{ "id": "product_scale", "result": "PASS", "detail": "box 60% belt, bottle 18%, plate 42%, pen 2% — matches §17.1" },
|
||||
{ "id": "classifier_unchanged", "result": "PASS", "detail": "classifier.ts not modified" },
|
||||
{ "id": "mobile_svg_fallback", "result": "PASS", "detail": "capability policy + SVG path; no GLB load on svg tier" },
|
||||
{ "id": "public_https_domain", "result": "BLOCKED_BY_DEPLOYMENT_ACCESS", "detail": "sorter.arhipovdan.ru A→185.160.137.162 openresty without SNI; production on :3100 + Quick Tunnel only" }
|
||||
]
|
||||
}
|
||||
BIN
docs/stage2_real_sorter/screenshots/01-real-cad-overview.png
Normal file
|
After Width: | Height: | Size: 260 KiB |
BIN
docs/stage2_real_sorter/screenshots/02-volumetric-rollers.png
Normal file
|
After Width: | Height: | Size: 129 KiB |
BIN
docs/stage2_real_sorter/screenshots/03-volumetric-brackets.png
Normal file
|
After Width: | Height: | Size: 297 KiB |
BIN
docs/stage2_real_sorter/screenshots/04-realsense-front.png
Normal file
|
After Width: | Height: | Size: 297 KiB |
BIN
docs/stage2_real_sorter/screenshots/05-realsense-mounted.png
Normal file
|
After Width: | Height: | Size: 260 KiB |
|
After Width: | Height: | Size: 260 KiB |
BIN
docs/stage2_real_sorter/screenshots/07-continuous-scan-entry.png
Normal file
|
After Width: | Height: | Size: 258 KiB |
BIN
docs/stage2_real_sorter/screenshots/08-continuous-scan-exit.png
Normal file
|
After Width: | Height: | Size: 277 KiB |
|
After Width: | Height: | Size: 304 KiB |
BIN
docs/stage2_real_sorter/screenshots/10-mechanism-contact.png
Normal file
|
After Width: | Height: | Size: 310 KiB |
BIN
docs/stage2_real_sorter/screenshots/11-box-route-b.png
Normal file
|
After Width: | Height: | Size: 234 KiB |
BIN
docs/stage2_real_sorter/screenshots/12-oversized-route-c.png
Normal file
|
After Width: | Height: | Size: 337 KiB |
BIN
docs/stage2_real_sorter/screenshots/13-plate-route-d.png
Normal file
|
After Width: | Height: | Size: 274 KiB |
BIN
docs/stage2_real_sorter/screenshots/14-cylinder-route-d.png
Normal file
|
After Width: | Height: | Size: 291 KiB |
|
After Width: | Height: | Size: 258 KiB |
BIN
docs/stage2_real_sorter/screenshots/16-premium-overview.png
Normal file
|
After Width: | Height: | Size: 260 KiB |
BIN
docs/stage2_real_sorter/screenshots/17-mobile-public-domain.png
Normal file
|
After Width: | Height: | Size: 270 KiB |
|
After Width: | Height: | Size: 326 KiB |
1
docs/stage2_real_sorter/videos/README.md
Normal file
@@ -0,0 +1 @@
|
||||
Videos not captured in this environment (no real-time WebGL recorder under SwiftShader). Capture via: BASE=<url> node scripts/stage2b-capture.mjs videos
|
||||