# FINAL PHYSICS FIX REPORT — 3D Demo Conveyor Network Дата: 2026-07-09 Ветка: `dan_branch` Production: https://arhipovdan.ru/ · https://arhipovdan.ru/details --- ## 1. Root causes confirmed Подтверждено на production (`docs/final_physics_fix_screenshots/before/`) и в коде: | # | Проблема | Root cause в коде | |---|----------|-------------------| | 1 | Движение ленты «жило отдельно» от товара | `BeltStripe` двигался через `useFrame` с покадровым инкрементом `posRef += delta * speedFactor` — отдельный источник, зависящий от FPS и не связанный с позицией товара. | | 2 | B-товар исчезал после сортировки | В `PhysicalPlaybackItem` стоял ранний `return null` для `isSettled && expectedCategory === 'B'`. Физической приёмной зоны B не было — только плоский `ZoneMarker`. | | 3 | Геометрия движения разбросана | Все координаты (belt, chute, cage) считались ad-hoc внутри `physicalItemMotion` через `ZONES.*`, без единой модели поверхностей. Не было единого «источника правды». | | 4 | Кубик вместо STL для c_priority | `SKU-011` был помечен `loaderType: 'procedural'`, хотя round STL (`cylinder.stl`, 106 KB) доступен. | | 5 | Слабый contain в cage | `cageFloorY` был захардкожен `0.1`, cage не имел видимого внутреннего пола — товар выглядел «висящим». | --- ## 2. Conveyor network surfaces Создан единый источник правды: **`src/domain/conveyorNetwork.ts`**. Все координаты в метрах (1 unit = 1 m). | Surface | start → end (m) | surfaceY | width | speed | target | |---------|-----------------|----------|-------|-------|--------| | `main_belt` | A(-4,0) → CAMERA(-1.5,0) | 0.70 | 0.5 | 1.0 m/s | — | | `inspection_station` | CAMERA (dwell) | 0.70 | 0.5 | 0 | — | | `routing_junction` | CAMERA(-1.5) → GATE(1.5) | 0.70 | 0.5 | 1.0 m/s | — | | `b_receiver` | GATE(1.5) → rest(4.0) | 0.70 | 0.5 | 1.0 m/s | B | | `chute_c` | GATE edge(0.25z) → cage C front | 0.70→0.13 | 0.5 | 0.5 m/s | C | | `chute_d` | GATE edge(-0.25z) → cage D front | 0.70→0.13 | 0.5 | 0.5 m/s | D | | `c_cage_floor` | C(2.0, 2.0) | 0.08 | 1.2 | 0 | C | | `d_cage_floor` | D(2.0, -2.0) | 0.08 | 1.2 | 0 | D | Пути: - **B**: `main_belt → inspection_station → routing_junction → b_receiver → settled_b` - **C**: `main_belt → inspection_station → routing_junction → chute_c → c_cage_floor → settled_c` - **D**: `main_belt → inspection_station → routing_junction → chute_d → d_cage_floor → settled_d` Каждая поверхность имеет `bounds` (minX/maxX/minZ/maxZ) для containment-проверок. --- ## 3. Physical motion model `src/domain/physicalItemMotion.ts` полностью переписан и теперь берёт **всю геометрию только из `conveyorNetwork`**: - поза считается детерминированно от `elapsedMs` (без random, без покадровых инкрементов); - позиция = интерполяция вдоль текущего segment (`lerp3`); - `y = surfaceY + itemHeight / 2` (низ товара точно на поверхности); - `rotation` = heading текущей поверхности (`surfaceHeading`); - на `main_belt`/`routing_junction` скорость = **1 м/с**; - на chute — плавный спуск по наклонной (0.5 м/с), не полёт; - в cage товар фиксируется в детерминированном slot (grid 3×2 внутри bounds); - `isSettled = true` только на `b_receiver`/`c_cage_floor`/`d_cage_floor` после завершения кейса; - защита от NaN/Infinity. Убраны все конкурирующие источники движения: `BeltStripe` больше не использует покадровый инкремент. --- ## 4. B receiving zone Добавлен физический приёмный лоток **`BReceiver`** (в `SorterDigitalTwinContinuous.tsx`): - короткий downstream receiving tray сразу после сортировщика; - ширина 0.5 m, верхняя поверхность 0.7 m (на уровне ленты, не «платформа в воздухе»); - невысокие борта (0.12 m) + торцевой стоп; - опорные ноги до пола; - B-товар доезжает по лотку и **остаётся** в нём (ранний `return null` удалён). Константы: `B_RECEIVER` в `physicalLayout.ts` (`startX 2.2 → endX 4.4`, `restX 4.0`). --- ## 5. C/D containment - `RollCage` получил **сплошной внутренний пол** на `CAGE_FLOOR_Y` (0.08 m), товар физически лежит на нём. - cage стоит на полу (колёса), есть нижняя/верхняя рамка, вертикальные стойки, wireframe-стенки. - вход — со стороны chute (front edge cage). - после chute товар оказывается внутри `bounds`, распределяется по slot-grid, не выше верхней границы, после settled cage не покидает. - проверено тестами: финальная поза C/D внутри `c_cage_floor` / `d_cage_floor` bounds, `y = CAGE_FLOOR_Y + h/2`. --- ## 6. STL / fallback table | case | itemId | model path | STL / fallback | |------|--------|-----------|----------------| | box_b | SKU-001 | `/models/box-300.stl` | **STL** (29 KB) | | lunchbox_b | SKU-002 | `/models/lunchbox.stl` | **STL** (566 KB) | | oversized_box_c | SKU-004 | `/models/box-400.stl` | **STL** (27 KB) | | small_item_c | SKU-009 (Ручка) | — | fallback thin box (исходный STL 2.0 MB — тяжёлый) | | plate_d | SKU-006 | `/models/plate.stl` | **STL** (123 KB) | | bottle_d | SKU-007 | `/models/bottle.stl` | **STL** (319 KB) | | c_priority | SKU-011 | `/models/cylinder.stl` | **STL** (106 KB, round STL reused) | | low_confidence | SKU-003 (Моющее ср-во) | — | fallback box (исходный STL 3.5 MB — тяжёлый) | **Итог: 6/8 кейсов используют реальные STL.** 2 fallback — честные (исходные STL >2 MB, исключены по WebGL performance budget). Все 6 доступных лёгких STL используются. --- ## 7. Belt sync proof - Формула ленты: `stripe.x = baseOffset + (time * CONVEYOR_SPEED_MPS) mod beltLen` — та же скорость 1 м/с, то же направление, что и товар. - Детерминированно от `totalElapsedMs`: pause → лента и товар замирают; stop → оба reset (0). - Не зависит от FPS (нет `delta`-инкремента). - Unit-test: товар на `main_belt` за 1000 ms смещается ровно на **1.0 m** (`±0.02`). Debug (из модели, height 0.2 m): - t0 = 300 ms → item.x = −4.00 m (A) - t1 = 1300 ms → item.x = −3.00 m - Δx = **1.00 m за 1.00 s** ✓ Скриншоты: `02_belt_sync_t0.png`, `03_belt_sync_t1_delta_1m.png`. --- ## 8. Tests `npm run test` → **13 файлов, 132 теста passed.** Новые/обновлённые: - `src/domain/conveyorNetwork.test.ts` — все surfaces присутствуют; конечные координаты/bounds; belt=1 м/с, cage/junction статичны; chute медленнее ленты, но >0; path per category; bounds-check; chute имеет реальную длину. - `src/domain/physicalItemMotion.test.ts` — старт на main_belt; **1 m за 1 s**; низ товара = surfaceY; детерминизм; B внутри b_receiver; C внутри c_cage; D внутри d_cage; settled не двигается; нет NaN/Infinity по всему таймлайну; C едет по chute перед settle. - `src/data/modelAssets.test.ts` — все 6 доступных demo-STL замаплены на реальные пути; fallback явные. --- ## 9. Production QA - `npm run build` → OK. - `npm run test` → 132 passed. - `docker compose -p owl -f docker-compose.server.yml up -d --build` → `owl-web-1` recreated & started. - `curl -I` → `https://arhipovdan.ru/` **200**, `https://arhipovdan.ru/details` **200**, `https://ai-shorts.ru/` **200**. - Playwright прогон полного цикла 8 сценариев: - Play запускает, все 8 кейсов доходят до отображения; - **console errors: NONE**; - товар не летит / не телепортируется (виден спуск по chute — `05_chute_c_motion.png`); - STL видны на ленте и на chute; - B-товар остаётся в приёмном лотке; - mobile: `scrollWidth == clientWidth == 390` → **горизонтального scroll нет**. --- ## 10. Screenshots `docs/final_physics_fix_screenshots/before/` — before (хаос, нет B-зоны, исчезновение товара). `docs/final_physics_fix_screenshots/after/`: - `01_stl_box_on_belt.png` — STL короб на ленте; - `02_belt_sync_t0.png`, `03_belt_sync_t1_delta_1m.png` — синхронизация ленты (Δ≈1 m); - `04_b_receiver_item_settled.png` — B receiving tray; - `05_chute_c_motion.png` — STL короб съезжает по chute C; - `06_item_inside_c_cage.png` — C roll-cage; - `07_chute_d_motion.png`, `08_item_inside_d_cage.png` — маршрут/cage D; - `09_all_zones_physical.png` — все зоны (A, belt, inspection, B-tray, C/D cages); - `10_no_overlay_overlap.png` — HUD и CV overlay не перекрываются; - `11_mobile.png` — mobile (2D fallback, single column, без h-scroll); - `12_details.png` — /details. --- ## 11. Remaining risks - `11_mobile.png` в headless-Chromium показывает 2D fallback («WebGL not available») — это ожидаемо без GPU в CI; на реальных устройствах WebGL доступен. Layout корректный, h-scroll отсутствует. - 2 кейса (`small_item_c`, `low_confidence`) остаются fallback-примитивами: исходные STL слишком тяжёлые (2.0 / 3.5 MB) — компромисс по WebGL performance budget. - Скорость chute (0.5 м/с) — визуальная аппроксимация наклонного спуска, не результат физического движка (по требованию physics engine не добавлялся). - Auto-camera может ловить кадр в момент перехода фаз; поза товара при этом всегда детерминирована и корректна. --- ## 12. Honest verdict **ГОТОВО для записи видео защиты.** Товар всегда на физической поверхности (belt → inspection → junction → b_receiver / chute → cage), не летает, не телепортируется, не проваливается, не исчезает и остаётся в зоне. Лента и товар синхронизированы (1 м/с, детерминированно, FPS-independent). B получил физическую приёмную зону. C/D удерживают товар. 6/8 STL реальны, 2 fallback честные. build/test/docker/production — зелёные, console без ошибок.