Compare commits
2 Commits
dan_branch
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f2ce611330 | ||
|
|
d701339930 |
286
README.md
286
README.md
@@ -1,110 +1,98 @@
|
|||||||
# OZON Sorter Digital Twin
|
# OWL PRIME — Ozon Tech Track 3
|
||||||
|
|
||||||
Complete Track 3 Ozon Tech solution in one repository: a web digital twin of the sorting line, plus a real RealSense/OpenCV CV prototype.
|
## О проекте
|
||||||
|
|
||||||
|
OWL PRIME — инженерный контур предварительной сортировки товаров: веб-цифровой двойник конвейерной линии, классификация B/C/D, физическая маршрутизация в симуляции, рабочий CV-прототип на RealSense D415 и экспериментальный физический стенд.
|
||||||
|
|
||||||
|
Это не сертифицированный промышленный ПАК, а воспроизводимое решение хакатона: цифровой twin на проде, CV-прототип в репозитории, CAD/ассеты и документация для проверки судьями.
|
||||||
|
|
||||||
|
## Состав решения
|
||||||
|
|
||||||
|
1. **Web digital twin** — React/Three.js симуляция конвейера, измерения, classifier, diverters.
|
||||||
|
2. **CV-прототип** — `cv/`: RealSense D415 + OpenCV → габариты / круг → B/C/D.
|
||||||
|
3. **CAD и runtime 3D** — авторский FreeCAD и GLB/STL для деплоя.
|
||||||
|
4. **Физический стенд** — лента, рама, камера над полотном, электроника/SCADA.
|
||||||
|
5. **Документация** — `/documentation`, `docs/ENGINEERING.md`.
|
||||||
|
6. **Презентация** — `presentation/Owl_Prime_Ozon_Tech_Track_3_FINAL.pdf`.
|
||||||
|
|
||||||
|
## Демо
|
||||||
|
|
||||||
**Production:** https://arhipovdan.ru
|
**Production:** https://arhipovdan.ru
|
||||||
|
|
||||||
## 1. Overview
|
| Route | Назначение |
|
||||||
|
|
||||||
This repository delivers a continuous web simulation of an Ozon conveyor sorter: CAD conveyor, product models, camera/measurement stage, B/C/D classification, diverters, and Rapier physics. The public demo runs on https://arhipovdan.ru with routes `/` (simulation) and `/documentation` (engineering status).
|
|
||||||
|
|
||||||
Separately, `cv/` contains a **working hardware prototype** that reads Intel RealSense D415 depth, measures parcels with OpenCV, classifies B/C/D, and can publish results over MQTT. It is **not** wired into the live website.
|
|
||||||
|
|
||||||
Both paths share the same Track 3 classification domain (exclusive 10×10×10 … 450×320×320 mm, roundness K > 0.8). The web twin uses a digital sensor simulation; the CV folder uses real depth frames.
|
|
||||||
|
|
||||||
`main` is the canonical complete solution. Developers do not need other branches to run the web app or inspect/run the CV prototype.
|
|
||||||
|
|
||||||
Large submission artifacts (presentation, video, optional CAD/model mirrors) belong in team cloud storage; runtime assets required by deploy stay in Git.
|
|
||||||
|
|
||||||
## 2. Submission components
|
|
||||||
|
|
||||||
| Component | Location | Notes |
|
|
||||||
|---|---|---|
|
|
||||||
| Web digital twin | `src/`, `public/` | Production-integrated |
|
|
||||||
| Real CV prototype | `cv/` | WORKING_PROTOTYPE, not live-integrated |
|
|
||||||
| Author CAD | `3d_models/conveer.FCStd` | FreeCAD source |
|
|
||||||
| Official materials | `input_info/`, `official_sources/` | PDFs/ZIPs cited by docs/code |
|
|
||||||
| Production domain | https://arhipovdan.ru | Docker + nginx |
|
|
||||||
| Presentation / video | cloud (owner) | Links TBD — see §17 |
|
|
||||||
|
|
||||||
## 3. Production demo
|
|
||||||
|
|
||||||
- **URL:** https://arhipovdan.ru
|
|
||||||
- **`/`** — continuous digital-twin simulation
|
|
||||||
- **`/documentation`** — canonical engineering status
|
|
||||||
- Unknown routes redirect to `/`
|
|
||||||
|
|
||||||
Device behavior on current baseline:
|
|
||||||
|
|
||||||
- **Desktop:** interactive WebGL 3D
|
|
||||||
- **Mobile:** lightweight **2D** fallback (not full WebGL 3D)
|
|
||||||
|
|
||||||
Build identity: `/version.json`.
|
|
||||||
|
|
||||||
## 4. Web capabilities
|
|
||||||
|
|
||||||
- Three-module CAD conveyor (clean → camera → sorter)
|
|
||||||
- Camera / measurement simulation and Track 3 classifier
|
|
||||||
- B / C / D routing with CAD diverters (−45° / +45°)
|
|
||||||
- Rapier product physics (contact sorting **not** fully validated)
|
|
||||||
- Continuous playback HUD + documentation page
|
|
||||||
|
|
||||||
## 5. Real CV prototype
|
|
||||||
|
|
||||||
| Field | Value |
|
|
||||||
|---|---|
|
|---|---|
|
||||||
| Path | `cv/` |
|
| `/` | Непрерывная симуляция |
|
||||||
| Origin | `drho1y-mvp_1` / `vision_classifier/` |
|
| `/documentation` | Инженерный статус |
|
||||||
| Technology | RealSense D415 + OpenCV (depth segmentation + metrics) |
|
| `*` | Редирект на `/` |
|
||||||
| Status | WORKING_PROTOTYPE |
|
|
||||||
| Integration | **Not** connected to production web runtime |
|
|
||||||
|
|
||||||
See **[cv/README.md](cv/README.md)** for install, demo, live camera, and MQTT.
|
- **Desktop:** интерактивный WebGL 3D.
|
||||||
|
- **Mobile:** облегчённый 2D fallback (как в текущем baseline).
|
||||||
|
|
||||||
## 6. Architecture
|
Идентичность сборки: `/version.json`.
|
||||||
|
|
||||||
|
## Основные возможности
|
||||||
|
|
||||||
|
- непрерывный CAD-конвейер и STL-товары;
|
||||||
|
- цифровой этап камеры / измерения;
|
||||||
|
- rule-based classifier B/C/D;
|
||||||
|
- CAD-дивертеры LEFT/RIGHT (−45° / +45°);
|
||||||
|
- физика Rapier (лента 1 м/с);
|
||||||
|
- CV-прототип depth→B/C/D (не в live web);
|
||||||
|
- инженерная документация в приложении.
|
||||||
|
|
||||||
|
## Правила классификации
|
||||||
|
|
||||||
|
Официальные границы (`official_sources/doc-1783095831.pdf`), реализованы в web (`src/domain/classifier.ts`) и CV (`cv/classify.py`):
|
||||||
|
|
||||||
|
1. Габариты строго **> 10×10×10 мм** и **< 450×320×320 мм**, иначе → **C**.
|
||||||
|
2. Если габариты OK и **K > 0.8** (круг) → **D**.
|
||||||
|
3. Иначе → **B**.
|
||||||
|
4. **C-priority:** негабарит + круг → только **C**.
|
||||||
|
5. Граница: **K = 0.8 не круг** (→ B, не D). Одинаково в web и CV.
|
||||||
|
|
||||||
|
## Архитектура
|
||||||
|
|
||||||
|
**Web**
|
||||||
|
|
||||||
```
|
```
|
||||||
Real device path:
|
Product → measurement (digital) → classifier → route → twin → B/C/D receiver
|
||||||
RealSense D415 → depth preprocess → segmentation → measurement
|
|
||||||
→ B/C/D → optional MQTT / hardware
|
|
||||||
|
|
||||||
Web path:
|
|
||||||
Digital product → simulated sensor → classifier
|
|
||||||
→ physical digital twin → B/C/D receiver visualization
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Shared: B/C/D semantics and official dimension/roundness rules.
|
**CV**
|
||||||
Not shared today: live camera frames into the website.
|
|
||||||
|
|
||||||
## 7. Repository structure
|
|
||||||
|
|
||||||
```
|
```
|
||||||
.github/ CI (build, unit, e2e)
|
RealSense D415 → depth → segmentation → L×W×H + K → B/C/D → optional MQTT
|
||||||
3d_models/ Author CAD (conveer.FCStd)
|
|
||||||
cv/ Real CV prototype (Python)
|
|
||||||
docs/ Engineering notes
|
|
||||||
e2e/ Playwright smoke/routes
|
|
||||||
input_info/ Official Ozon input packs
|
|
||||||
official_sources/ Classifier bounds PDF
|
|
||||||
public/ Runtime static assets (GLB/STL/draco)
|
|
||||||
src/ React/Three web twin
|
|
||||||
Dockerfile Web production image
|
|
||||||
docker-compose.server.yml
|
|
||||||
nginx.conf
|
|
||||||
package.json / lock
|
|
||||||
vite / vitest / playwright / tsconfig
|
|
||||||
README.md
|
|
||||||
```
|
```
|
||||||
|
|
||||||
No other top-level product directories are required to run or understand the solution.
|
CV **не подключён** к https://arhipovdan.ru. Общее — домен правил B/C/D, не live-канал кадров.
|
||||||
|
|
||||||
## 8. Requirements
|
## Технологии
|
||||||
|
|
||||||
**Web:** Node.js 20+, npm (`package-lock.json`).
|
| Слой | Стек |
|
||||||
**CV:** Python 3.10+, ffmpeg, V4L2; RealSense D415 for live mode (`cv/requirements.txt`).
|
|---|---|
|
||||||
**Hardware (CV live / MQTT):** D415 USB3; optional MQTT broker + servo/motor controllers on site network.
|
| Web | React, TypeScript, Three.js / R3F, Rapier, Vite, Vitest, Playwright |
|
||||||
|
| CV | Python, OpenCV, RealSense D415 (V4L2), optional MQTT (`paho-mqtt`) |
|
||||||
|
|
||||||
## 9. Web quick start
|
## Структура репозитория
|
||||||
|
|
||||||
|
```
|
||||||
|
.github/ CI
|
||||||
|
3d_models/ Авторский CAD (conveer.FCStd)
|
||||||
|
cv/ CV-прототип RealSense + OpenCV
|
||||||
|
docs/ Engineering notes
|
||||||
|
e2e/ Playwright smoke/routes
|
||||||
|
input_info/ Официальные входные пакеты
|
||||||
|
official_sources/ PDF с границами classifier
|
||||||
|
presentation/ Финальная презентация (один PDF)
|
||||||
|
public/ Runtime GLB/STL/draco
|
||||||
|
src/ Web twin
|
||||||
|
```
|
||||||
|
|
||||||
|
Конфиги деплоя: `Dockerfile`, `docker-compose.server.yml`, `nginx.conf`, `package.json`.
|
||||||
|
|
||||||
|
## Запуск web
|
||||||
|
|
||||||
|
Node.js 20+.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm ci
|
npm ci
|
||||||
@@ -114,100 +102,78 @@ npm run build
|
|||||||
npm run preview # http://127.0.0.1:3100
|
npm run preview # http://127.0.0.1:3100
|
||||||
```
|
```
|
||||||
|
|
||||||
## 10. CV quick start
|
## Запуск CV
|
||||||
|
|
||||||
|
Python 3.10+. Live-режим требует Intel RealSense D415 и ffmpeg.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd cv
|
cd cv
|
||||||
./demo.sh # venv + deps; HUD on :8080 (needs D415 for live view)
|
python3 -m venv .venv
|
||||||
# without camera:
|
source .venv/bin/activate
|
||||||
python3 -m venv .venv && .venv/bin/pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
.venv/bin/python test_classify.py
|
cp config.example.yaml config.yaml # MQTT выключен по умолчанию
|
||||||
.venv/bin/python test_geometry.py
|
|
||||||
# live pipeline (hardware):
|
# без камеры:
|
||||||
|
python test_classify.py
|
||||||
|
python test_geometry.py
|
||||||
|
|
||||||
|
# с камерой:
|
||||||
|
./demo.sh # HUD http://127.0.0.1:8080/
|
||||||
./run.sh --preview --no-mqtt --no-motor
|
./run.sh --preview --no-mqtt --no-motor
|
||||||
```
|
```
|
||||||
|
|
||||||
`npm install` does **not** install CV dependencies.
|
Подробности: [`cv/README.md`](cv/README.md). `npm` CV-зависимости не ставит.
|
||||||
|
|
||||||
## 11. Configuration
|
## Конфигурация
|
||||||
|
|
||||||
**Web (build-time, optional):** `VITE_BUILD_COMMIT`, `VITE_BUILD_BRANCH`, `VITE_BUILD_RELEASE` → `/version.json`. No runtime secrets.
|
- `.env` **не** коммитится.
|
||||||
|
- Web: опционально `VITE_BUILD_COMMIT` / `VITE_BUILD_BRANCH` / `VITE_BUILD_RELEASE` для `/version.json`.
|
||||||
|
- CV: `cv/config.example.yaml` → локальный `cv/config.yaml` (gitignored). MQTT/motor/routing **disabled by default**. Секреты не хранить в Git.
|
||||||
|
|
||||||
**CV:** copy `cv/config.example.yaml` → `cv/config.yaml` (gitignored). MQTT/motor/routing **disabled by default**. Never commit real passwords or broker credentials.
|
## CAD и модели
|
||||||
|
|
||||||
## 12. Classification rules
|
| Файл | Роль |
|
||||||
|
|
||||||
Verified in web (`src/domain/classifier.ts`) and CV (`cv/classify.py`):
|
|
||||||
|
|
||||||
- dimensions strictly **> 10×10×10 mm** and **< 450×320×320 mm**
|
|
||||||
- circular when **K > 0.8** (web) / `circle_ratio ≥ 0.8` (CV)
|
|
||||||
- order: dimensions fail → **C**; else circular → **D**; else **B**
|
|
||||||
|
|
||||||
Official citation: `official_sources/doc-1783095831.pdf` (present; not re-parsed on every doc pass). Missing extracted brief PDF is not claimed.
|
|
||||||
|
|
||||||
## 13. Physics (accepted `main`)
|
|
||||||
|
|
||||||
From current source (not superseded experimental branches):
|
|
||||||
|
|
||||||
- belt speed **1.0 m/s** (`CONVEYOR_SPEED_MPS`)
|
|
||||||
- fixed timestep **1/60 s** (`PHYSICS_TIMESTEP_SEC`)
|
|
||||||
- CCD for light/thin product bodies
|
|
||||||
- diverters LEFT **−45°**, RIGHT **+45°**
|
|
||||||
- full contact-only junction sorting through CAD: **not fully validated**
|
|
||||||
|
|
||||||
## 14. CAD and assets
|
|
||||||
|
|
||||||
| Asset | Path |
|
|
||||||
|---|---|
|
|---|---|
|
||||||
| Author CAD | `3d_models/conveer.FCStd` |
|
| `3d_models/conveer.FCStd` | Авторский CAD |
|
||||||
| Runtime GLB | `public/models/sorter/conveyor-clean.glb` |
|
| `public/models/sorter/conveyor-clean.glb` | Runtime конвейер |
|
||||||
| Products | `public/models/*.stl` |
|
| `public/models/*.stl` | Модели товаров |
|
||||||
|
|
||||||
Keep runtime assets in Git for deploy. Mirror large CAD/models/presentation/video to cloud for submission.
|
Крупные материалы для сдачи дополнительно зеркалируйте в облако; runtime-ассеты для деплоя остаются в Git.
|
||||||
|
|
||||||
SHA-256 (frozen):
|
## Физика (текущий main)
|
||||||
|
|
||||||
```
|
- скорость ленты **1.0 м/с**;
|
||||||
3d_models/conveer.FCStd
|
- timestep **1/60 с**;
|
||||||
90c1844a4ca05e26def783d6130fc4b993430dde14307534ef8fbb21c9fac2e6
|
- CCD для лёгких/тонких тел;
|
||||||
|
- diverters −45° / +45°;
|
||||||
|
- полный contact-only junction sorting **не fully validated**.
|
||||||
|
|
||||||
public/models/sorter/conveyor-clean.glb
|
## Проверка
|
||||||
1dc7a8d7891bfe756e277ad5368df74cb73410156b2fe0f92845afb8a56f285a
|
|
||||||
```
|
|
||||||
|
|
||||||
## 15. Testing
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm test -- --run
|
npm ci && npm test -- --run && npm run build
|
||||||
# current release result: 196/196
|
# E2E: preview :3101 + e2e/routes.spec.ts + e2e/smoke.spec.ts
|
||||||
npm run build
|
|
||||||
PLAYWRIGHT_BASE_URL=http://127.0.0.1:3101 npm run test:e2e -- e2e/routes.spec.ts e2e/smoke.spec.ts
|
|
||||||
|
|
||||||
cd cv && .venv/bin/python test_classify.py && .venv/bin/python test_geometry.py
|
cd cv && python -m compileall . && python test_classify.py && python test_geometry.py
|
||||||
```
|
```
|
||||||
|
|
||||||
## 16. Deployment
|
Актуальный релиз: unit **196/196**, build PASS, focused E2E PASS, CV compile + unit без камеры PASS. Прод: `/` и `/documentation` → 200.
|
||||||
|
|
||||||
Nginx terminates TLS for `arhipovdan.ru` and proxies to Docker `owl-web-1` (`docker-compose.server.yml` + `Dockerfile`) on `127.0.0.1:3100`. Deploy from `main` with build-args for `/version.json`. CV is **not** part of the web container.
|
## Ограничения
|
||||||
|
|
||||||
## 17. Submission materials
|
- инженерный прототип, не industrial-certified ПАК;
|
||||||
|
- CV не live-интегрирован в web;
|
||||||
|
- live CV требует D415; на CI — только unit/compile;
|
||||||
|
- параметры симуляции требуют калибровки на стенде;
|
||||||
|
- mobile — 2D lite fallback;
|
||||||
|
- облачная ссылка на доп. материалы — по решению владельца.
|
||||||
|
|
||||||
| Material | Status |
|
## Материалы
|
||||||
|---|---|
|
|
||||||
| Presentation URL | REQUIRED_FROM_OWNER |
|
|
||||||
| Video demo URL | REQUIRED_FROM_OWNER |
|
|
||||||
| Cloud folder URL | REQUIRED_FROM_OWNER |
|
|
||||||
|
|
||||||
Do not invent links. Runtime site assets remain in Git even when mirrored to cloud.
|
- Презентация: [`presentation/Owl_Prime_Ozon_Tech_Track_3_FINAL.pdf`](presentation/Owl_Prime_Ozon_Tech_Track_3_FINAL.pdf)
|
||||||
|
- Production: https://arhipovdan.ru
|
||||||
|
- Cloud folder: `[ДОБАВИТЬ ССЫЛКУ]`
|
||||||
|
|
||||||
## 18. Known limitations
|
## Статус
|
||||||
|
|
||||||
- Mobile uses 2D lite fallback on current baseline
|
**`main` — каноническое полное решение.** Другие ветки исторические и не нужны для запуска web или CV.
|
||||||
- CV is a prototype and is not live-integrated into arhipovdan.ru
|
|
||||||
- Simulation physics is engineering-derived; hardware calibration still required
|
|
||||||
- Contact routing through CAD diverters not fully validated
|
|
||||||
- Large presentation/video must be uploaded to cloud by owner
|
|
||||||
|
|
||||||
## 19. Branch history policy
|
|
||||||
|
|
||||||
**`main` is the canonical complete solution** (web + cleaned CV under `cv/`). Historical branches (`dan_branch`, `drho1y-mvp_1`, …) may remain for audit but are not required to run the product.
|
|
||||||
|
|||||||
@@ -36,7 +36,8 @@ Primary modules: `main.py` (live), `demo.py` (HUD), `classify.py` (rules), `meas
|
|||||||
## Classification rules (Track 3)
|
## Classification rules (Track 3)
|
||||||
|
|
||||||
1. Dimensions must be strictly **> 10×10×10 mm** and **< 450×320×320 mm** → else **C**
|
1. Dimensions must be strictly **> 10×10×10 mm** and **< 450×320×320 mm** → else **C**
|
||||||
2. Else if `circle_ratio ≥ 0.8` → **D**
|
2. Else if `circle_ratio > 0.8` → **D**
|
||||||
|
(`K == 0.8` is **not** circular — same strict rule as web `classifier.ts`)
|
||||||
3. Else → **B**
|
3. Else → **B**
|
||||||
|
|
||||||
Stabilization: median window + vote → **LOCK**. Uncertain cases fall back to zone **C** after N frames.
|
Stabilization: median window + vote → **LOCK**. Uncertain cases fall back to zone **C** after N frames.
|
||||||
|
|||||||
@@ -79,13 +79,14 @@ def classify(
|
|||||||
"""
|
"""
|
||||||
Порядок ТЗ:
|
Порядок ТЗ:
|
||||||
1) габариты → иначе C (приоритет над кругом)
|
1) габариты → иначе C (приоритет над кругом)
|
||||||
2) если r_in/r_out >= 0.8 в любом сечении → D
|
2) если r_in/r_out > 0.8 в любом сечении → D
|
||||||
|
(K == 0.8 НЕ круг — как в web classifier.ts)
|
||||||
3) иначе → B
|
3) иначе → B
|
||||||
"""
|
"""
|
||||||
dims = _sorted_dims(measurement.length_mm, measurement.width_mm, measurement.height_mm)
|
dims = _sorted_dims(measurement.length_mm, measurement.width_mm, measurement.height_mm)
|
||||||
passes = check_size(dims, min_mm, max_mm)
|
passes = check_size(dims, min_mm, max_mm)
|
||||||
ratio = float(measurement.circle_ratio)
|
ratio = float(measurement.circle_ratio)
|
||||||
circular = ratio >= float(circle_ratio_threshold)
|
circular = ratio > float(circle_ratio_threshold)
|
||||||
clipped = bool(getattr(measurement, "clipped_by_frame", False))
|
clipped = bool(getattr(measurement, "clipped_by_frame", False))
|
||||||
|
|
||||||
if not passes or clipped:
|
if not passes or clipped:
|
||||||
@@ -112,7 +113,7 @@ def classify(
|
|||||||
circle_ratio=ratio,
|
circle_ratio=ratio,
|
||||||
passes_size=True,
|
passes_size=True,
|
||||||
is_circular=True,
|
is_circular=True,
|
||||||
reason=f"круг в сечении: r_in/r_out={ratio:.3f} >= {circle_ratio_threshold}",
|
reason=f"круг в сечении: r_in/r_out={ratio:.3f} > {circle_ratio_threshold}",
|
||||||
)
|
)
|
||||||
|
|
||||||
return ClassificationResult(
|
return ClassificationResult(
|
||||||
@@ -121,7 +122,7 @@ def classify(
|
|||||||
circle_ratio=ratio,
|
circle_ratio=ratio,
|
||||||
passes_size=True,
|
passes_size=True,
|
||||||
is_circular=False,
|
is_circular=False,
|
||||||
reason=f"габариты OK, круга нет: r_in/r_out={ratio:.3f} < {circle_ratio_threshold}",
|
reason=f"габариты OK, круга нет: r_in/r_out={ratio:.3f} <= {circle_ratio_threshold}",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -113,12 +113,13 @@ class DecisionStabilizer:
|
|||||||
top_m = _median(self._tops)
|
top_m = _median(self._tops)
|
||||||
sec_m = _median(self._secs)
|
sec_m = _median(self._secs)
|
||||||
ratio_p75 = _percentile(self._ratios, 75)
|
ratio_p75 = _percentile(self._ratios, 75)
|
||||||
# устойчивый круг: медиана >= 0.8 ИЛИ (медиана сечений >= 0.8 и ≥ половины окна сильные)
|
# устойчивый круг: медиана > 0.8 ИЛИ (медиана сечений > 0.8 и ≥ половины окна сильные)
|
||||||
|
# K == 0.8 официально НЕ круг (строгое > threshold)
|
||||||
sec_strong = (
|
sec_strong = (
|
||||||
sum(1 for x in self._secs if x >= self.enter_circle) / max(1, len(self._secs))
|
sum(1 for x in self._secs if x > self.enter_circle) / max(1, len(self._secs))
|
||||||
)
|
)
|
||||||
circular = ratio >= self.enter_circle or (
|
circular = ratio > self.enter_circle or (
|
||||||
sec_m >= self.enter_circle and sec_strong >= 0.55
|
sec_m > self.enter_circle and sec_strong >= 0.55
|
||||||
)
|
)
|
||||||
ratio_show = max(ratio, sec_m) if circular else ratio
|
ratio_show = max(ratio, sec_m) if circular else ratio
|
||||||
|
|
||||||
@@ -153,7 +154,7 @@ class DecisionStabilizer:
|
|||||||
reason=(
|
reason=(
|
||||||
f"круг: med={ratio:.3f} p75={ratio_p75:.3f} "
|
f"круг: med={ratio:.3f} p75={ratio_p75:.3f} "
|
||||||
f"top={top_m:.3f} sec={sec_m:.3f} strong={sec_strong:.0%} "
|
f"top={top_m:.3f} sec={sec_m:.3f} strong={sec_strong:.0%} "
|
||||||
f">= {self.enter_circle}"
|
f"> {self.enter_circle}"
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@@ -165,7 +166,7 @@ class DecisionStabilizer:
|
|||||||
is_circular=False,
|
is_circular=False,
|
||||||
reason=(
|
reason=(
|
||||||
f"не круг: med={ratio:.3f} sec={sec_m:.3f} "
|
f"не круг: med={ratio:.3f} sec={sec_m:.3f} "
|
||||||
f"strong={sec_strong:.0%} < {self.enter_circle}"
|
f"strong={sec_strong:.0%} <= {self.enter_circle}"
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -43,11 +43,40 @@ def test_lying_bottle_must_be_D_not_B():
|
|||||||
assert r.category.zone == "D"
|
assert r.category.zone == "D"
|
||||||
|
|
||||||
|
|
||||||
def test_border_circle():
|
def test_strict_circle_boundary():
|
||||||
r = classify_from_dims(100, 50, 50, circle_ratio=0.8)
|
"""Официально: круг только при K > 0.8. K == 0.8 → B (не D)."""
|
||||||
|
below = classify_from_dims(100, 50, 50, circle_ratio=0.799999)
|
||||||
|
assert below.category == Category.SUITABLE
|
||||||
|
assert below.category.zone == "B"
|
||||||
|
assert below.is_circular is False
|
||||||
|
|
||||||
|
exact = classify_from_dims(100, 50, 50, circle_ratio=0.8)
|
||||||
|
assert exact.category == Category.SUITABLE
|
||||||
|
assert exact.category.zone == "B"
|
||||||
|
assert exact.is_circular is False
|
||||||
|
|
||||||
|
above = classify_from_dims(100, 50, 50, circle_ratio=0.800001)
|
||||||
|
assert above.category == Category.NEED_PACK
|
||||||
|
assert above.category.zone == "D"
|
||||||
|
assert above.is_circular is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_normal_non_circular_B():
|
||||||
|
r = classify_from_dims(120, 80, 40, circle_ratio=0.5)
|
||||||
|
assert r.category == Category.SUITABLE
|
||||||
|
assert r.category.zone == "B"
|
||||||
|
|
||||||
|
|
||||||
|
def test_normal_circular_above_threshold_D():
|
||||||
|
r = classify_from_dims(100, 50, 50, circle_ratio=0.81)
|
||||||
assert r.category == Category.NEED_PACK
|
assert r.category == Category.NEED_PACK
|
||||||
r2 = classify_from_dims(100, 50, 50, circle_ratio=0.799)
|
assert r.category.zone == "D"
|
||||||
assert r2.category == Category.SUITABLE
|
|
||||||
|
|
||||||
|
def test_oversized_circular_still_C():
|
||||||
|
r = classify_from_dims(500, 100, 100, circle_ratio=0.95)
|
||||||
|
assert r.category == Category.OVERSIZE
|
||||||
|
assert r.category.zone == "C"
|
||||||
|
|
||||||
|
|
||||||
def test_border_dims_strict():
|
def test_border_dims_strict():
|
||||||
@@ -80,7 +109,10 @@ if __name__ == "__main__":
|
|||||||
test_too_small()
|
test_too_small()
|
||||||
test_need_pack_cylinder()
|
test_need_pack_cylinder()
|
||||||
test_lying_bottle_must_be_D_not_B()
|
test_lying_bottle_must_be_D_not_B()
|
||||||
test_border_circle()
|
test_strict_circle_boundary()
|
||||||
|
test_normal_non_circular_B()
|
||||||
|
test_normal_circular_above_threshold_D()
|
||||||
|
test_oversized_circular_still_C()
|
||||||
test_border_dims_strict()
|
test_border_dims_strict()
|
||||||
test_dims_order_independent()
|
test_dims_order_independent()
|
||||||
print("OK: all classification tests passed")
|
print("OK: all classification tests passed")
|
||||||
|
|||||||
@@ -20,8 +20,11 @@ Companion to `README.md` and `/documentation`. Not a stage changelog.
|
|||||||
| `OPENING_SAFETY_MARGIN_SEC` | 0.15 | same |
|
| `OPENING_SAFETY_MARGIN_SEC` | 0.15 | same |
|
||||||
| Contact / clear planes | ≈1.0538 / 1.6000 | `buildDiverterPlanes` + mount hinge |
|
| Contact / clear planes | ≈1.0538 / 1.6000 | `buildDiverterPlanes` + mount hinge |
|
||||||
| Classifier min/max | exclusive 10³ / 450×320×320 | `src/domain/classifier.ts` |
|
| Classifier min/max | exclusive 10³ / 450×320×320 | `src/domain/classifier.ts` |
|
||||||
| Roundness | K > 0.8 | same |
|
| Roundness | K > 0.8 (K = 0.8 not circular); web + CV aligned | `classifier.ts` / `cv/classify.py` |
|
||||||
|
| Belt speed (sim) | 1.0 m/s | runtime / physics helpers |
|
||||||
|
| Physics timestep | 1/60 s | Rapier world step |
|
||||||
| `CONVEYOR_CAD_URL` | `/models/sorter/conveyor-clean.glb` | `ConveyorCadModel.tsx` |
|
| `CONVEYOR_CAD_URL` | `/models/sorter/conveyor-clean.glb` | `ConveyorCadModel.tsx` |
|
||||||
|
| Presentation | `presentation/Owl_Prime_Ozon_Tech_Track_3_FINAL.pdf` | 10 slides |
|
||||||
|
|
||||||
## Active source tree (runtime)
|
## Active source tree (runtime)
|
||||||
|
|
||||||
@@ -69,10 +72,10 @@ Grounded only in present sources + current `main` code/tests (not a full officia
|
|||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| B/C/D bounds & roundness | `official_sources/doc-1783095831.pdf` | web `classifier.ts`, CV `cv/classify.py` | unit tests | PARTIAL (PDF not re-parsed each pass) |
|
| B/C/D bounds & roundness | `official_sources/doc-1783095831.pdf` | web `classifier.ts`, CV `cv/classify.py` | unit tests | PARTIAL (PDF not re-parsed each pass) |
|
||||||
| Digital twin demo | workspace PDFs in `input_info/` | `/` + `/documentation` | e2e smoke/routes, production | PASS (demo present) |
|
| Digital twin demo | workspace PDFs in `input_info/` | `/` + `/documentation` | e2e smoke/routes, production | PASS (demo present) |
|
||||||
| CAD conveyor | author pack / STL references | `3d_models/`, `conveyor-clean.glb` | checksums in README | PASS (assets present) |
|
| CAD conveyor | author pack / STL references | `3d_models/`, `conveyor-clean.glb` | checksums in `/documentation` | PASS (assets present) |
|
||||||
| Real measurement CV | Track 3 camera intent in briefs | `cv/` RealSense+OpenCV | `cv/README.md`, `test_classify.py` | PARTIAL (prototype, not live web) |
|
| Real measurement CV | Track 3 camera intent in briefs | `cv/` RealSense+OpenCV on `main` | `cv/README.md`, `test_classify.py` | PARTIAL (WORKING_PROTOTYPE, not live web) |
|
||||||
| Physical industrial line | scoring/workspace PDFs | web physics + optional MQTT CV | code; contact not fully validated | PARTIAL |
|
| Physical industrial line | scoring/workspace PDFs | web physics + optional MQTT CV | code; contact not fully validated | PARTIAL |
|
||||||
| Presentation / video | platform rules | cloud links | README §17 | NOT_VERIFIED (links missing) |
|
| Presentation | platform rules | `presentation/Owl_Prime_Ozon_Tech_Track_3_FINAL.pdf` | 10-page PDF in repo | PASS (file present) |
|
||||||
|
|
||||||
## Real CV prototype
|
## Real CV prototype
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,10 @@ test.describe('routes', () => {
|
|||||||
await page.getByTestId('nav-documentation').first().click();
|
await page.getByTestId('nav-documentation').first().click();
|
||||||
await expect(page).toHaveURL(/\/documentation\/?$/);
|
await expect(page).toHaveURL(/\/documentation\/?$/);
|
||||||
await expect(page.getByTestId('documentation-page')).toBeVisible();
|
await expect(page.getByTestId('documentation-page')).toBeVisible();
|
||||||
await expect(page.getByTestId('docs-production-status')).toContainText('DATA_ACQUISITION_PACK_READY');
|
await expect(page.getByTestId('docs-production-status')).toContainText(
|
||||||
|
'FINAL_ENGINEERING_PROTOTYPE_READY',
|
||||||
|
);
|
||||||
|
await expect(page.getByTestId('docs-production-status')).toContainText('WORKING_PROTOTYPE');
|
||||||
await expect(page.getByTestId('nav-documentation').first()).toHaveClass(/active/);
|
await expect(page.getByTestId('nav-documentation').first()).toHaveClass(/active/);
|
||||||
|
|
||||||
await page.reload();
|
await page.reload();
|
||||||
|
|||||||
BIN
presentation/Owl_Prime_Ozon_Tech_Track_3_FINAL.pdf
Normal file
BIN
presentation/Owl_Prime_Ozon_Tech_Track_3_FINAL.pdf
Normal file
Binary file not shown.
@@ -4,16 +4,29 @@
|
|||||||
* Unverified / planned items are labeled explicitly — never implied complete.
|
* Unverified / planned items are labeled explicitly — never implied complete.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/** Kept for e2e (`docs-production-status`) and acquisition-pack provenance. */
|
|
||||||
export const PRODUCTION_STATUS = {
|
export const PRODUCTION_STATUS = {
|
||||||
acquisitionPackStatus: 'DATA_ACQUISITION_PACK_READY',
|
projectStatus: 'FINAL_ENGINEERING_PROTOTYPE_READY',
|
||||||
webTwinStatus: 'BASELINE_PRESERVED',
|
webTwinStatus: 'PRODUCTION_LIVE',
|
||||||
|
cvStatus: 'WORKING_PROTOTYPE',
|
||||||
|
cvLiveIntegrated: false,
|
||||||
unitTests: '196/196',
|
unitTests: '196/196',
|
||||||
|
cvTests: 'PASS (no camera)',
|
||||||
productionBuild: 'PASS',
|
productionBuild: 'PASS',
|
||||||
|
productionUrl: 'https://arhipovdan.ru',
|
||||||
contactPhysics: 'NOT_FULLY_VALIDATED',
|
contactPhysics: 'NOT_FULLY_VALIDATED',
|
||||||
officialCompliance: 'PARTIAL_SOURCES_PRESENT',
|
officialCompliance: 'PARTIAL_SOURCES_PRESENT',
|
||||||
|
canonicalBranch: 'main',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
export const SOLUTION_COMPONENTS = [
|
||||||
|
'Web digital twin (https://arhipovdan.ru)',
|
||||||
|
'Real CV working prototype (cv/, RealSense D415 + OpenCV)',
|
||||||
|
'Physical experimental conveyor stand',
|
||||||
|
'Author CAD (3d_models/conveer.FCStd)',
|
||||||
|
'Official B/C/D classifier (web + CV, K > 0.8)',
|
||||||
|
'Engineering documentation (/documentation)',
|
||||||
|
] as const;
|
||||||
|
|
||||||
export const CONFIRMED_LAYOUT = {
|
export const CONFIRMED_LAYOUT = {
|
||||||
workspaceMm: { length: 10000, width: 6000 },
|
workspaceMm: { length: 10000, width: 6000 },
|
||||||
conveyorWidthMm: 500,
|
conveyorWidthMm: 500,
|
||||||
@@ -23,7 +36,7 @@ export const CONFIRMED_LAYOUT = {
|
|||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const CLASSIFIER_BOUNDS = {
|
export const CLASSIFIER_BOUNDS = {
|
||||||
status: 'CURRENT_IMPLEMENTATION_VERIFIED_IN_CODE' as const,
|
status: 'ALIGNED_WEB_AND_CV' as const,
|
||||||
officialSourceReference: 'official_sources/doc-1783095831.pdf',
|
officialSourceReference: 'official_sources/doc-1783095831.pdf',
|
||||||
officialSourceParsedThisPass: false,
|
officialSourceParsedThisPass: false,
|
||||||
minExclusiveMm: { width: 10, depth: 10, height: 10 },
|
minExclusiveMm: { width: 10, depth: 10, height: 10 },
|
||||||
@@ -33,8 +46,63 @@ export const CLASSIFIER_BOUNDS = {
|
|||||||
min: '> 10×10×10 мм',
|
min: '> 10×10×10 мм',
|
||||||
max: '< 450×320×320 мм',
|
max: '< 450×320×320 мм',
|
||||||
roundness: 'K > 0.8',
|
roundness: 'K > 0.8',
|
||||||
|
exactBoundary: 'K = 0.8 → B (not circular), not D',
|
||||||
},
|
},
|
||||||
checkOrder: 'dimensions→C, else circular→D, else B' as const,
|
checkOrder: 'dimensions→C, else circular (K>0.8)→D, else B' as const,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const CV_PROTOTYPE = {
|
||||||
|
path: 'cv/',
|
||||||
|
status: 'WORKING_PROTOTYPE',
|
||||||
|
hardware: 'Intel RealSense D415',
|
||||||
|
software: 'Python + OpenCV (+ optional MQTT)',
|
||||||
|
liveIntegrated: false,
|
||||||
|
pipeline: [
|
||||||
|
'depth frame',
|
||||||
|
'segmentation',
|
||||||
|
'object contour',
|
||||||
|
'L×W×H',
|
||||||
|
'roundness K',
|
||||||
|
'B/C/D',
|
||||||
|
'optional MQTT',
|
||||||
|
] as const,
|
||||||
|
notes: [
|
||||||
|
'CV works as a separate prototype under cv/ on main.',
|
||||||
|
'CV is not connected directly to the live public website.',
|
||||||
|
'Hardware live validation requires RealSense D415.',
|
||||||
|
'No-camera unit tests and compileall run in CI/local verification.',
|
||||||
|
],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const PHYSICAL_STAND = {
|
||||||
|
status: 'EXPERIMENTAL_PROTOTYPE',
|
||||||
|
elements: [
|
||||||
|
'Physical belt conveyor',
|
||||||
|
'Camera mounting structure above the belt',
|
||||||
|
'Intel RealSense D415',
|
||||||
|
'Electronics / control nodes',
|
||||||
|
'Experimental actuators and printed components',
|
||||||
|
] as const,
|
||||||
|
purpose: 'Measurements, calibration, and hardware validation — not claimed as industrial end-to-end certified sorting.',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const ARCHITECTURE = {
|
||||||
|
realPath: [
|
||||||
|
'physical product',
|
||||||
|
'RealSense D415',
|
||||||
|
'OpenCV measurement',
|
||||||
|
'classifier (K > 0.8)',
|
||||||
|
'B/C/D result',
|
||||||
|
'optional MQTT/controller',
|
||||||
|
] as const,
|
||||||
|
digitalPath: [
|
||||||
|
'digital product',
|
||||||
|
'simulated measurement',
|
||||||
|
'same classifier rules',
|
||||||
|
'route command',
|
||||||
|
'digital twin',
|
||||||
|
'B/C/D receiver',
|
||||||
|
] as const,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const ROUTE_MAPPING = [
|
export const ROUTE_MAPPING = [
|
||||||
@@ -72,6 +140,7 @@ export const PHYSICS_STATUS = {
|
|||||||
'Synchronized CAD diverter visual / kinematic targets',
|
'Synchronized CAD diverter visual / kinematic targets',
|
||||||
'CCD enabled for light/thin SKUs in runtime and headless sim',
|
'CCD enabled for light/thin SKUs in runtime and headless sim',
|
||||||
'Visual/physics spawn gating via product asset preload',
|
'Visual/physics spawn gating via product asset preload',
|
||||||
|
'Belt speed target 1.0 m/s; physics timestep 1/60 s',
|
||||||
],
|
],
|
||||||
notFullyValidated: [
|
notFullyValidated: [
|
||||||
'Complete contact-only routing through CAD diverters',
|
'Complete contact-only routing through CAD diverters',
|
||||||
@@ -83,18 +152,26 @@ export const PHYSICS_STATUS = {
|
|||||||
planned: [
|
planned: [
|
||||||
'Visual full belt loop with surface-velocity coupling',
|
'Visual full belt loop with surface-velocity coupling',
|
||||||
'Controlled tangential friction at 1 m/s',
|
'Controlled tangential friction at 1 m/s',
|
||||||
'Dynamic rigid bodies for divert segment with fixed timestep',
|
|
||||||
'Per-SKU collider, damping, and friction profiles',
|
'Per-SKU collider, damping, and friction profiles',
|
||||||
],
|
],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
export const MOBILE_BEHAVIOR = {
|
||||||
|
desktop: 'Interactive WebGL 3D digital twin',
|
||||||
|
mobile:
|
||||||
|
'Capability-based tier: WebGL when viable; SVG/2D lite fallback on low FPS / missing WebGL (not claimed as full 3D)',
|
||||||
|
} as const;
|
||||||
|
|
||||||
export const VALIDATION_BOARD = [
|
export const VALIDATION_BOARD = [
|
||||||
{ item: 'Unit tests', status: '196/196 PASS' },
|
{ item: 'Web unit tests', status: '196/196 PASS' },
|
||||||
{ item: 'Production build', status: 'PASS' },
|
{ item: 'Production build', status: 'PASS' },
|
||||||
{ item: 'Active routes / + /documentation', status: 'PASS' },
|
{ item: 'Focused E2E (/ + /documentation)', status: 'PASS' },
|
||||||
|
{ item: 'CV compileall', status: 'PASS' },
|
||||||
|
{ item: 'CV classify/geometry tests', status: 'PASS (no camera)' },
|
||||||
|
{ item: 'Production / and /documentation', status: 'PASS' },
|
||||||
{ item: 'conveyor-clean.glb checksum', status: 'PASS' },
|
{ item: 'conveyor-clean.glb checksum', status: 'PASS' },
|
||||||
{ item: 'Author FCStd checksum', status: 'PASS' },
|
{ item: 'Author FCStd checksum', status: 'PASS' },
|
||||||
{ item: 'Frozen diverter angles / 0.50 s', status: 'PASS' },
|
{ item: 'Classifier K > 0.8 (web + CV)', status: 'ALIGNED' },
|
||||||
{ item: 'Full contact physics', status: 'NOT_FULLY_VALIDATED' },
|
{ item: 'Full contact physics', status: 'NOT_FULLY_VALIDATED' },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
@@ -146,7 +223,14 @@ export const OFFICIAL_SOURCE_MATRIX = [
|
|||||||
purpose: 'Classifier bounds authority cited by code',
|
purpose: 'Classifier bounds authority cited by code',
|
||||||
present: true,
|
present: true,
|
||||||
canonical: true,
|
canonical: true,
|
||||||
usage: 'Referenced by classifier.ts; not re-parsed this pass',
|
usage: 'Referenced by classifier.ts and cv/classify.py',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: 'presentation/Owl_Prime_Ozon_Tech_Track_3_FINAL.pdf',
|
||||||
|
purpose: 'Final presentation (10 slides)',
|
||||||
|
present: true,
|
||||||
|
canonical: true,
|
||||||
|
usage: 'Single presentation PDF in repository',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
source: 'input_info/extracted/Постановка_Задача_3_сжато_2.pdf',
|
source: 'input_info/extracted/Постановка_Задача_3_сжато_2.pdf',
|
||||||
@@ -169,14 +253,24 @@ export const RUNTIME_FLOW = [
|
|||||||
'RECEIVER',
|
'RECEIVER',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export const CURRENT_LIMITATIONS = [
|
export const REPOSITORY_LAYOUT = [
|
||||||
'Full physical contact sorting through CAD diverters is not fully validated.',
|
'src/ — web digital twin',
|
||||||
'Belt surface-velocity drive at exactly 1 m/s is planned, not complete.',
|
'cv/ — RealSense + OpenCV prototype',
|
||||||
'Per-SKU physical parameters still require profiling/calibration.',
|
'3d_models/ — author CAD',
|
||||||
'Author CAD horn / transmission incomplete in active GLB.',
|
'public/ — runtime GLB/STL/draco',
|
||||||
'Official compliance is partial: missing extracted task PDF; scoring PDF not re-parsed this pass.',
|
'docs/ — engineering notes',
|
||||||
'Repository retains only the web twin, author CAD, official sources, and active tests.',
|
'presentation/ — final PDF',
|
||||||
|
'e2e/ + Vitest — tests',
|
||||||
|
'Docker / nginx — deployment',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
/** @deprecated alias — historical Gate wording retained for acquisition-pack note */
|
export const CURRENT_LIMITATIONS = [
|
||||||
export const DATA_ACQUISITION_SEQUENCE = ['MEASURE', 'VERIFY', 'FREEZE', 'DESIGN'] as const;
|
'Engineering prototype, not an industrial-certified PAK.',
|
||||||
|
'Real CV prototype is not live-integrated into the public website.',
|
||||||
|
'Live camera mode requires Intel RealSense D415 hardware.',
|
||||||
|
'Physical stand parameters still require calibration against the digital twin.',
|
||||||
|
'Full physical contact sorting through CAD diverters is not fully validated.',
|
||||||
|
'Belt surface-velocity drive at exactly 1 m/s is not fully validated.',
|
||||||
|
'Author CAD horn / transmission incomplete in active GLB.',
|
||||||
|
'Cloud presentation/video links for the platform form are provided separately by the team.',
|
||||||
|
] as const;
|
||||||
|
|||||||
@@ -1,30 +1,37 @@
|
|||||||
import AppNav from '../components/AppNav';
|
import AppNav from '../components/AppNav';
|
||||||
import {
|
import {
|
||||||
|
ARCHITECTURE,
|
||||||
CAD_PROVENANCE,
|
CAD_PROVENANCE,
|
||||||
CLASSIFIER_BOUNDS,
|
CLASSIFIER_BOUNDS,
|
||||||
CONFIRMED_LAYOUT,
|
CONFIRMED_LAYOUT,
|
||||||
CURRENT_LIMITATIONS,
|
CURRENT_LIMITATIONS,
|
||||||
|
CV_PROTOTYPE,
|
||||||
DIVERTER_KINEMATICS,
|
DIVERTER_KINEMATICS,
|
||||||
|
MOBILE_BEHAVIOR,
|
||||||
OFFICIAL_SOURCE_MATRIX,
|
OFFICIAL_SOURCE_MATRIX,
|
||||||
|
PHYSICAL_STAND,
|
||||||
PHYSICS_STATUS,
|
PHYSICS_STATUS,
|
||||||
PRODUCTION_STATUS,
|
PRODUCTION_STATUS,
|
||||||
|
REPOSITORY_LAYOUT,
|
||||||
ROUTE_MAPPING,
|
ROUTE_MAPPING,
|
||||||
RUNTIME_FLOW,
|
RUNTIME_FLOW,
|
||||||
|
SOLUTION_COMPONENTS,
|
||||||
VALIDATION_BOARD,
|
VALIDATION_BOARD,
|
||||||
} from '../data/productionStatusSummary';
|
} from '../data/productionStatusSummary';
|
||||||
|
|
||||||
const SECTIONS = [
|
const SECTIONS = [
|
||||||
{ id: 'about', title: '1. Цель проекта' },
|
{ id: 'about', title: '1. Обзор проекта' },
|
||||||
{ id: 'runtime-flow', title: '2. Runtime flow' },
|
{ id: 'production-web', title: '2. Production web' },
|
||||||
{ id: 'layout', title: '3. Активная линия' },
|
{ id: 'cv', title: '3. Real CV prototype' },
|
||||||
{ id: 'classifier', title: '4. Классификация' },
|
{ id: 'classifier', title: '4. Официальная классификация' },
|
||||||
{ id: 'routes', title: '5. Маршруты B/C/D' },
|
{ id: 'stand', title: '5. Физический стенд' },
|
||||||
{ id: 'diverter', title: '6. Diverter state machine' },
|
{ id: 'twin', title: '6. Digital twin' },
|
||||||
{ id: 'cad', title: '7. CAD provenance' },
|
{ id: 'architecture', title: '7. Архитектура' },
|
||||||
{ id: 'physics', title: '8. Физика' },
|
{ id: 'repository', title: '8. Репозиторий' },
|
||||||
{ id: 'validation', title: '9. Validation status' },
|
{ id: 'validation', title: '9. Validation' },
|
||||||
{ id: 'official', title: '10. Official-source matrix' },
|
{ id: 'limits', title: '10. Ограничения' },
|
||||||
{ id: 'limits', title: '11. Known limitations' },
|
{ id: 'routes', title: '11. Маршруты B/C/D' },
|
||||||
|
{ id: 'official', title: '12. Official sources' },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
function TocList({ onNavigate }: { onNavigate?: () => void }) {
|
function TocList({ onNavigate }: { onNavigate?: () => void }) {
|
||||||
@@ -55,15 +62,15 @@ export default function DocumentationPage() {
|
|||||||
O
|
O
|
||||||
</span>
|
</span>
|
||||||
<div className="product-brand-text">
|
<div className="product-brand-text">
|
||||||
<div className="product-brand-title">Ozon Tech Sorter</div>
|
<div className="product-brand-title">OWL PRIME</div>
|
||||||
<div className="product-brand-sub">Цифровой двойник линии сортировки</div>
|
<div className="product-brand-sub">Ozon Tech Track 3 — документация</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<AppNav variant="solid" />
|
<AppNav variant="solid" />
|
||||||
<div className="product-telemetry docs-topbar-meta">
|
<div className="product-telemetry docs-topbar-meta">
|
||||||
<div className="product-tele-item">
|
<div className="product-tele-item">
|
||||||
<span className="product-tele-label">Раздел</span>
|
<span className="product-tele-label">Ветка</span>
|
||||||
<span className="product-tele-value">Документация</span>
|
<span className="product-tele-value">{PRODUCTION_STATUS.canonicalBranch}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -75,7 +82,7 @@ export default function DocumentationPage() {
|
|||||||
</span>
|
</span>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="docs-title">Документация</h1>
|
<h1 className="docs-title">Документация</h1>
|
||||||
<p className="docs-subtitle">Текущее состояние веб-симуляции и инженерный статус</p>
|
<p className="docs-subtitle">Финальный инженерный статус решения OWL PRIME</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<AppNav variant="solid" />
|
<AppNav variant="solid" />
|
||||||
@@ -99,143 +106,158 @@ export default function DocumentationPage() {
|
|||||||
<main className="docs-main">
|
<main className="docs-main">
|
||||||
<section className="docs-status-banner" data-testid="docs-production-status">
|
<section className="docs-status-banner" data-testid="docs-production-status">
|
||||||
<div className="docs-status-label">Текущий статус</div>
|
<div className="docs-status-label">Текущий статус</div>
|
||||||
<div className="docs-status-value">{PRODUCTION_STATUS.acquisitionPackStatus}</div>
|
<div className="docs-status-value">{PRODUCTION_STATUS.projectStatus}</div>
|
||||||
<p className="docs-status-note">
|
<p className="docs-status-note">
|
||||||
Web twin: {PRODUCTION_STATUS.webTwinStatus}. Unit tests {PRODUCTION_STATUS.unitTests},
|
Web: {PRODUCTION_STATUS.webTwinStatus}. CV: {PRODUCTION_STATUS.cvStatus} (live
|
||||||
build {PRODUCTION_STATUS.productionBuild}. Contact physics:{' '}
|
integrated: {PRODUCTION_STATUS.cvLiveIntegrated ? 'YES' : 'NO'}). Unit tests{' '}
|
||||||
{PRODUCTION_STATUS.contactPhysics}. Official compliance:{' '}
|
{PRODUCTION_STATUS.unitTests}, CV tests {PRODUCTION_STATUS.cvTests}, build{' '}
|
||||||
{PRODUCTION_STATUS.officialCompliance}.
|
{PRODUCTION_STATUS.productionBuild}. Domain: {PRODUCTION_STATUS.productionUrl}. Contact
|
||||||
|
physics: {PRODUCTION_STATUS.contactPhysics}.
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="about" className="docs-section">
|
<section id="about" className="docs-section">
|
||||||
<h2>{SECTIONS[0].title}</h2>
|
<h2>{SECTIONS[0].title}</h2>
|
||||||
<p>
|
<p>
|
||||||
Цифровой двойник линии сортировки Ozon Tech: конвейер, камера/классификация, физическая
|
OWL PRIME — инженерный прототип предварительной сортировки товаров для Ozon Tech Track
|
||||||
маршрутизация и три приёмные категории (B / C / D). Страница <code>/</code> — рабочая
|
3: веб-цифровой двойник, реальный CV-прототип, физический экспериментальный стенд,
|
||||||
веб-симуляция; эта страница — каноническая сводка текущего состояния.
|
авторский CAD и единые правила классификации B/C/D.
|
||||||
</p>
|
</p>
|
||||||
|
<ul>
|
||||||
|
{SOLUTION_COMPONENTS.map((item) => (
|
||||||
|
<li key={item}>{item}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="runtime-flow" className="docs-section">
|
<section id="production-web" className="docs-section">
|
||||||
<h2>{SECTIONS[1].title}</h2>
|
<h2>{SECTIONS[1].title}</h2>
|
||||||
<p>{RUNTIME_FLOW.join(' → ')}</p>
|
<p>
|
||||||
</section>
|
Production: <a href={PRODUCTION_STATUS.productionUrl}>{PRODUCTION_STATUS.productionUrl}</a>
|
||||||
|
</p>
|
||||||
<section id="layout" className="docs-section">
|
|
||||||
<h2>{SECTIONS[2].title}</h2>
|
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
Рабочая зона (чертёж): {CONFIRMED_LAYOUT.workspaceMm.length} ×{' '}
|
<code>/</code> — непрерывная симуляция конвейера
|
||||||
{CONFIRMED_LAYOUT.workspaceMm.width} мм; лента {CONFIRMED_LAYOUT.conveyorWidthMm} мм /
|
|
||||||
высота верха {CONFIRMED_LAYOUT.conveyorHeightMm} мм.
|
|
||||||
</li>
|
</li>
|
||||||
<li>Модули CAD: {CONFIRMED_LAYOUT.modules.join(', ')}.</li>
|
<li>
|
||||||
|
<code>/documentation</code> — эта инженерная документация
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
На desktop — интерактивный WebGL 3D twin: CAD-конвейер, цифровое измерение,
|
||||||
|
классификация B/C/D, CAD-дивертеры и физика Rapier. Mobile: {MOBILE_BEHAVIOR.mobile}.
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
Рабочая зона: {CONFIRMED_LAYOUT.workspaceMm.length} × {CONFIRMED_LAYOUT.workspaceMm.width}{' '}
|
||||||
|
мм; лента {CONFIRMED_LAYOUT.conveyorWidthMm} мм / высота{' '}
|
||||||
|
{CONFIRMED_LAYOUT.conveyorHeightMm} мм.
|
||||||
|
</li>
|
||||||
|
<li>Модули: {CONFIRMED_LAYOUT.modules.join(', ')}.</li>
|
||||||
<li>Приёмники: {CONFIRMED_LAYOUT.receivers.join(', ')}.</li>
|
<li>Приёмники: {CONFIRMED_LAYOUT.receivers.join(', ')}.</li>
|
||||||
|
<li>Runtime flow: {RUNTIME_FLOW.join(' → ')}</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="cv" className="docs-section">
|
||||||
|
<h2>{SECTIONS[2].title}</h2>
|
||||||
|
<p>
|
||||||
|
Path: <code>{CV_PROTOTYPE.path}</code>. Status:{' '}
|
||||||
|
<code>{CV_PROTOTYPE.status}</code>. Hardware: {CV_PROTOTYPE.hardware}. Software:{' '}
|
||||||
|
{CV_PROTOTYPE.software}.
|
||||||
|
</p>
|
||||||
|
<p>Pipeline: {CV_PROTOTYPE.pipeline.join(' → ')}</p>
|
||||||
|
<ul>
|
||||||
|
{CV_PROTOTYPE.notes.map((n) => (
|
||||||
|
<li key={n}>{n}</li>
|
||||||
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="classifier" className="docs-section">
|
<section id="classifier" className="docs-section">
|
||||||
<h2>{SECTIONS[3].title}</h2>
|
<h2>{SECTIONS[3].title}</h2>
|
||||||
<p>
|
<p>
|
||||||
Status: <code>{CLASSIFIER_BOUNDS.status}</code>
|
Status: <code>{CLASSIFIER_BOUNDS.status}</code>. Source:{' '}
|
||||||
|
<code>{CLASSIFIER_BOUNDS.officialSourceReference}</code>.
|
||||||
</p>
|
</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Min: {CLASSIFIER_BOUNDS.display.min}</li>
|
<li>Min: {CLASSIFIER_BOUNDS.display.min}</li>
|
||||||
<li>Max: {CLASSIFIER_BOUNDS.display.max}</li>
|
<li>Max: {CLASSIFIER_BOUNDS.display.max}</li>
|
||||||
<li>Roundness: {CLASSIFIER_BOUNDS.display.roundness}</li>
|
<li>Roundness: {CLASSIFIER_BOUNDS.display.roundness}</li>
|
||||||
|
<li>
|
||||||
|
Boundary: <strong>{CLASSIFIER_BOUNDS.display.exactBoundary}</strong>
|
||||||
|
</li>
|
||||||
<li>Order: {CLASSIFIER_BOUNDS.checkOrder}</li>
|
<li>Order: {CLASSIFIER_BOUNDS.checkOrder}</li>
|
||||||
|
<li>Oversized circular product still goes to C (C priority).</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p>
|
<p>
|
||||||
OFFICIAL_SOURCE_REFERENCE: <code>{CLASSIFIER_BOUNDS.officialSourceReference}</code>
|
Web (<code>src/domain/classifier.ts</code>) and CV (<code>cv/classify.py</code>) both use
|
||||||
{CLASSIFIER_BOUNDS.officialSourceParsedThisPass
|
strict <code>K > 0.8</code>.
|
||||||
? ''
|
|
||||||
: ' — PDF not re-parsed in this documentation pass; bounds match current code/tests.'}
|
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="routes" className="docs-section">
|
<section id="stand" className="docs-section">
|
||||||
<h2>{SECTIONS[4].title}</h2>
|
<h2>{SECTIONS[4].title}</h2>
|
||||||
<div className="docs-table-scroll">
|
<p>
|
||||||
<table className="docs-table">
|
Status: <code>{PHYSICAL_STAND.status}</code>. {PHYSICAL_STAND.purpose}
|
||||||
<thead>
|
</p>
|
||||||
<tr>
|
<ul>
|
||||||
<th>Category</th>
|
{PHYSICAL_STAND.elements.map((el) => (
|
||||||
<th>Physical route</th>
|
<li key={el}>{el}</li>
|
||||||
<th>CAD diverter</th>
|
))}
|
||||||
<th>Angle</th>
|
</ul>
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{ROUTE_MAPPING.map((row) => (
|
|
||||||
<tr key={row.category}>
|
|
||||||
<td>{row.category}</td>
|
|
||||||
<td>
|
|
||||||
<code>{row.physicalRoute}</code>
|
|
||||||
</td>
|
|
||||||
<td>{row.activeDiverter}</td>
|
|
||||||
<td>{row.signedAngleDeg}°</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="diverter" className="docs-section">
|
<section id="twin" className="docs-section">
|
||||||
<h2>{SECTIONS[5].title}</h2>
|
<h2>{SECTIONS[5].title}</h2>
|
||||||
<p>{DIVERTER_KINEMATICS.phases.join(' → ')}</p>
|
|
||||||
<ul>
|
|
||||||
<li>Rotation duration: {DIVERTER_KINEMATICS.rotationDurationSec.toFixed(2)} s</li>
|
|
||||||
<li>Safety margin: {DIVERTER_KINEMATICS.openingSafetyMarginSec.toFixed(2)} s</li>
|
|
||||||
<li>Contact plane S: {DIVERTER_KINEMATICS.contactPlaneS}</li>
|
|
||||||
<li>Clear plane S: {DIVERTER_KINEMATICS.clearPlaneS.toFixed(4)}</li>
|
|
||||||
<li>Route command bound to productId; one active product at a time</li>
|
|
||||||
<li>Close after rear-clear of clear plane</li>
|
|
||||||
<li>
|
|
||||||
Invented sorter drive mesh active:{' '}
|
|
||||||
{DIVERTER_KINEMATICS.generatedMechanismActive ? 'YES' : 'NO'}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section id="cad" className="docs-section">
|
|
||||||
<h2>{SECTIONS[6].title}</h2>
|
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
Author: <code>{CAD_PROVENANCE.authorFcstd}</code>
|
Author CAD: <code>{CAD_PROVENANCE.authorFcstd}</code>
|
||||||
<br />
|
|
||||||
SHA-256: <code>{CAD_PROVENANCE.authorSha256}</code>
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
Runtime: <code>{CAD_PROVENANCE.runtimeGlb}</code>
|
Runtime conveyor: <code>{CAD_PROVENANCE.runtimeGlb}</code>
|
||||||
<br />
|
|
||||||
SHA-256: <code>{CAD_PROVENANCE.runtimeSha256}</code>
|
|
||||||
</li>
|
</li>
|
||||||
<li>Author servo/holder nodes used in sorter module only.</li>
|
<li>Product STL models in <code>public/models/</code></li>
|
||||||
<li>Invented sorter drive mesh inactive; author CAD diverters only.</li>
|
<li>Same B/C/D classifier rules as CV</li>
|
||||||
<li>Horn / transmission in GLB: {CAD_PROVENANCE.hornTransmissionInGlb}.</li>
|
<li>Rapier physics + CAD diverters (−45° / +45°)</li>
|
||||||
|
<li>Synchronized digital routing into B/C/D receivers</li>
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
<h3>Physics</h3>
|
||||||
|
<h4>IMPLEMENTED</h4>
|
||||||
<section id="physics" className="docs-section">
|
|
||||||
<h2>{SECTIONS[7].title}</h2>
|
|
||||||
<h3>IMPLEMENTED</h3>
|
|
||||||
<ul>
|
<ul>
|
||||||
{PHYSICS_STATUS.implemented.map((x) => (
|
{PHYSICS_STATUS.implemented.map((x) => (
|
||||||
<li key={x}>{x}</li>
|
<li key={x}>{x}</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
<h3>NOT FULLY VALIDATED</h3>
|
<h4>NOT FULLY VALIDATED</h4>
|
||||||
<ul>
|
<ul>
|
||||||
{PHYSICS_STATUS.notFullyValidated.map((x) => (
|
{PHYSICS_STATUS.notFullyValidated.map((x) => (
|
||||||
<li key={x}>{x}</li>
|
<li key={x}>{x}</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
<h3>PLANNED</h3>
|
<p>
|
||||||
|
Diverter phases: {DIVERTER_KINEMATICS.phases.join(' → ')} (
|
||||||
|
{DIVERTER_KINEMATICS.rotationDurationSec.toFixed(2)} s rotation).
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="architecture" className="docs-section">
|
||||||
|
<h2>{SECTIONS[6].title}</h2>
|
||||||
|
<h3>REAL PATH</h3>
|
||||||
|
<p>{ARCHITECTURE.realPath.join(' → ')}</p>
|
||||||
|
<h3>DIGITAL PATH</h3>
|
||||||
|
<p>{ARCHITECTURE.digitalPath.join(' → ')}</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="repository" className="docs-section">
|
||||||
|
<h2>{SECTIONS[7].title}</h2>
|
||||||
|
<p>
|
||||||
|
Canonical branch: <code>{PRODUCTION_STATUS.canonicalBranch}</code>. Other branches are
|
||||||
|
historical and not required to run the solution.
|
||||||
|
</p>
|
||||||
<ul>
|
<ul>
|
||||||
{PHYSICS_STATUS.planned.map((x) => (
|
{REPOSITORY_LAYOUT.map((line) => (
|
||||||
<li key={x}>{x}</li>
|
<li key={line}>{line}</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
@@ -264,8 +286,45 @@ export default function DocumentationPage() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="official" className="docs-section">
|
<section id="limits" className="docs-section" data-testid="docs-blockers">
|
||||||
<h2>{SECTIONS[9].title}</h2>
|
<h2>{SECTIONS[9].title}</h2>
|
||||||
|
<ul className="docs-blocker-list">
|
||||||
|
{CURRENT_LIMITATIONS.map((b) => (
|
||||||
|
<li key={b}>{b}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="routes" className="docs-section">
|
||||||
|
<h2>{SECTIONS[10].title}</h2>
|
||||||
|
<div className="docs-table-scroll">
|
||||||
|
<table className="docs-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Category</th>
|
||||||
|
<th>Physical route</th>
|
||||||
|
<th>CAD diverter</th>
|
||||||
|
<th>Angle</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{ROUTE_MAPPING.map((row) => (
|
||||||
|
<tr key={row.category}>
|
||||||
|
<td>{row.category}</td>
|
||||||
|
<td>
|
||||||
|
<code>{row.physicalRoute}</code>
|
||||||
|
</td>
|
||||||
|
<td>{row.activeDiverter}</td>
|
||||||
|
<td>{row.signedAngleDeg}°</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="official" className="docs-section">
|
||||||
|
<h2>{SECTIONS[11].title}</h2>
|
||||||
<div className="docs-table-scroll">
|
<div className="docs-table-scroll">
|
||||||
<table className="docs-table">
|
<table className="docs-table">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -293,15 +352,6 @@ export default function DocumentationPage() {
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="limits" className="docs-section" data-testid="docs-blockers">
|
|
||||||
<h2>{SECTIONS[10].title}</h2>
|
|
||||||
<ul className="docs-blocker-list">
|
|
||||||
{CURRENT_LIMITATIONS.map((b) => (
|
|
||||||
<li key={b}>{b}</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
171
src/styles.css
171
src/styles.css
@@ -3312,17 +3312,24 @@ button:disabled {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* =========================================================
|
/* =========================================================
|
||||||
Documentation page
|
Documentation page — light readable surface (Ozon blue)
|
||||||
========================================================= */
|
========================================================= */
|
||||||
|
|
||||||
.docs-page {
|
.docs-page {
|
||||||
|
--docs-bg: #f5f8fc;
|
||||||
|
--docs-panel: #ffffff;
|
||||||
|
--docs-text: #101828;
|
||||||
|
--docs-muted: #344054;
|
||||||
|
--docs-heading: #0b1f3a;
|
||||||
|
--docs-blue: #005bff;
|
||||||
|
--docs-magenta: #f91155;
|
||||||
|
--docs-line: #d0d5dd;
|
||||||
|
--docs-status-bg: #e8f1ff;
|
||||||
min-height: 100dvh;
|
min-height: 100dvh;
|
||||||
max-width: 100vw;
|
max-width: 100vw;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
background:
|
background: var(--docs-bg);
|
||||||
radial-gradient(circle at 12% 0%, rgba(56, 189, 248, 0.12), transparent 32%),
|
color: var(--docs-text);
|
||||||
linear-gradient(180deg, #07111d 0%, #050910 100%);
|
|
||||||
color: var(--text);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.docs-header {
|
.docs-header {
|
||||||
@@ -3336,9 +3343,8 @@ button:disabled {
|
|||||||
gap: 12px 20px;
|
gap: 12px 20px;
|
||||||
min-height: 56px;
|
min-height: 56px;
|
||||||
padding: 14px 20px;
|
padding: 14px 20px;
|
||||||
border-bottom: 1px solid var(--line);
|
border-bottom: 1px solid var(--docs-line);
|
||||||
background: rgba(5, 9, 16, 0.92);
|
background: #ffffff;
|
||||||
backdrop-filter: blur(12px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.docs-brand {
|
.docs-brand {
|
||||||
@@ -3354,9 +3360,9 @@ button:disabled {
|
|||||||
width: 36px;
|
width: 36px;
|
||||||
height: 36px;
|
height: 36px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
background: rgba(56, 189, 248, 0.15);
|
background: rgba(0, 91, 255, 0.1);
|
||||||
border: 1px solid rgba(56, 189, 248, 0.35);
|
border: 1px solid rgba(0, 91, 255, 0.35);
|
||||||
color: var(--cyan);
|
color: var(--docs-blue);
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
@@ -3364,11 +3370,13 @@ button:disabled {
|
|||||||
.docs-title {
|
.docs-title {
|
||||||
font-size: 1.25rem;
|
font-size: 1.25rem;
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
|
color: var(--docs-heading);
|
||||||
}
|
}
|
||||||
|
|
||||||
.docs-subtitle {
|
.docs-subtitle {
|
||||||
color: var(--muted);
|
color: var(--docs-muted);
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.docs-layout {
|
.docs-layout {
|
||||||
@@ -3385,9 +3393,9 @@ button:disabled {
|
|||||||
top: 84px;
|
top: 84px;
|
||||||
align-self: start;
|
align-self: start;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--docs-line);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
background: rgba(10, 19, 31, 0.7);
|
background: var(--docs-panel);
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3405,16 +3413,17 @@ button:disabled {
|
|||||||
|
|
||||||
.docs-toc-desktop a,
|
.docs-toc-desktop a,
|
||||||
.docs-toc-mobile a {
|
.docs-toc-mobile a {
|
||||||
color: var(--muted);
|
color: var(--docs-muted);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
min-height: 36px;
|
min-height: 36px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.docs-toc-desktop a:hover,
|
.docs-toc-desktop a:hover,
|
||||||
.docs-toc-mobile a:hover {
|
.docs-toc-mobile a:hover {
|
||||||
color: var(--cyan);
|
color: var(--docs-blue);
|
||||||
}
|
}
|
||||||
|
|
||||||
.docs-main {
|
.docs-main {
|
||||||
@@ -3432,31 +3441,39 @@ button:disabled {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.docs-status-banner {
|
.docs-status-banner {
|
||||||
border: 1px solid rgba(74, 222, 128, 0.35);
|
border: 1px solid rgba(0, 91, 255, 0.28);
|
||||||
background: rgba(20, 48, 32, 0.45);
|
background: var(--docs-status-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.docs-status-label {
|
.docs-status-label {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.08em;
|
letter-spacing: 0.08em;
|
||||||
color: var(--muted);
|
color: var(--docs-blue);
|
||||||
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.docs-status-value {
|
.docs-status-value {
|
||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
font-size: 1.15rem;
|
font-size: 1.15rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--green);
|
color: var(--docs-heading);
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.docs-status-note {
|
.docs-status-note {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
color: var(--muted);
|
color: var(--docs-muted);
|
||||||
font-size: 0.92rem;
|
font-size: 0.92rem;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-section h2,
|
||||||
|
.docs-section h3,
|
||||||
|
.docs-section h4 {
|
||||||
|
color: var(--docs-heading);
|
||||||
}
|
}
|
||||||
|
|
||||||
.docs-section h2 {
|
.docs-section h2 {
|
||||||
@@ -3464,9 +3481,19 @@ button:disabled {
|
|||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.docs-section a {
|
||||||
|
color: var(--docs-blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-section strong {
|
||||||
|
color: var(--docs-magenta);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
.docs-section p,
|
.docs-section p,
|
||||||
.docs-section li {
|
.docs-section li {
|
||||||
color: #c9d8e8;
|
color: var(--docs-text);
|
||||||
|
opacity: 1;
|
||||||
font-size: 0.98rem;
|
font-size: 0.98rem;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
@@ -3480,19 +3507,22 @@ button:disabled {
|
|||||||
.docs-section code {
|
.docs-section code {
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||||
font-size: 0.88em;
|
font-size: 0.88em;
|
||||||
color: var(--cyan-hover);
|
color: var(--docs-blue);
|
||||||
|
background: rgba(0, 91, 255, 0.08);
|
||||||
|
padding: 0.1em 0.35em;
|
||||||
|
border-radius: 4px;
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.docs-conflict {
|
.docs-conflict {
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
border: 1px solid rgba(245, 158, 11, 0.4);
|
border: 1px solid rgba(249, 17, 85, 0.35);
|
||||||
background: rgba(48, 32, 10, 0.4);
|
background: #fff5f8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.docs-blocker-list li {
|
.docs-blocker-list li {
|
||||||
color: #ffd0d5;
|
color: var(--docs-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.docs-table-scroll {
|
.docs-table-scroll {
|
||||||
@@ -3507,20 +3537,24 @@ button:disabled {
|
|||||||
min-width: 420px;
|
min-width: 420px;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
font-size: 0.92rem;
|
font-size: 0.92rem;
|
||||||
|
background: var(--docs-panel);
|
||||||
}
|
}
|
||||||
|
|
||||||
.docs-table th,
|
.docs-table th,
|
||||||
.docs-table td {
|
.docs-table td {
|
||||||
padding: 8px 10px;
|
padding: 8px 10px;
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--docs-line);
|
||||||
text-align: left;
|
text-align: left;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
white-space: nowrap;
|
white-space: normal;
|
||||||
|
color: var(--docs-text);
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.docs-table th {
|
.docs-table th {
|
||||||
background: rgba(12, 23, 37, 0.9);
|
background: #eef4ff;
|
||||||
color: var(--muted);
|
color: var(--docs-heading);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3689,8 +3723,7 @@ button:disabled {
|
|||||||
background: #070d16;
|
background: #070d16;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-page--product .product-topbar,
|
.main-page--product .product-topbar {
|
||||||
.docs-page .product-topbar {
|
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 30;
|
z-index: 30;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
@@ -3706,6 +3739,41 @@ button:disabled {
|
|||||||
backdrop-filter: blur(14px);
|
backdrop-filter: blur(14px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.docs-page .product-topbar {
|
||||||
|
position: relative;
|
||||||
|
z-index: 30;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 20px;
|
||||||
|
height: 68px;
|
||||||
|
max-height: 72px;
|
||||||
|
padding: 0 24px;
|
||||||
|
border-bottom: 1px solid var(--docs-line);
|
||||||
|
background: #ffffff;
|
||||||
|
backdrop-filter: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-page .product-brand-mark {
|
||||||
|
border-color: rgba(0, 91, 255, 0.35);
|
||||||
|
background: rgba(0, 91, 255, 0.1);
|
||||||
|
color: var(--docs-blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-page .product-brand-title {
|
||||||
|
color: var(--docs-heading);
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-page .product-brand-sub,
|
||||||
|
.docs-page .product-tele-label {
|
||||||
|
color: var(--docs-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-page .product-tele-value {
|
||||||
|
color: var(--docs-text);
|
||||||
|
}
|
||||||
|
|
||||||
.product-brand {
|
.product-brand {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -3742,8 +3810,7 @@ button:disabled {
|
|||||||
color: #8aa0b8;
|
color: #8aa0b8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-page--product .app-nav-solid,
|
.main-page--product .app-nav-solid {
|
||||||
.docs-page .product-topbar .app-nav-solid {
|
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
@@ -3753,14 +3820,38 @@ button:disabled {
|
|||||||
background: rgba(8, 16, 26, 0.7);
|
background: rgba(8, 16, 26, 0.7);
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-page--product .app-nav-link,
|
.docs-page .product-topbar .app-nav-solid,
|
||||||
.docs-page .product-topbar .app-nav-link {
|
.docs-page .docs-header .app-nav-solid {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
display: inline-flex;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 3px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid var(--docs-line);
|
||||||
|
background: #f2f6ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-page--product .app-nav-link {
|
||||||
min-height: 34px;
|
min-height: 34px;
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.docs-page .product-topbar .app-nav-link,
|
||||||
|
.docs-page .docs-header .app-nav-link {
|
||||||
|
min-height: 34px;
|
||||||
|
padding: 0 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: var(--docs-heading);
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-page .app-nav-link.active {
|
||||||
|
background: var(--docs-blue);
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
.product-telemetry {
|
.product-telemetry {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
@@ -3940,11 +4031,9 @@ button:disabled {
|
|||||||
white-space: normal;
|
white-space: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Documentation desktop grid aligned to product chrome */
|
/* Documentation desktop grid — keep light readable surface */
|
||||||
.docs-page {
|
.docs-page {
|
||||||
background:
|
background: var(--docs-bg);
|
||||||
radial-gradient(circle at 10% 0%, rgba(56, 189, 248, 0.1), transparent 34%),
|
|
||||||
linear-gradient(180deg, #07111d 0%, #050910 100%);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.docs-layout {
|
.docs-layout {
|
||||||
@@ -3960,6 +4049,8 @@ button:disabled {
|
|||||||
top: 88px;
|
top: 88px;
|
||||||
padding: 14px 12px;
|
padding: 14px 12px;
|
||||||
font-size: 0.84rem;
|
font-size: 0.84rem;
|
||||||
|
background: var(--docs-panel);
|
||||||
|
border-color: var(--docs-line);
|
||||||
}
|
}
|
||||||
|
|
||||||
.docs-main {
|
.docs-main {
|
||||||
|
|||||||
Reference in New Issue
Block a user