37 Commits

Author SHA1 Message Date
8aa0c16359 vision model 2026-08-03 01:26:42 +07:00
3f8f3a40ec minor fix 2026-08-03 01:25:02 +07:00
e451b84450 fix: CWE-119 2026-07-16 03:46:24 +03:00
988a9ebbf1 fix: minor change schematics 2026-07-16 03:21:17 +03:00
4aeec2bee7 Update: gui actulization contol panel 2026-07-16 03:20:26 +03:00
3b9fc4c874 New: adding vl53l0x sensor 2026-07-16 03:18:20 +03:00
2831c396b9 Update: actualize schematic 2026-07-15 23:47:51 +03:00
31ca8bacd4 Update: actual control program 2026-07-15 23:45:56 +03:00
ff51eccf22 Update: replace board, add servo driver program 2026-07-15 23:44:36 +03:00
50638a11be final: the end of 3d modelling 2026-07-12 12:47:54 +03:00
6b395e083e update: new models 2026-07-12 01:39:08 +03:00
6f57372d30 update: fix model 2026-07-10 03:40:41 +07:00
2618bd9a7f update: create new models 2026-07-09 14:48:40 +07:00
f38e3ff273 Update: adding al profile and some models 2026-07-08 20:36:15 +07:00
422d438b85 New: 3D models 2026-07-08 05:01:29 +07:00
a75d74cab4 New: Create 3D model sptep-drive nema17 2026-07-08 00:39:59 +07:00
256dfbe280 fix: minor change schematic 2026-07-08 00:00:37 +07:00
00d60d03fb Update: update specification 2026-07-07 23:57:00 +07:00
0a0c5915ca fix: change pin number, for spare i2c communication 2026-07-07 23:55:54 +07:00
fd8be02416 New: adding TOF laser and more servo-drive 2026-07-07 23:54:11 +07:00
c62ebaae16 New: adding servo-drive 2026-07-07 21:56:00 +07:00
893f881419 fix: update gui interface 2026-07-07 21:54:53 +07:00
023e699771 New: adding control servo-drive 2026-07-07 21:54:03 +07:00
b69fe70122 Update schematic 2026-07-07 17:25:57 +07:00
70bdfc1c5c minor edit 2026-07-07 17:24:06 +07:00
ff49b03be2 Create gui interface for controll step-drive 2026-07-07 17:23:07 +07:00
15645f7d25 Switch controlling step driver to serial interface 2026-07-07 05:31:54 +07:00
2c4aae9523 delete unused command 2026-07-07 05:31:02 +07:00
e546707069 Update to actual schematic 2026-07-07 00:20:54 +07:00
5fa88eaa56 update script 2026-07-06 23:12:12 +07:00
9c522fb95c refactoring codebase 2026-07-06 23:11:34 +07:00
77997736f1 update script 2026-07-06 21:36:58 +07:00
8f43b9f455 adding more mqtt topics 2026-07-06 21:36:09 +07:00
6ffc2e606b adding backend script + test program for microcontroller 2026-07-06 18:25:06 +07:00
d09e1a58e5 adding README and gitignore files 2026-07-06 18:24:11 +07:00
93dce8e237 Добавил mindmap 2026-07-04 21:16:40 +07:00
e923afd4c7 Добавил набросок конвейера, примерный список запчастей. И записал возможные проблемы и решение от ии 2026-07-03 02:39:23 +07:00
184 changed files with 11149 additions and 196308 deletions

View File

@@ -1,15 +0,0 @@
node_modules
dist
.git
npm-debug.log
.DS_Store
.agent
releases
e2e
playwright-report
test-results
agent/reports
agent/state
docs
*.md
.git

View File

@@ -1,38 +0,0 @@
name: CI
on:
push:
branches: [feature/**, dan_branch, main]
pull_request:
jobs:
build-test:
runs-on: ubuntu-latest
timeout-minutes: 25
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: npm
- run: npm ci
- run: npm test
- run: npm run build
- name: Install Playwright Chromium
run: npx playwright install --with-deps chromium
- name: Start preview
run: |
npx vite preview --host 127.0.0.1 --port 3101 &
for i in $(seq 1 30); do curl -sf http://127.0.0.1:3101/ && break; sleep 1; done
- name: E2E smoke
run: npm run test:e2e
env:
PLAYWRIGHT_BASE_URL: http://127.0.0.1:3101
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: |
playwright-report/
test-results/
if-no-files-found: ignore

22
.gitignore vendored
View File

@@ -1,22 +0,0 @@
node_modules/
dist/
*.tsbuildinfo
vite.config.js
vite.config.d.ts
playwright.config.js
playwright.config.d.ts
.env
.env.*
public/version.json
npm-debug.log*
test-results/
playwright-report/
blob-report/
.demo-preview.pid
.demo-preview.log
__pycache__/
*.pyc
cv/.venv/
cv/debug_frames/
cv/logs/
cv/config.yaml

View File

@@ -1,19 +0,0 @@
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
# .git is dockerignored — pass identity from host deploy script
ARG BUILD_COMMIT=unknown
ARG BUILD_BRANCH=unknown
ARG BUILD_RELEASE=unknown
ENV VITE_BUILD_COMMIT=$BUILD_COMMIT \
VITE_BUILD_BRANCH=$BUILD_BRANCH \
VITE_BUILD_RELEASE=$BUILD_RELEASE
RUN npm run build
FROM nginx:alpine AS runtime
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

240
README.md
View File

@@ -1,213 +1,49 @@
# OZON Sorter Digital Twin
Complete Track 3 Ozon Tech solution in one repository: a web digital twin of the sorting line, plus a real RealSense/OpenCV CV prototype.
**Production:** https://arhipovdan.ru
## 1. Overview
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/` |
| 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.
## 6. Architecture
# Структура репозитория:
```
Real device path:
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
├── arduino_code
│ └── Test
├── include
│ ├── lib
│ ├── platformio.ini
├── src
└── test
├── backend_control
│ ├── create_venv.sh
│ ├── main.py
│ └── testing
│ ├── bin
│ ├── include
│ ├── lib
│ ├── lib64 -> lib
│ └── pyvenv.cfg
├── kicad
│ └── ozon
│ ├── ozon.kicad_pcb
│ ├── ozon.kicad_pro
│ ├── ozon.kicad_sch
│ └── ~ozon.kicad_sch.lck
├── README.md
└── specification
├── mindmap.md
├── mvp_1.drawio
├── mvp_1.jpg
├── mvp_1.md
└── specification.md
```
Shared: B/C/D semantics and official dimension/roundness rules.
Not shared today: live camera frames into the website.
## Папка arduino_code
## 7. Repository structure
Тут находится код для контроллера
```
.github/ CI (build, unit, e2e)
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
```
## Папка Backend_control
No other top-level product directories are required to run or understand the solution.
Тут находится простой python скрипт для управления шагововым двигателем по средствам последовательных команд.
## 8. Requirements
## Папка kicad
**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.
Тут будут находится электрические схемы проекта
## 9. Web quick start
## Папка specification
```bash
npm ci
npm run dev # http://127.0.0.1:3100
npm test -- --run
npm run build
npm run preview # http://127.0.0.1:3100
```
## 10. CV quick start
```bash
cd cv
./demo.sh # venv + deps; HUD on :8080 (needs D415 for live view)
# without camera:
python3 -m venv .venv && .venv/bin/pip install -r requirements.txt
.venv/bin/python test_classify.py
.venv/bin/python test_geometry.py
# live pipeline (hardware):
./run.sh --preview --no-mqtt --no-motor
```
`npm install` does **not** install CV dependencies.
## 11. Configuration
**Web (build-time, optional):** `VITE_BUILD_COMMIT`, `VITE_BUILD_BRANCH`, `VITE_BUILD_RELEASE``/version.json`. No runtime secrets.
**CV:** copy `cv/config.example.yaml``cv/config.yaml` (gitignored). MQTT/motor/routing **disabled by default**. Never commit real passwords or broker credentials.
## 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` |
| Runtime GLB | `public/models/sorter/conveyor-clean.glb` |
| Products | `public/models/*.stl` |
Keep runtime assets in Git for deploy. Mirror large CAD/models/presentation/video to cloud for submission.
SHA-256 (frozen):
```
3d_models/conveer.FCStd
90c1844a4ca05e26def783d6130fc4b993430dde14307534ef8fbb21c9fac2e6
public/models/sorter/conveyor-clean.glb
1dc7a8d7891bfe756e277ad5368df74cb73410156b2fe0f92845afb8a56f285a
```
## 15. Testing
```bash
npm test -- --run
# current release result: 196/196
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
```
## 16. Deployment
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
| 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.
## 18. Known limitations
- Mobile uses 2D lite fallback on current baseline
- 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.
В этой папке разрабатывается спецификация на проект

5
arduino_code/Test/.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
.pio
.vscode/.browse.c_cpp.db*
.vscode/c_cpp_properties.json
.vscode/launch.json
.vscode/ipch

View File

@@ -0,0 +1,10 @@
{
// See http://go.microsoft.com/fwlink/?LinkId=827846
// for the documentation about the extensions.json format
"recommendations": [
"platformio.platformio-ide"
],
"unwantedRecommendations": [
"ms-vscode.cpptools-extension-pack"
]
}

View File

@@ -0,0 +1,95 @@
# 📡 Документация по MQTT интерфейсу (ESP32 + TMC2209 + Servo + VL53L0X)
## 📌 Общая информация
- **Архитектура**: ESP32 (FreeRTOS задача `mqttTask`).
- **Период опроса телеметрии**: 500 мс.
- **Оптимизация трафика**:
1. Публикация данных происходит **только при изменении значения** (строгое кэширование).
2. Используется статический буфер (`intToString`/`uintToString`) вместо динамического класса `String` для экономии памяти и предотвращения фрагментации кучи.
- **Префиксы**:
- `.../control/...` — топики для **отправки команд** устройству (подписка).
- `.../feedback/...` — топики для **получения статуса/телеметрии** от устройства (публикация).
---
## ⚙️ 1. Управление шаговым двигателем (Motor Control)
| Топик | Тип данных | Описание | Пример Payload |
| :--- | :---: | :--- | :--- |
| `motor/control/rpm` | Integer | Целевая скорость в об/мин (RPM). Отрицательные значения включают реверс. | `-150`, `0`, `300` |
| `motor/control/driver` | String | Аппаратное вкл/выкл драйвера (пин `EN_PIN`). `on` = LOW (вкл), иначе = HIGH (выкл). | `on`, `off` |
| `motor/control/totalsteps/reset`| Any | Сброс счетчика шагов в ноль. Устройство сразу опубликует `"0"` в feedback. | `1`, `reset` |
### Телеметрия двигателя (Motor Feedback)
*(Публикуется только при изменении)*
- `motor/feedback/rpm` (Integer): Текущая скорость.
- `motor/feedback/totalsteps` (Integer): Общее количество шагов.
- `motor/feedback/is_run` (String: `true`/`false`): Двигатель движется.
- `motor/feedback/driver/status` (String: `on`/`off`): Общий статус драйвера.
- `motor/feedback/tmc/status` (String: `on`/`off`): Статус программного включения TMC.
#### Детальная телеметрия TMC2209 (`motor/feedback/tmc/...`)
- `current_percent` (Integer): Текущий % рабочего тока.
- `microsteps` (Integer): Текущий режим микрошага.
- `sg_result` (Integer): Текущее значение StallGuard (нагрузка).
- `interstep_duration` (Integer): Длительность между шагами.
- `status/over_temp` (String: `true`/`false`): Перегрев.
- `status/short_to_ground` (String: `true`/`false`): КЗ на землю.
- `status/open_load` (String: `true`/`false`): Обрыв нагрузки.
- `status/stealth_chop_active` (String: `true`/`false`): Активен ли StealthChop.
- `status/standstill` (String: `true`/`false`): Двигатель в покое.
- `status/current_scaling` (Integer): Внутренний масштабный коэффициент тока.
---
## 🦾 2. Управление сервоприводами (Servo Control)
Поддержка нескольких каналов (`{channel}` от `0` до `MAX_SERVOS - 1`).
### Команды
| Топик (пример для канала 0) | Тип данных | Описание | Пример Payload |
| :--- | :---: | :--- | :--- |
| `servo/control/0/angle` | Integer (0-180) | Установить угол поворота. | `90` |
| `servo/control/0/enable` | String | Включить (`on`, `1`, `true`) или выключить. | `on` |
### Обратная связь
- `servo/0/feedback/status` (String: `on`/`off`): Статус питания сервопривода.
- `servo/0/feedback/angle` (Integer): Текущий установленный угол.
---
## 📏 3. Управление датчиками VL53L0X (Sensor Control) **(НОВОЕ)**
Поддержка до 8 каналов (`VL53L0X_MAX_CHANNELS = 8`). Топики используют параметр `{channel}` (07).
| Топик | Тип данных | Описание | Пример Payload |
| :--- | :---: | :--- | :--- |
| `sensor/control/mode` | Integer | Установка глобального режима измерения (0 до `MODE_COUNT - 1`). | `0`, `1` |
| `sensor/control/mode_name` | String | *Заглушка/Логирование.* Принимает имя режима для отладки. | `LongRange` |
| `sensor/control/calibrate/start/{ch}`| Integer | Начало калибровки: указать близкое расстояние в мм. | `50` |
| `sensor/control/calibrate/finish/{ch}`| Integer | Завершение калибровки: указать дальнее расстояние в мм. | `500` |
| `sensor/control/clear_cal/{ch}` | Any | Сбросить калибровку для указанного канала. | `1` |
| `sensor/control/enable/{ch}` | String | Включить (`on`, `1`, `true`) или выключить конкретный канал. | `on` |
| `sensor/control/publish_all` | Any | **Принудительный сброс кэша.** Заставляет устройство немедленно опубликовать текущие значения всех датчиков, даже если они не изменились. | `1` |
---
## 📊 4. Телеметрия датчиков VL53L0X (Sensor Feedback) **(НОВОЕ)**
### Глобальная информация о режиме
Публикуется только при смене режима измерения:
- `sensor/feedback/mode` (String): Человекочитаемое имя текущего режима (например, "Default", "LongRange").
- `sensor/feedback/mode_id` (Integer): Числовой ID текущего режима.
- `sensor/feedback/max_range` (Integer): Максимальная дальность для текущего режима (в мм).
### Постатусная информация по каналам (`{channel}` = 0..7)
*(Публикуется только при изменении состояния или значения)*
| Топик (пример для канала 0) | Тип данных | Описание |
| :--- | :---: | :--- |
| `sensor/feedback/0/status` | String (`on`/`off`) | Включен ли логически данный канал. |
| `sensor/feedback/0/calibrated` | String (`true`/`false`)| Была ли проведена калибровка для этого канала. |
| `sensor/feedback/0/distance` | Integer или String | **Калиброванное** расстояние в мм. Если значение `65535` (ошибка/вне диапазона), публикуется строка `"out_of_range"`. |
| `sensor/feedback/0/raw` | Integer | **Сырое** (некалиброванное) значение расстояния в мм. |
*Примечание: Топики `distance` и `raw` публикуются только если канал активен (`vl53l0xIsChannelActive`).*

View File

@@ -0,0 +1,37 @@
This directory is intended for project header files.
A header file is a file containing C declarations and macro definitions
to be shared between several project source files. You request the use of a
header file in your project source file (C, C++, etc) located in `src` folder
by including it, with the C preprocessing directive `#include'.
```src/main.c
#include "header.h"
int main (void)
{
...
}
```
Including a header file produces the same results as copying the header file
into each source file that needs it. Such copying would be time-consuming
and error-prone. With a header file, the related declarations appear
in only one place. If they need to be changed, they can be changed in one
place, and programs that include the header file will automatically use the
new version when next recompiled. The header file eliminates the labor of
finding and changing all the copies as well as the risk that a failure to
find one copy will result in inconsistencies within a program.
In C, the convention is to give header files names that end with `.h'.
Read more about using header files in official GCC documentation:
* Include Syntax
* Include Operation
* Once-Only Headers
* Computed Includes
https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html

View File

@@ -0,0 +1,46 @@
This directory is intended for project specific (private) libraries.
PlatformIO will compile them to static libraries and link into the executable file.
The source code of each library should be placed in a separate directory
("lib/your_library_name/[Code]").
For example, see the structure of the following example libraries `Foo` and `Bar`:
|--lib
| |
| |--Bar
| | |--docs
| | |--examples
| | |--src
| | |- Bar.c
| | |- Bar.h
| | |- library.json (optional. for custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html
| |
| |--Foo
| | |- Foo.c
| | |- Foo.h
| |
| |- README --> THIS FILE
|
|- platformio.ini
|--src
|- main.c
Example contents of `src/main.c` using Foo and Bar:
```
#include <Foo.h>
#include <Bar.h>
int main (void)
{
...
}
```
The PlatformIO Library Dependency Finder will find automatically dependent
libraries by scanning project source files.
More information about PlatformIO Library Dependency Finder
- https://docs.platformio.org/page/librarymanager/ldf.html

View File

@@ -0,0 +1,28 @@
; PlatformIO Project Configuration File
;
; Build options: build flags, source filter
; Upload options: custom upload port, speed and extra flags
; Library options: dependencies, extra library storages
; Advanced options: extra scripting
;
; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html
[env:upesy_wroom]
platform = espressif32
board = upesy_wroom
framework = arduino
monitor_speed = 115200
upload_speed = 921600
upload_port = /dev/ttyUSB0
lib_deps =
knolleary/PubSubClient@^2.8
janelia-arduino/TMC2209@^9.4.0
madhephaestus/ESP32Servo@^3.2.1
adafruit/Adafruit PWM Servo Driver Library@^3.0.3
pololu/VL53L0X@^1.3.1
build_flags =
-Os
-ffunction-sections
-fdata-sections
-Wl,--gc-sections

View File

@@ -0,0 +1,38 @@
#include "config.h"
const char* WIFI_SSID = "Home";
const char* WIFI_PASS = "88888888qwE";
const char* MQTT_SERVER = "192.168.31.225";
const int MQTT_PORT = 1883;
const char* MQTT_USER = "test";
const char* MQTT_PASS = "1234";
const char* MQTT_CLIENT_ID = "ESP32_Stepper";
// UART2: RX=16, TX=17
HardwareSerial& TMC_SERIAL = Serial2;
const uint32_t TMC_BAUD_RATE = 115200;
const uint8_t TMC_SERIAL_ADDRESS = 0; // Если MS1 и MS2 на GND
const int16_t TMC_RX_PIN = 16;
const int16_t TMC_TX_PIN = 17;
const int EN_PIN = 18;
const int STEPS_PER_REVOLUTION = 200;
const unsigned long RAMP_DURATION_MS = 2000;
// Ток задается в процентах от максимума (зависит от R_sense).
// Для R_sense=0.11 Ом, 100% ~ 1.77А RMS. Для R_sense=0.15 Ом, 100% ~ 1.2А RMS.
const uint8_t TMC_RUN_CURRENT_PERCENT = 50; // 50% тока при движении
const uint8_t TMC_HOLD_CURRENT_PERCENT = 20; // 20% тока в простое
const uint8_t TMC_STALL_GUARD_THRESH = 10;
const uint16_t TMC_MICROSTEPS = 1;
// I2C Настройка
const int16_t I2C_SDA_PIN = 21;
const int16_t I2C_SCL_PIN = 22;
// ========== SERVO CONFIGURATION ==========
#define SERVO_DEFAULT_CHANNEL 0
#define SERVO_MIN_ANGLE 0
#define SERVO_MAX_ANGLE 180

View File

@@ -0,0 +1,38 @@
#ifndef CONFIG_H
#define CONFIG_H
#include <stdint.h>
#include <Arduino.h>
// WiFi & MQTT
extern const char* WIFI_SSID;
extern const char* WIFI_PASS;
extern const char* MQTT_SERVER;
extern const int MQTT_PORT;
extern const char* MQTT_USER;
extern const char* MQTT_PASS;
extern const char* MQTT_CLIENT_ID;
// UART for TMC2209
extern HardwareSerial& TMC_SERIAL;
extern const uint32_t TMC_BAUD_RATE;
extern const uint8_t TMC_SERIAL_ADDRESS; // Адрес драйвера (0-3)
extern const int16_t TMC_RX_PIN;
extern const int16_t TMC_TX_PIN;
extern const int EN_PIN;
// Motor Params
extern const int STEPS_PER_REVOLUTION; // Базовые шаги мотора (обычно 200)
extern const unsigned long RAMP_DURATION_MS;
// TMC2209 Defaults (Токи в процентах 0-100%)
extern const uint8_t TMC_RUN_CURRENT_PERCENT;
extern const uint8_t TMC_HOLD_CURRENT_PERCENT;
extern const uint8_t TMC_STALL_GUARD_THRESH;
extern const uint16_t TMC_MICROSTEPS;
// I2C Настройка
extern const int16_t I2C_SDA_PIN;
extern const int16_t I2C_SCL_PIN;
#endif

View File

@@ -0,0 +1,42 @@
#include <Arduino.h>
#include <TMC2209.h>
#include "config.h"
#include "motor.h"
#include "vl53l0x_sensor.h"
#include "mqtt_handler.h"
void setup() {
Serial.begin(115200);
// Инициализация мотора (Core 1 context initially)
motorInit();
xTaskCreatePinnedToCore(
sensorTask,
"SensorTask",
4096,
NULL,
1,
NULL,
0
);
// Запуск задачи MQTT на Core 0
xTaskCreatePinnedToCore(
mqttTask,
"MQTT_Task",
20480,
NULL,
3,
NULL,
0 // CORE 0
);
Serial.println("System Initialized. Multi-core ready.");
}
void loop() {
// Loop выполняется на Core 1
motorLoop();
vTaskDelay(pdMS_TO_TICKS(5));
}

View File

@@ -0,0 +1,295 @@
#include "motor.h"
#include "config.h"
static TMC2209 stepper_driver;
static bool tmc_initialized = false;
static SemaphoreHandle_t tmc_uart_mutex = NULL;
volatile unsigned long total_steps = 0;
static int target_rpm = 0;
static int current_rpm_display = 0;
static bool is_ramping = false;
static unsigned long ramp_start_ms = 0;
static float start_speed_sps = 0;
static float end_speed_sps = 0;
static float current_speed_sps = 0;
static int32_t last_vactual = 0;
static bool velocity_sent = false; // Флаг для отправки хотя бы раз
static uint16_t current_microsteps = TMC_MICROSTEPS;
static uint8_t current_run_percent = TMC_RUN_CURRENT_PERCENT;
#define TMC_LOCK() xSemaphoreTake(tmc_uart_mutex, portMAX_DELAY)
#define TMC_UNLOCK() xSemaphoreGive(tmc_uart_mutex)
const float TMC_FCLK = 12800000.0;
const float VACTUAL_FACTOR = 8388608.0 / TMC_FCLK;
int32_t calculateVActual(float microsteps_per_second) {
return (int32_t)(microsteps_per_second * VACTUAL_FACTOR);
}
void motorInit() {
pinMode(EN_PIN, OUTPUT);
digitalWrite(EN_PIN, LOW);
tmc_uart_mutex = xSemaphoreCreateMutex();
TMC_SERIAL.begin(TMC_BAUD_RATE, SERIAL_8N1, TMC_RX_PIN, TMC_TX_PIN);
delay(500);
TMC_LOCK();
stepper_driver.setup(TMC_SERIAL, TMC_BAUD_RATE,
(TMC2209::SerialAddress)TMC_SERIAL_ADDRESS,
TMC_RX_PIN, TMC_TX_PIN);
delay(200);
// Проверка связи
if (!stepper_driver.isCommunicating()) {
Serial.println("ERROR: TMC2209 not communicating!");
TMC_UNLOCK();
tmc_initialized = false;
return;
}
Serial.println("TMC2209 communicating OK");
// Базовая настройка
stepper_driver.setMicrostepsPerStep(TMC_MICROSTEPS);
stepper_driver.setRunCurrent(TMC_RUN_CURRENT_PERCENT);
stepper_driver.setHoldCurrent(TMC_HOLD_CURRENT_PERCENT);
stepper_driver.setHoldDelay(7);
stepper_driver.setStallGuardThreshold(TMC_STALL_GUARD_THRESH);
stepper_driver.enableAutomaticCurrentScaling();
stepper_driver.enableAutomaticGradientAdaptation();
// КРИТИЧНО: Отключаем StealthChop для работы moveAtVelocity()!
stepper_driver.disableStealthChop();
delay(10);
// Включаем CoolStep для энергосбережения
stepper_driver.enableCoolStep();
// Программное включение драйвера
stepper_driver.enable();
delay(100);
tmc_initialized = stepper_driver.isSetupAndCommunicating();
if (tmc_initialized) {
Serial.println("TMC2209 initialized successfully");
TMC2209::Settings settings = stepper_driver.getSettings();
Serial.printf("Run: %d%%, Hold: %d%%, Microsteps: %d, StealthChop: %s\n",
settings.irun_percent, settings.ihold_percent,
settings.microsteps_per_step, settings.stealth_chop_enabled ? "ON" : "OFF");
} else {
Serial.println("ERROR: TMC2209 setup failed!");
}
TMC_UNLOCK();
current_microsteps = TMC_MICROSTEPS;
current_run_percent = TMC_RUN_CURRENT_PERCENT;
}
void motorLoop() {
if (is_ramping) {
unsigned long now = millis();
unsigned long elapsed = now - ramp_start_ms;
if (elapsed >= RAMP_DURATION_MS) {
is_ramping = false;
current_speed_sps = end_speed_sps;
} else {
float progress = (float)elapsed / RAMP_DURATION_MS;
current_speed_sps = start_speed_sps + (end_speed_sps - start_speed_sps) * progress;
}
int32_t vactual = calculateVActual(current_speed_sps);
// Отправляем если: скорость изменилась ИЛИ это первая отправка в рампе
if (abs(vactual - last_vactual) >= 0 || !velocity_sent) {
TMC_LOCK();
stepper_driver.moveAtVelocity(vactual);
TMC_UNLOCK();
last_vactual = vactual;
velocity_sent = true;
Serial.printf("VACTUAL: %d (SPS: %.1f, RPM: %d)\n",
vactual, current_speed_sps, current_rpm_display);
}
unsigned long steps_per_rev = (unsigned long)STEPS_PER_REVOLUTION * current_microsteps;
current_rpm_display = ((unsigned long)abs(current_speed_sps) * 60) / steps_per_rev;
} else if (!velocity_sent && current_speed_sps == 0) {
// Если мотор стоит и скорость не отправлялась - отправляем 0
TMC_LOCK();
stepper_driver.moveAtVelocity(0);
TMC_UNLOCK();
velocity_sent = true;
}
if (current_speed_sps != 0) {
total_steps += (unsigned long)(abs(current_speed_sps) * 0.005);
}
vTaskDelay(pdMS_TO_TICKS(5));
}
void setTargetRPM(int rpm) {
target_rpm = rpm;
if (rpm != 0 && current_rpm_display < 5) resetSteps();
unsigned long steps_per_rev = (unsigned long)STEPS_PER_REVOLUTION * current_microsteps;
start_speed_sps = current_speed_sps;
end_speed_sps = (rpm != 0) ? ((float)rpm * steps_per_rev) / 60.0f : 0;
ramp_start_ms = millis();
is_ramping = true;
velocity_sent = false; // Сбрасываем флаг для новой отправки
Serial.printf("Target RPM: %d -> SPS: %.1f\n", rpm, end_speed_sps);
}
void resetSteps() {
total_steps = 0;
}
unsigned long getMotorSteps() { return total_steps; }
int getCurrentRPM() { return current_rpm_display; }
bool isMotorRunning() { return (abs(current_speed_sps) > 1); }
void tmcSetCurrentPercent(uint8_t run_percent, uint8_t hold_percent) {
if (!tmc_initialized) return;
TMC_LOCK();
stepper_driver.setAllCurrentValues(run_percent, hold_percent, 7);
TMC_UNLOCK();
current_run_percent = run_percent;
}
void tmcSetMicrosteps(uint16_t ms) {
if (!tmc_initialized) return;
TMC_LOCK();
stepper_driver.setMicrostepsPerStep(ms);
TMC_UNLOCK();
current_microsteps = ms;
if (target_rpm != 0) setTargetRPM(target_rpm);
}
void tmcSetStallGuard(uint8_t threshold) {
if (!tmc_initialized) return;
TMC_LOCK();
stepper_driver.setStallGuardThreshold(threshold);
TMC_UNLOCK();
}
void tmcSoftwareEnable(bool enable) {
if (!tmc_initialized) return;
TMC_LOCK();
if (enable) {
stepper_driver.enable();
// После enable нужно заново отправить скорость
velocity_sent = false;
} else {
stepper_driver.moveAtVelocity(0);
stepper_driver.disable();
last_vactual = 0;
current_speed_sps = 0;
is_ramping = false;
}
TMC_UNLOCK();
}
void tmcSetStealthChop(bool enable) {
if (!tmc_initialized) return;
TMC_LOCK();
if (enable) {
stepper_driver.enableStealthChop();
// В StealthChop VACTUAL не работает, останавливаем мотор
stepper_driver.moveAtVelocity(0);
last_vactual = 0;
current_speed_sps = 0;
} else {
stepper_driver.disableStealthChop();
velocity_sent = false;
}
TMC_UNLOCK();
}
void tmcSetCoolStep(bool enable) {
if (!tmc_initialized) return;
TMC_LOCK();
enable ? stepper_driver.enableCoolStep() : stepper_driver.disableCoolStep();
TMC_UNLOCK();
}
bool tmcIsInitialized() { return tmc_initialized; }
bool tmcIsCommunicating() {
if (!tmc_initialized) return false;
TMC_LOCK();
bool res = stepper_driver.isCommunicating();
TMC_UNLOCK();
return res;
}
// Функция проверки состояния EN_PIN
bool checkDriverStatus() {
bool en_state = (digitalRead(EN_PIN) == LOW);
return en_state;
}
// Функция проверки программного состояния TMC
bool checkTmcSoftwareEnable() {
TMC2209::Settings s = tmcGetSettings();
bool tmc_state = s.software_enabled;
return tmc_state;
}
uint16_t tmcGetMicrostepsSetting() { return current_microsteps; }
uint8_t tmcGetRunCurrentPercent() { return current_run_percent; }
TMC2209::Status tmcGetStatus() {
TMC2209::Status s = {};
if (!tmc_initialized) return s;
TMC_LOCK();
s = stepper_driver.getStatus();
TMC_UNLOCK();
return s;
}
TMC2209::Settings tmcGetSettings() {
TMC2209::Settings s = {};
if (!tmc_initialized) return s;
TMC_LOCK();
s = stepper_driver.getSettings();
TMC_UNLOCK();
return s;
}
uint16_t tmcGetStallGuardResult() {
if (!tmc_initialized) return 0;
TMC_LOCK();
uint16_t res = stepper_driver.getStallGuardResult();
TMC_UNLOCK();
return res;
}
uint32_t tmcGetInterstepDuration() {
if (!tmc_initialized) return 0;
TMC_LOCK();
uint32_t res = stepper_driver.getInterstepDuration();
TMC_UNLOCK();
return res;
}
uint16_t tmcGetMicrostepCounter() {
if (!tmc_initialized) return 0;
TMC_LOCK();
uint16_t res = stepper_driver.getMicrostepCounter();
TMC_UNLOCK();
return res;
}

View File

@@ -0,0 +1,42 @@
#ifndef MOTOR_H
#define MOTOR_H
#include <Arduino.h>
#include <TMC2209.h>
void motorInit();
void motorLoop();
// Управление движением (через UART VACTUAL)
void setTargetRPM(int rpm); // Поддерживает отрицательные значения для реверса!
void resetSteps();
// Геттеры состояния движения
unsigned long getMotorSteps(); // Считается программно
int getCurrentRPM();
bool isMotorRunning();
// Управление TMC2209 через UART
void tmcSetCurrentPercent(uint8_t run_percent, uint8_t hold_percent);
void tmcSetMicrosteps(uint16_t ms);
void tmcSetStallGuard(uint8_t threshold);
void tmcSoftwareEnable(bool enable); // Вкл/Выкл драйвер программно
void tmcSetStealthChop(bool enable);
void tmcSetCoolStep(bool enable);
// Расширенная телеметрия
bool tmcIsInitialized();
bool tmcIsCommunicating();
uint16_t tmcGetMicrostepsSetting();
uint8_t tmcGetRunCurrentPercent();
// Структуры статусов для MQTT
TMC2209::Status tmcGetStatus();
TMC2209::Settings tmcGetSettings();
uint16_t tmcGetStallGuardResult();
uint32_t tmcGetInterstepDuration();
uint16_t tmcGetMicrostepCounter();
bool checkDriverStatus();
bool checkTmcSoftwareEnable();
#endif

View File

@@ -0,0 +1,506 @@
#include "mqtt_handler.h"
#include "config.h"
#include "motor.h"
#include "servo_control.h"
#include "vl53l0x_sensor.h"
static WiFiClient espClient;
static PubSubClient client(espClient);
// Кэш телеметрии
static unsigned long last_feedback_time = 0;
static int last_pub_rpm = -1;
static unsigned long last_pub_steps = -1;
static int last_pub_is_run = -1;
// TMC Кэш
static uint16_t last_pub_sg = 65535;
static uint32_t last_pub_interstep = 0;
static uint8_t last_pub_current_pct = 255;
static uint16_t last_pub_microsteps = 0;
// Статусы (битовые флаги)
static int last_pub_over_temp = -1;
static int last_pub_short_gnd = -1;
static int last_pub_open_load = -1;
static int last_pub_stealth_active = -1;
static int last_pub_standstill = -1;
static int last_pub_driver_status = -1;
static int last_pub_tmc_software_enable = -1;
static uint8_t last_pub_current_scaling = 255;
// ============================================
// КЭШИРОВАНИЕ ДЛЯ VL53L0X
// ============================================
#define VL53L0X_MAX_CHANNELS 8
static uint16_t last_pub_vl53_distance[VL53L0X_MAX_CHANNELS] = {65535};
static uint16_t last_pub_vl53_raw[VL53L0X_MAX_CHANNELS] = {65535};
static int last_pub_vl53_status[VL53L0X_MAX_CHANNELS] = {-1};
static int last_pub_vl53_calibrated[VL53L0X_MAX_CHANNELS] = {-1};
static MeasurementMode last_pub_vl53_mode = MODE_COUNT;
// ============================================
// БУФЕРЫ ДЛЯ ПРЕОБРАЗОВАНИЯ
// ============================================
static char int_buffer[16];
static char uint_buffer[16];
static const char* intToString(int value) {
snprintf(int_buffer, sizeof(int_buffer), "%d", value);
return int_buffer;
}
static const char* uintToString(unsigned long value) {
snprintf(uint_buffer, sizeof(uint_buffer), "%lu", value);
return uint_buffer;
}
// ============================================
// WIFI И MQTT ПОДКЛЮЧЕНИЕ
// ============================================
static void setup_wifi() {
Serial.print("Connecting to WiFi");
WiFi.begin(WIFI_SSID, WIFI_PASS);
while (WiFi.status() != WL_CONNECTED) {
vTaskDelay(pdMS_TO_TICKS(500));
Serial.print(".");
}
Serial.println("\nWiFi Connected");
}
static void resetAllCaches() {
// Motor
last_pub_rpm = -1;
last_pub_steps = (unsigned long)-1;
last_pub_is_run = -1;
last_pub_sg = 65535;
last_pub_interstep = 0;
last_pub_current_pct = 255;
last_pub_microsteps = 0;
last_pub_over_temp = -1;
last_pub_short_gnd = -1;
last_pub_open_load = -1;
last_pub_stealth_active = -1;
last_pub_standstill = -1;
last_pub_current_scaling = 255;
last_pub_driver_status = -1;
last_pub_tmc_software_enable = -1;
// VL53L0X
for (int i = 0; i < VL53L0X_MAX_CHANNELS; i++) {
last_pub_vl53_distance[i] = 65535;
last_pub_vl53_raw[i] = 65535;
last_pub_vl53_status[i] = -1;
last_pub_vl53_calibrated[i] = -1;
}
last_pub_vl53_mode = MODE_COUNT;
}
static void subscribeToAllTopics() {
// Motor
client.subscribe("motor/control/rpm");
client.subscribe("motor/control/driver");
client.subscribe("motor/control/totalsteps/reset");
client.subscribe("motor/control/tmc/current_percent");
client.subscribe("motor/control/tmc/microsteps");
client.subscribe("motor/control/tmc/stallguard");
client.subscribe("motor/control/tmc/enable");
client.subscribe("motor/control/tmc/stealthchop");
client.subscribe("motor/control/tmc/coolstep");
// Servo
client.subscribe("servo/control/+/#");
// VL53L0X
client.subscribe("sensor/control/mode");
client.subscribe("sensor/control/mode_name");
client.subscribe("sensor/control/calibrate/start/+");
client.subscribe("sensor/control/calibrate/finish/+");
client.subscribe("sensor/control/clear_cal/+");
client.subscribe("sensor/control/enable/+");
client.subscribe("sensor/control/publish_all");
}
static void reconnect() {
while (!client.connected()) {
if (client.connect(MQTT_CLIENT_ID, MQTT_USER, MQTT_PASS)) {
resetAllCaches();
subscribeToAllTopics();
Serial.println("MQTT Connected and subscribed");
} else {
Serial.printf("MQTT connection failed, rc=%d, retrying...\n", client.state());
vTaskDelay(pdMS_TO_TICKS(5000));
}
}
}
// ============================================
// ОБРАБОТКА ВХОДЯЩИХ MQTT КОМАНД
// ============================================
static void callback(char* topic, byte* payload, unsigned int length) {
char msg[length + 1];
memcpy(msg, payload, length);
msg[length] = '\0';
if (strcmp(topic, "motor/control/rpm") == 0) {
setTargetRPM(atoi(msg)); // Поддерживает отрицательные для реверса!
}
else if (strcmp(topic, "motor/control/driver") == 0) {
// TMC2209: LOW = Enabled, HIGH = Disabled
bool enable = (strcmp(msg, "on") == 0);
digitalWrite(EN_PIN, enable ? LOW : HIGH);
}
else if (strcmp(topic, "motor/control/totalsteps/reset") == 0) {
resetSteps();
if (client.connected()) client.publish("motor/feedback/totalsteps", "0");
last_pub_steps = 0;
}
// --- TMC Control ---
else if (strcmp(topic, "motor/control/tmc/current_percent") == 0) {
uint8_t pct = atoi(msg);
if (pct <= 100) tmcSetCurrentPercent(pct, pct / 2); // Hold = 50% от Run
}
else if (strcmp(topic, "motor/control/tmc/microsteps") == 0) {
uint16_t ms = atoi(msg);
tmcSetMicrosteps(ms);
}
else if (strcmp(topic, "motor/control/tmc/stallguard") == 0) {
tmcSetStallGuard(atoi(msg));
}
else if (strcmp(topic, "motor/control/tmc/enable") == 0) {
tmcSoftwareEnable(strcmp(msg, "on") == 0);
}
else if (strcmp(topic, "motor/control/tmc/stealthchop") == 0) {
tmcSetStealthChop(strcmp(msg, "on") == 0);
}
else if (strcmp(topic, "motor/control/tmc/coolstep") == 0) {
tmcSetCoolStep(strcmp(msg, "on") == 0);
}
else if (strncmp(topic, "servo/control", 12) == 0) {
handleServoMQTTCommand(topic, msg);
}
// === VL53L0X ===
else if (strcmp(topic, "sensor/control/mode") == 0) {
int mode = atoi(msg);
if (mode >= 0 && mode < MODE_COUNT) {
vl53l0xSetMode((MeasurementMode)mode);
}
}
else if (strcmp(topic, "sensor/control/mode_name") == 0) {
// Маппинг имени режима на ID (если нужно)
// Пока просто логируем
Serial.printf("Mode name request: %s\n", msg);
}
else if (strncmp(topic, "sensor/control/calibrate/start/", 31) == 0) {
int channel = atoi(topic + 31);
int near_mm = atoi(msg);
if (channel >= 0 && channel < VL53L0X_MAX_CHANNELS && near_mm > 0) {
vl53l0xStartCalibration(channel, near_mm);
}
}
else if (strncmp(topic, "sensor/control/calibrate/finish/", 32) == 0) {
int channel = atoi(topic + 32);
int far_mm = atoi(msg);
if (channel >= 0 && channel < VL53L0X_MAX_CHANNELS && far_mm > 0) {
vl53l0xFinishCalibration(channel, far_mm);
}
}
else if (strncmp(topic, "sensor/control/clear_cal/", 25) == 0) {
int channel = atoi(topic + 25);
if (channel >= 0 && channel < VL53L0X_MAX_CHANNELS) {
vl53l0xClearCalibration(channel);
}
}
else if (strncmp(topic, "sensor/control/enable/", 22) == 0) {
int channel = atoi(topic + 22);
bool enable = (strcmp(msg, "on") == 0 || strcmp(msg, "1") == 0 || strcmp(msg, "true") == 0);
if (channel >= 0 && channel < VL53L0X_MAX_CHANNELS) {
vl53l0xEnableChannel(channel, enable);
}
}
else if (strcmp(topic, "sensor/control/publish_all") == 0) {
// Сбрасываем кэш для принудительной публикации
for (int i = 0; i < VL53L0X_MAX_CHANNELS; i++) {
last_pub_vl53_distance[i] = 65535;
last_pub_vl53_raw[i] = 65535;
}
}
}
// ============================================
// ПУБЛИКАЦИЯ MOTOR TELEMETRY
// ============================================
static void publishMotorTelemetry() {
// RPM
int rpm = getCurrentRPM();
if (rpm != last_pub_rpm) {
client.publish("motor/feedback/rpm", intToString(rpm));
last_pub_rpm = rpm;
}
// Steps
unsigned long steps = getMotorSteps();
if (steps != last_pub_steps) {
client.publish("motor/feedback/totalsteps", uintToString(steps));
last_pub_steps = steps;
}
// Is running
int run = isMotorRunning() ? 1 : 0;
if (run != last_pub_is_run) {
client.publish("motor/feedback/is_run", run ? "true" : "false");
last_pub_is_run = run;
}
// TMC
if (tmcIsInitialized()) {
uint8_t pct = tmcGetRunCurrentPercent();
if (pct != last_pub_current_pct) {
client.publish("motor/feedback/tmc/current_percent", intToString(pct));
last_pub_current_pct = pct;
}
uint16_t ms = tmcGetMicrostepsSetting();
if (ms != last_pub_microsteps) {
client.publish("motor/feedback/tmc/microsteps", intToString(ms));
last_pub_microsteps = ms;
}
uint16_t sg = tmcGetStallGuardResult();
if (sg != last_pub_sg) {
client.publish("motor/feedback/tmc/sg_result", intToString(sg));
last_pub_sg = sg;
}
uint32_t interstep = tmcGetInterstepDuration();
if (interstep != last_pub_interstep) {
client.publish("motor/feedback/tmc/interstep_duration", uintToString(interstep));
last_pub_interstep = interstep;
}
TMC2209::Status status = tmcGetStatus();
int ot = (status.over_temperature_warning || status.over_temperature_shutdown) ? 1 : 0;
if (ot != last_pub_over_temp) {
client.publish("motor/feedback/tmc/status/over_temp", ot ? "true" : "false");
last_pub_over_temp = ot;
}
int sgnd = (status.short_to_ground_a || status.short_to_ground_b) ? 1 : 0;
if (sgnd != last_pub_short_gnd) {
client.publish("motor/feedback/tmc/status/short_to_ground", sgnd ? "true" : "false");
last_pub_short_gnd = sgnd;
}
int ol = (status.open_load_a || status.open_load_b) ? 1 : 0;
if (ol != last_pub_open_load) {
client.publish("motor/feedback/tmc/status/open_load", ol ? "true" : "false");
last_pub_open_load = ol;
}
int sa = status.stealth_chop_mode ? 1 : 0;
if (sa != last_pub_stealth_active) {
client.publish("motor/feedback/tmc/status/stealth_chop_active", sa ? "true" : "false");
last_pub_stealth_active = sa;
}
int ss = status.standstill ? 1 : 0;
if (ss != last_pub_standstill) {
client.publish("motor/feedback/tmc/status/standstill", ss ? "true" : "false");
last_pub_standstill = ss;
}
if (status.current_scaling != last_pub_current_scaling) {
client.publish("motor/feedback/tmc/status/current_scaling", intToString(status.current_scaling));
last_pub_current_scaling = status.current_scaling;
}
int cds = checkDriverStatus() ? 1 : 0;
if (cds != last_pub_driver_status) {
client.publish("motor/feedback/driver/status", cds ? "on" : "off");
last_pub_driver_status = cds;
}
int tse = checkTmcSoftwareEnable() ? 1 : 0;
if (tse != last_pub_tmc_software_enable) {
client.publish("motor/feedback/tmc/status", tse ? "on" : "off");
last_pub_tmc_software_enable = tse;
}
}
}
// ============================================
// ПУБЛИКАЦИЯ VL53L0X TELEMETRY
// ============================================
static void publishVL53L0XTelemetry() {
// Публикация режима
MeasurementMode current_mode = vl53l0xGetMode();
if (current_mode != last_pub_vl53_mode) {
const ModeProfile* profile = vl53l0xGetModeProfile();
client.publish("sensor/feedback/mode", profile->name);
client.publish("sensor/feedback/mode_id", intToString(current_mode));
client.publish("sensor/feedback/max_range", intToString(profile->max_range_mm));
last_pub_vl53_mode = current_mode;
}
// Публикация данных с каждого канала
for (uint8_t ch = 0; ch < VL53L0X_MAX_CHANNELS; ch++) {
if (!vl53l0xIsChannelPresent(ch)) continue;
char topic[64];
// Статус канала
int status = vl53l0xIsChannelEnabled(ch) ? 1 : 0;
if (status != last_pub_vl53_status[ch]) {
snprintf(topic, sizeof(topic), "sensor/feedback/%d/status", ch);
client.publish(topic, status ? "on" : "off");
last_pub_vl53_status[ch] = status;
}
// Статус калибровки
int calibrated = vl53l0xIsCalibrated(ch) ? 1 : 0;
if (calibrated != last_pub_vl53_calibrated[ch]) {
snprintf(topic, sizeof(topic), "sensor/feedback/%d/calibrated", ch);
client.publish(topic, calibrated ? "true" : "false");
last_pub_vl53_calibrated[ch] = calibrated;
}
// Только если канал активен, публикуем расстояния
if (vl53l0xIsChannelActive(ch)) {
// Калиброванное расстояние
uint16_t distance = vl53l0xReadDistance(ch);
if (distance != last_pub_vl53_distance[ch]) {
snprintf(topic, sizeof(topic), "sensor/feedback/%d/distance", ch);
if (distance != 65535) {
client.publish(topic, intToString(distance));
} else {
client.publish(topic, "out_of_range");
}
last_pub_vl53_distance[ch] = distance;
}
// Сырое значение
uint16_t raw = vl53l0xReadRawDistance(ch);
if (raw != last_pub_vl53_raw[ch]) {
snprintf(topic, sizeof(topic), "sensor/feedback/%d/raw", ch);
if (raw != 65535) {
client.publish(topic, intToString(raw));
}
last_pub_vl53_raw[ch] = raw;
}
}
}
}
/////
void checkAndPublishServoStatus(uint8_t channel) {
if (channel >= MAX_SERVOS) return;
bool current_state = servo_enabled[channel];
if (current_state != last_published_status[channel]) {
last_published_status[channel] = current_state;
char topic[64];
snprintf(topic, sizeof(topic), "servo/%d/feedback/status", channel);
String status = current_state ? "on" : "off";
client.publish(topic, status.c_str());
Serial.printf("Published servo %d status: %s\n", channel, status.c_str());
}
}
void checkAndPublishServoAngle(uint8_t channel) {
if (channel >= MAX_SERVOS) return;
uint8_t current_angle = current_angles[channel];
if (current_angle != last_published_angles[channel]) {
last_published_angles[channel] = current_angle;
char topic[64];
snprintf(topic, sizeof(topic), "servo/%d/feedback/angle", channel);
char payload[8];
snprintf(payload, sizeof(payload), "%d", current_angle);
client.publish(topic, payload);
Serial.printf("Published servo %d angle: %d\n", channel, current_angle);
}
}
void handleServoMQTTCommand(const char* topic, const char* payload) {
// Парсим топик: servo/control/{channel}/{command}
int channel = -1;
char command[32] = {0};
if (sscanf(topic, "servo/control/%2d/%8s", &channel, command) != 2) {
Serial.printf("Invalid servo topic: %s\n", topic);
return;
}
if (channel < 0 || channel >= MAX_SERVOS) {
Serial.printf("Invalid servo channel: %d\n", channel);
return;
}
Serial.printf("Servo %d command: %s = %s\n", channel, command, payload);
if (strcmp(command, "angle") == 0) {
int angle = atoi(payload);
if (angle >= 0 && angle <= 180) {
setServoAngle(channel, (uint8_t)angle);
checkAndPublishServoAngle(channel);
checkAndPublishServoStatus(channel);
}
}
else if (strcmp(command, "enable") == 0) {
bool enable = (strcmp(payload, "on") == 0 || strcmp(payload, "1") == 0 || strcmp(payload, "true") == 0);
if (enable) {
enableServo(channel);
} else {
disableServo(channel);
}
checkAndPublishServoStatus(channel);
}
}
/////
// ============================================
// ГЛАВНЫЙ ЦИКЛ ПУБЛИКАЦИИ
// ============================================
static void publishTelemetry() {
unsigned long now = millis();
if (now - last_feedback_time >= 500) {
publishMotorTelemetry();
publishVL53L0XTelemetry();
last_feedback_time = now;
}
}
void mqttTask(void *parameter) {
static unsigned long last_stack_check = 0;
setup_wifi();
client.setServer(MQTT_SERVER, MQTT_PORT);
client.setCallback(callback);
servoInit();
for (;;) {
if (!client.connected()) reconnect();
client.loop();
publishTelemetry();
vTaskDelay(pdMS_TO_TICKS(10));
}
}

View File

@@ -0,0 +1,11 @@
#ifndef MQTT_HANDLER_H
#define MQTT_HANDLER_H
#include <Arduino.h>
#include <WiFi.h>
#include <PubSubClient.h>
void mqttTask(void *parameter);
void handleServoMQTTCommand(const char* topic, const char* payload);
#endif

View File

@@ -0,0 +1,98 @@
#include "servo_control.h"
#include "mqtt_handler.h"
#include "config.h"
static Adafruit_PWMServoDriver pwm = Adafruit_PWMServoDriver();
static bool servo_initialized = false;
void servoInit() {
Serial.println("Initializing PCA9685 servo driver...");
// Инициализация I2C на пинах 21 (SDA) и 22 (SCL)
Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN);
pwm.begin();
pwm.setOscillatorFrequency(27000000);
pwm.setPWMFreq(50); // 50 Hz для сервоприводов
delay(10);
servo_initialized = true;
Serial.println("PCA9685 initialized successfully");
// Инициализируем все каналы как выключенные
for (int i = 0; i < MAX_SERVOS; i++) {
current_angles[i] = 90; // Начальное положение - середина
servo_enabled[i] = false;
disableServo(i);
}
}
// Преобразование угла (0-180) в длину импульса
uint16_t angleToPulse(uint8_t angle) {
if (angle > 180) angle = 180;
return map(angle, 0, 180, SERVO_MIN_PULSE, SERVO_MAX_PULSE);
}
// Преобразование длины импульса в угол
uint8_t pulseToAngle(uint16_t pulse) {
if (pulse < SERVO_MIN_PULSE) return 0;
if (pulse > SERVO_MAX_PULSE) return 180;
return map(pulse, SERVO_MIN_PULSE, SERVO_MAX_PULSE, 0, 180);
}
void setServoAngle(uint8_t channel, uint8_t angle) {
if (!servo_initialized || channel >= MAX_SERVOS) return;
if (angle > 180) angle = 180;
current_angles[channel] = angle;
servo_enabled[channel] = true;
uint16_t pulse = angleToPulse(angle);
setServoPulse(channel, pulse);
Serial.printf("Servo %d: angle=%d, pulse=%d\n", channel, angle, pulse);
}
void setServoPulse(uint8_t channel, uint16_t pulse) {
if (!servo_initialized || channel >= MAX_SERVOS) return;
// Преобразование микросекунд в тики PCA9685
// PCA9685 имеет 4096 тиков на период при 50Hz = 20000 мкс
// 1 мкс = 4096 / 20000 = 0.2048 тика
double pulselength = 4096.0 / 20000.0; // тиков на микросекунду
uint16_t ticks = pulse * pulselength;
pwm.setPWM(channel, 0, ticks);
}
void enableServo(uint8_t channel) {
if (!servo_initialized || channel >= MAX_SERVOS) return;
servo_enabled[channel] = true;
setServoAngle(channel, current_angles[channel]);
Serial.printf("Servo %d: ENABLED\n", channel);
}
void disableServo(uint8_t channel) {
if (!servo_initialized || channel >= MAX_SERVOS) return;
servo_enabled[channel] = false;
pwm.setPWM(channel, 0, 0); // Отключаем сигнал
Serial.printf("Servo %d: DISABLED\n", channel);
}
uint8_t getServoAngle(uint8_t channel) {
if (channel >= MAX_SERVOS) return 0;
return current_angles[channel];
}
bool isServoEnabled(uint8_t channel) {
if (channel >= MAX_SERVOS) return false;
return servo_enabled[channel];
}

View File

@@ -0,0 +1,32 @@
#ifndef SERVO_H
#define SERVO_H
#include <Arduino.h>
#include <Adafruit_PWMServoDriver.h>
// Хранение текущего состояния сервоприводов
#define MAX_SERVOS 16
static uint8_t current_angles[MAX_SERVOS] = {0};
static bool servo_enabled[MAX_SERVOS] = {false};
static uint8_t last_published_angles[MAX_SERVOS] = {255};
static bool last_published_status[MAX_SERVOS] = {false};
// Минимальная и максимальная длина импульса для сервопривода (в микросекундах)
static const uint16_t SERVO_MIN_PULSE = 600;
static const uint16_t SERVO_MAX_PULSE = 2400;
// Инициализация сервопривода
void servoInit();
// Управление сервоприводом
void setServoAngle(uint8_t channel, uint8_t angle);
void setServoPulse(uint8_t channel, uint16_t pulse);
void enableServo(uint8_t channel);
void disableServo(uint8_t channel);
// Получение состояния
uint8_t getServoAngle(uint8_t channel);
bool isServoEnabled(uint8_t channel);
#endif // SERVO_H

View File

@@ -0,0 +1,477 @@
#include "vl53l0x_sensor.h"
#include "config.h"
// ============================================
// КОНСТАНТЫ
// ============================================
#define TCA9548A_ADDRESS 0x70
#define TCA9548A_CHANNELS 8
#define MIN_DISTANCE_MM 30
#define OUT_OF_RANGE_VALUE 65535
#define FILTER_SIZE 5
// ============================================
// ПРОФИЛИ РЕЖИМОВ
// ============================================
static const ModeProfile MODES[MODE_COUNT] = {
{
"HIGH_ACCURACY",
200000, 14, 10, 0.5f, 500, 2
},
{
"PRECISION",
66000, 14, 10, 0.3f, 1000, 5
},
{
"DEFAULT",
33000, 14, 10, 0.25f, 1200, 15
},
{
"LONG_RANGE",
33000, 18, 14, 0.1f, 2000, 40
},
{
"ULTRA_LONG",
100000, 18, 14, 0.05f, 2500, 80
}
};
// ============================================
// ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ
// ============================================
static VL53L0X sensor;
static Preferences preferences;
static MeasurementMode current_mode = MODE_DEFAULT;
static bool sensor_present[TCA9548A_CHANNELS] = {false};
static bool channel_enabled[TCA9548A_CHANNELS] = {false};
static CalibrationData calibration[TCA9548A_CHANNELS];
static uint16_t filter_buffer[TCA9548A_CHANNELS][FILTER_SIZE];
static uint8_t filter_index[TCA9548A_CHANNELS] = {0};
static bool calibration_in_progress = false;
static uint8_t calibration_channel = 0;
static uint16_t calibration_near_raw = 0;
static uint16_t calibration_near_known = 0;
// ============================================
// TCA9548A МУЛЬТИПЛЕКСОР
// ============================================
static void TCA9548A_Select(uint8_t channel) {
Wire.beginTransmission(TCA9548A_ADDRESS);
Wire.write((channel < TCA9548A_CHANNELS) ? (1 << channel) : 0x00);
Wire.endTransmission();
delay(3);
}
static void TCA9548A_DisableAll() {
Wire.beginTransmission(TCA9548A_ADDRESS);
Wire.write(0x00);
Wire.endTransmission();
}
static bool checkTCA9548A() {
Wire.beginTransmission(TCA9548A_ADDRESS);
return (Wire.endTransmission() == 0);
}
// ============================================
// NVS (СОХРАНЕНИЕ В FLASH)
// ============================================
static void loadCalibration() {
preferences.begin("vl53_cal", true);
for (uint8_t ch = 0; ch < TCA9548A_CHANNELS; ch++) {
char key[16];
snprintf(key, sizeof(key), "ch%d", ch);
size_t len = preferences.getBytesLength(key);
if (len == sizeof(CalibrationData)) {
preferences.getBytes(key, &calibration[ch], sizeof(CalibrationData));
} else {
calibration[ch].valid = false;
calibration[ch].scale = 1.0f;
calibration[ch].offset = 0.0f;
}
}
int saved_mode = preferences.getInt("mode", MODE_DEFAULT);
if (saved_mode >= 0 && saved_mode < MODE_COUNT) {
current_mode = (MeasurementMode)saved_mode;
}
preferences.end();
}
static void saveCalibration(uint8_t channel) {
preferences.begin("vl53_cal", false);
char key[16];
snprintf(key, sizeof(key), "ch%d", channel);
preferences.putBytes(key, &calibration[channel], sizeof(CalibrationData));
preferences.end();
}
static void saveMode() {
preferences.begin("vl53_cal", false);
preferences.putInt("mode", (int)current_mode);
preferences.end();
}
// ============================================
// ПРИМЕНЕНИЕ РЕЖИМА
// ============================================
static bool applyModeProfile() {
const ModeProfile& profile = MODES[current_mode];
sensor.setTimeout(500);
if (!sensor.init()) {
return false;
}
sensor.setAddress(0x29);
sensor.setSignalRateLimit(profile.signal_rate_limit);
sensor.setVcselPulsePeriod(VL53L0X::VcselPeriodPreRange, profile.vcsel_prerange);
sensor.setVcselPulsePeriod(VL53L0X::VcselPeriodFinalRange, profile.vcsel_final);
sensor.setMeasurementTimingBudget(profile.timing_budget_us);
return true;
}
// ============================================
// КАЛИБРОВКА И ФИЛЬТРАЦИЯ
// ============================================
static uint16_t applyCalibration(uint8_t channel, uint16_t raw_mm) {
if (!calibration[channel].valid || raw_mm == OUT_OF_RANGE_VALUE) {
return raw_mm;
}
float calibrated = (float)raw_mm * calibration[channel].scale + calibration[channel].offset;
const ModeProfile& profile = MODES[current_mode];
if (calibrated < MIN_DISTANCE_MM) calibrated = MIN_DISTANCE_MM;
if (calibrated > profile.max_range_mm) return OUT_OF_RANGE_VALUE;
return (uint16_t)calibrated;
}
static uint16_t applyFilter(uint8_t channel, uint16_t new_value) {
if (new_value == OUT_OF_RANGE_VALUE) return OUT_OF_RANGE_VALUE;
filter_buffer[channel][filter_index[channel]] = new_value;
filter_index[channel] = (filter_index[channel] + 1) % FILTER_SIZE;
uint32_t sum = 0;
uint8_t count = 0;
for (uint8_t i = 0; i < FILTER_SIZE; i++) {
if (filter_buffer[channel][i] != 0) {
sum += filter_buffer[channel][i];
count++;
}
}
return (count > 0) ? (uint16_t)(sum / count) : new_value;
}
// ============================================
// ПУБЛИЧНЫЕ ФУНКЦИИ
// ============================================
void vl53l0xInit() {
Serial.println("Initializing VL53L0X sensors...");
Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN);
Wire.setClock(400000);
delay(100);
if (!checkTCA9548A()) {
Serial.println("ERROR: TCA9548A not found!");
return;
}
Serial.println("✓ TCA9548A found");
TCA9548A_DisableAll();
loadCalibration();
Serial.printf("✓ Loaded mode: %s\n", MODES[current_mode].name);
Serial.println("\nScanning VL53L0X channels:");
for (uint8_t channel = 0; channel < TCA9548A_CHANNELS; channel++) {
TCA9548A_Select(channel);
delay(20);
if (applyModeProfile()) {
sensor_present[channel] = true;
channel_enabled[channel] = true;
Serial.printf(" ✓ Channel %d: VL53L0X found", channel);
if (calibration[channel].valid) Serial.print(" [CALIBRATED]");
Serial.println();
memset(filter_buffer[channel], 0, sizeof(filter_buffer[channel]));
filter_index[channel] = 0;
} else {
sensor_present[channel] = false;
channel_enabled[channel] = false;
Serial.printf(" ✗ Channel %d: No device\n", channel);
}
TCA9548A_DisableAll();
delay(5);
}
Serial.printf("\n✓ VL53L0X initialized: %d sensors found\n\n",
vl53l0xGetChannelCount());
}
void vl53l0xLoop() {
// Внутренняя логика датчиков (если нужна)
// Сейчас вся публикация в mqtt_handle.cpp
}
bool vl53l0xSetMode(MeasurementMode mode) {
if (mode < 0 || mode >= MODE_COUNT) {
Serial.printf("ERROR: Invalid mode %d\n", mode);
return false;
}
current_mode = mode;
saveMode();
const ModeProfile& profile = MODES[current_mode];
Serial.printf("✓ Mode changed to: %s (max %d mm)\n",
profile.name, profile.max_range_mm);
return true;
}
MeasurementMode vl53l0xGetMode() {
return current_mode;
}
const ModeProfile* vl53l0xGetModeProfile() {
return &MODES[current_mode];
}
uint16_t vl53l0xReadDistance(uint8_t channel) {
if (channel >= TCA9548A_CHANNELS || !sensor_present[channel] || !channel_enabled[channel]) {
return OUT_OF_RANGE_VALUE;
}
TCA9548A_Select(channel);
if (!applyModeProfile()) {
TCA9548A_DisableAll();
return OUT_OF_RANGE_VALUE;
}
uint16_t distance = sensor.readRangeSingleMillimeters();
bool timeout = sensor.timeoutOccurred();
TCA9548A_DisableAll();
if (distance == 65535 || timeout) return OUT_OF_RANGE_VALUE;
const ModeProfile& profile = MODES[current_mode];
if (distance < MIN_DISTANCE_MM || distance > profile.max_range_mm) {
return OUT_OF_RANGE_VALUE;
}
uint16_t filtered = applyFilter(channel, distance);
uint16_t calibrated = applyCalibration(channel, filtered);
return calibrated;
}
uint16_t vl53l0xReadRawDistance(uint8_t channel) {
if (channel >= TCA9548A_CHANNELS || !sensor_present[channel] || !channel_enabled[channel]) {
return OUT_OF_RANGE_VALUE;
}
TCA9548A_Select(channel);
if (!applyModeProfile()) {
TCA9548A_DisableAll();
return OUT_OF_RANGE_VALUE;
}
uint16_t distance = sensor.readRangeSingleMillimeters();
bool timeout = sensor.timeoutOccurred();
TCA9548A_DisableAll();
if (distance == 65535 || timeout) return OUT_OF_RANGE_VALUE;
return distance;
}
bool vl53l0xIsChannelActive(uint8_t channel) {
return (channel < TCA9548A_CHANNELS && sensor_present[channel] && channel_enabled[channel]);
}
int vl53l0xGetChannelCount() {
int count = 0;
for (uint8_t i = 0; i < TCA9548A_CHANNELS; i++) {
if (sensor_present[i]) count++;
}
return count;
}
bool vl53l0xStartCalibration(uint8_t channel, uint16_t near_known_mm) {
if (channel >= TCA9548A_CHANNELS || !sensor_present[channel]) {
Serial.printf("ERROR: Channel %d not available\n", channel);
return false;
}
Serial.printf("Starting calibration for channel %d (near point: %d mm)\n",
channel, near_known_mm);
uint32_t sum = 0;
uint8_t valid = 0;
for (uint8_t i = 0; i < 20; i++) {
uint16_t d = vl53l0xReadRawDistance(channel);
if (d != OUT_OF_RANGE_VALUE) {
sum += d;
valid++;
}
delay(50);
}
if (valid == 0) {
Serial.println("ERROR: Failed to read near point");
return false;
}
calibration_near_raw = (uint16_t)(sum / valid);
calibration_near_known = near_known_mm;
calibration_channel = channel;
calibration_in_progress = true;
Serial.printf("✓ Near point captured: raw=%d mm, actual=%d mm\n",
calibration_near_raw, calibration_near_known);
Serial.println("Now place object at FAR point and call vl53l0xFinishCalibration()");
return true;
}
bool vl53l0xFinishCalibration(uint8_t channel, uint16_t far_known_mm) {
if (!calibration_in_progress || channel != calibration_channel) {
Serial.println("ERROR: Calibration not in progress or wrong channel");
return false;
}
Serial.printf("Finishing calibration for channel %d (far point: %d mm)\n",
channel, far_known_mm);
uint32_t sum = 0;
uint8_t valid = 0;
for (uint8_t i = 0; i < 20; i++) {
uint16_t d = vl53l0xReadRawDistance(channel);
if (d != OUT_OF_RANGE_VALUE) {
sum += d;
valid++;
}
delay(50);
}
if (valid == 0) {
Serial.println("ERROR: Failed to read far point");
calibration_in_progress = false;
return false;
}
uint16_t far_raw = (uint16_t)(sum / valid);
Serial.printf("✓ Far point captured: raw=%d mm, actual=%d mm\n",
far_raw, far_known_mm);
if (far_raw == calibration_near_raw) {
Serial.println("ERROR: Raw values are identical");
calibration_in_progress = false;
return false;
}
float scale = (float)(far_known_mm - calibration_near_known) /
(float)(far_raw - calibration_near_raw);
float offset = (float)calibration_near_known -
(float)calibration_near_raw * scale;
calibration[channel].valid = true;
calibration[channel].near_raw = calibration_near_raw;
calibration[channel].near_known = calibration_near_known;
calibration[channel].far_raw = far_raw;
calibration[channel].far_known = far_known_mm;
calibration[channel].scale = scale;
calibration[channel].offset = offset;
saveCalibration(channel);
Serial.printf("✓ Calibration complete: scale=%.5f, offset=%.2f\n", scale, offset);
calibration_in_progress = false;
return true;
}
void vl53l0xClearCalibration(uint8_t channel) {
if (channel >= TCA9548A_CHANNELS) return;
calibration[channel].valid = false;
calibration[channel].scale = 1.0f;
calibration[channel].offset = 0.0f;
saveCalibration(channel);
Serial.printf("✓ Channel %d calibration cleared\n", channel);
}
bool vl53l0xIsCalibrated(uint8_t channel) {
return (channel < TCA9548A_CHANNELS && calibration[channel].valid);
}
CalibrationData vl53l0xGetCalibration(uint8_t channel) {
if (channel >= TCA9548A_CHANNELS) {
CalibrationData empty = {false, 0, 0, 0, 0, 1.0f, 0.0f};
return empty;
}
return calibration[channel];
}
void vl53l0xEnableChannel(uint8_t channel, bool enable) {
if (channel >= TCA9548A_CHANNELS) return;
if (!sensor_present[channel]) {
Serial.printf("ERROR: Channel %d not present\n", channel);
return;
}
channel_enabled[channel] = enable;
Serial.printf("✓ Channel %d %s\n", channel, enable ? "enabled" : "disabled");
}
bool vl53l0xIsChannelEnabled(uint8_t channel) {
return (channel < TCA9548A_CHANNELS && channel_enabled[channel]);
}
bool vl53l0xIsChannelPresent(uint8_t channel) {
return (channel < TCA9548A_CHANNELS && sensor_present[channel]);
}
void sensorTask(void *parameter) {
vl53l0xInit();
for (;;) {
vl53l0xLoop();
vTaskDelay(pdMS_TO_TICKS(50));
}
}

View File

@@ -0,0 +1,68 @@
#ifndef VL53L0X_SENSOR_H
#define VL53L0X_SENSOR_H
#include <Arduino.h>
#include <VL53L0X.h>
#include <Wire.h>
#include <Preferences.h>
// Режимы измерения
enum MeasurementMode {
MODE_HIGH_ACCURACY = 0,
MODE_PRECISION = 1,
MODE_DEFAULT = 2,
MODE_LONG_RANGE = 3,
MODE_ULTRA_LONG = 4,
MODE_COUNT = 5
};
struct ModeProfile {
const char* name;
uint32_t timing_budget_us;
uint8_t vcsel_prerange;
uint8_t vcsel_final;
float signal_rate_limit;
uint16_t max_range_mm;
uint8_t accuracy_mm;
};
struct CalibrationData {
bool valid;
uint16_t near_raw;
uint16_t near_known;
uint16_t far_raw;
uint16_t far_known;
float scale;
float offset;
};
// Инициализация и цикл
void vl53l0xInit();
void vl53l0xLoop();
// Управление режимами
bool vl53l0xSetMode(MeasurementMode mode);
MeasurementMode vl53l0xGetMode();
const ModeProfile* vl53l0xGetModeProfile();
// Чтение данных
uint16_t vl53l0xReadDistance(uint8_t channel);
uint16_t vl53l0xReadRawDistance(uint8_t channel);
bool vl53l0xIsChannelActive(uint8_t channel);
int vl53l0xGetChannelCount();
// Калибровка
bool vl53l0xStartCalibration(uint8_t channel, uint16_t near_known_mm);
bool vl53l0xFinishCalibration(uint8_t channel, uint16_t far_known_mm);
void vl53l0xClearCalibration(uint8_t channel);
bool vl53l0xIsCalibrated(uint8_t channel);
CalibrationData vl53l0xGetCalibration(uint8_t channel);
// Управление каналами
void vl53l0xEnableChannel(uint8_t channel, bool enable);
bool vl53l0xIsChannelEnabled(uint8_t channel);
bool vl53l0xIsChannelPresent(uint8_t channel);
void sensorTask(void *parameter);
#endif // VL53L0X_SENSOR_H

View File

@@ -0,0 +1,11 @@
This directory is intended for PlatformIO Test Runner and project tests.
Unit Testing is a software testing method by which individual units of
source code, sets of one or more MCU program modules together with associated
control data, usage procedures, and operating procedures, are tested to
determine whether they are fit for use. Unit testing finds problems early
in the development cycle.
More information about PlatformIO Unit Testing:
- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html

1
backend_control/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/testing

61
backend_control/README.md Normal file
View File

@@ -0,0 +1,61 @@
# Интерфейс управления
![](img/gui_interface.png)
## Область Управления
`Целевой RPM` - Задается уставка скорости в диапозоне от -1000 до 1000 оборотов в минуту
`Ток` - Уставка тока в диапозоне 0 - 100%
`StallGuard` -
`Микрошаг` - Уставка микрошага
`Сброс счетчика шагов` - Сброс счетчика шагов
## Область Режимы
`Аппаратное вкл. (Driver EN)` - Аппаратное Включение/Выключение драйвера tmc2209
`Программное вкл. (TMC Chip)` - Программное Включение/Выключение драйвера tmc2209
`StealthChop (Тихий)` - Включение/Выключение тихого режима работы шагового двигателя.
`CoolStep (Энергосбер.)` - Включение/Выключение энергосберегающего режима работы шагового двигателя.
## Область Телеметрия и Статусы
`Текущий RPM (Факт)`
`Всего шагов`
`Вращение`
`SG Result` - Для отслеживания нагрузки на вал или момента срыва шагов. Резкое падение значения sg_result при движении обычно означает столкновение или заклинивание механизма.
`Interstep (ns)`
`Current Scaling`
## Область Ошибки и Флаги
`Перегрев`
`КЗ на землю`
`Обрыв нагрузки`
`StealthChop активен`
`Остановка (Standstill)`
![](img/servo_interface.png)
## Область Управление Серво
Тут можно задать угол сервопривода и отключить сервопривод
## Облать Статус и телеметрия
Вывод информации о сервоприводе включен или выключен. Так же выведен текущий угол.

View File

@@ -0,0 +1,6 @@
#!/bin/bash
python -m venv testing
source testing/bin/activate
pip install -r requirements.txt

460
backend_control/gui.py Normal file
View File

@@ -0,0 +1,460 @@
import customtkinter as ctk
import paho.mqtt.client as mqtt
# ================= НАСТРОЙКИ =================
MQTT_BROKER = "192.168.0.200"
MQTT_PORT = 1883
MQTT_USER = "test"
MQTT_PASSWORD = "1234"
MAX_SERVOS = 4
MAX_SENSORS = 8 # Количество каналов VL53L0X
# Цвета
COLOR_OK = "#28a745"
COLOR_ERR = "#dc3545"
COLOR_OFF = "#555555"
COLOR_ACTIVE = "#00d2ff"
COLOR_PENDING = "#ffaa00"
COLOR_WARN = "#ff9900" # Для out_of_range
class MotorSCADA:
def __init__(self):
ctk.set_appearance_mode("Dark")
ctk.set_default_color_theme("blue")
self.root = ctk.CTk()
self.root.title("🚀 Motor, Servo & Sensor SCADA")
self.root.geometry("1200x900")
self.root.minsize(1100, 800)
# Флаги ожидания
self.driver_pending = False
self.tmc_pending = False
self.servo_pending = {i: False for i in range(MAX_SERVOS)}
self.sensor_pending = {i: False for i in range(MAX_SENSORS)}
self.setup_gui()
self.setup_mqtt()
def setup_gui(self):
# --- Шапка ---
header = ctk.CTkFrame(self.root, height=60)
header.pack(fill="x", padx=20, pady=(20, 10))
header.pack_propagate(False)
ctk.CTkLabel(header, text="Motor, Servo & Sensor SCADA", font=ctk.CTkFont(size=24, weight="bold")).pack(side="left", padx=20)
self.lbl_status = ctk.CTkLabel(header, text="● Отключено", text_color=COLOR_ERR, font=ctk.CTkFont(size=16, weight="bold"))
self.lbl_status.pack(side="right", padx=20)
# --- Вкладки ---
self.tabview = ctk.CTkTabview(self.root)
self.tabview.pack(fill="both", expand=True, padx=20, pady=10)
self.tab_motor = self.tabview.add("Шаговый двигатель (TMC2209)")
self.tab_servo = self.tabview.add(f"Сервоприводы (0-{MAX_SERVOS-1})")
self.tab_sensor = self.tabview.add(f"Датчики VL53L0X (0-{MAX_SENSORS-1})")
self.create_motor_tab(self.tab_motor)
self.create_servo_tab(self.tab_servo)
self.create_sensor_tab(self.tab_sensor)
# ================= ВКЛАДКА ШАГОВОГО ДВИГАТЕЛЯ =================
# (Код для мотора остался без изменений, чтобы не раздувать ответ,
# но в реальном файле он должен быть здесь полностью)
def create_motor_tab(self, parent):
grid = ctk.CTkFrame(parent, fg_color="transparent")
grid.pack(fill="both", expand=True)
grid.grid_columnconfigure((0, 1, 2), weight=1, uniform="col")
grid.grid_rowconfigure(0, weight=1)
self.create_control_frame(grid)
self.create_modes_frame(grid)
self.create_telemetry_frame(grid)
def create_control_frame(self, parent):
frame = ctk.CTkFrame(parent)
frame.grid(row=0, column=0, sticky="nsew", padx=(0, 10))
ctk.CTkLabel(frame, text="⚙️ Управление", font=ctk.CTkFont(size=18, weight="bold")).pack(pady=(10, 20))
ctk.CTkLabel(frame, text="Целевой RPM:").pack(anchor="w", padx=20)
rpm_frame = ctk.CTkFrame(frame, fg_color="transparent")
rpm_frame.pack(fill="x", padx=20, pady=5)
self.sld_rpm = ctk.CTkSlider(rpm_frame, from_=-1000, to=1000, command=self.on_rpm_slider_change)
self.sld_rpm.pack(side="left", fill="x", expand=True, padx=(0, 10))
self.ent_rpm = ctk.CTkEntry(rpm_frame, width=80, justify="right")
self.ent_rpm.insert(0, "0"); self.ent_rpm.pack(side="left", padx=(0, 10))
self.ent_rpm.bind("<Return>", self.on_rpm_entry_apply); self.ent_rpm.bind("<FocusOut>", self.on_rpm_entry_apply)
self.lbl_rpm_val = ctk.CTkLabel(rpm_frame, text="0", width=50); self.lbl_rpm_val.pack(side="right")
ctk.CTkLabel(frame, text="Ток (%):").pack(anchor="w", padx=20, pady=(15,0))
cur_frame = ctk.CTkFrame(frame, fg_color="transparent")
cur_frame.pack(fill="x", padx=20, pady=5)
self.sld_current = ctk.CTkSlider(cur_frame, from_=0, to=100, command=self.on_current_change)
self.sld_current.pack(side="left", fill="x", expand=True)
self.lbl_cur_val = ctk.CTkLabel(cur_frame, text="50", width=50); self.lbl_cur_val.pack(side="right", padx=(10, 0))
ctk.CTkLabel(frame, text="StallGuard (0-255):").pack(anchor="w", padx=20, pady=(15,0))
self.ent_sg = ctk.CTkEntry(frame, width=100); self.ent_sg.insert(0, "0")
self.ent_sg.pack(anchor="w", padx=20, pady=5)
ctk.CTkButton(frame, text="Применить SG", width=150, command=self.on_sg_apply).pack(pady=5)
ctk.CTkLabel(frame, text="Микрошаги:").pack(anchor="w", padx=20, pady=(15,0))
self.opt_msteps = ctk.CTkOptionMenu(frame, values=["1", "2", "4", "8", "16", "32", "64", "128", "256"], command=self.on_msteps_change)
self.opt_msteps.set("16"); self.opt_msteps.pack(anchor="w", padx=20, pady=5)
ctk.CTkButton(frame, text="Сбросить счетчик шагов", fg_color="#dc3545", hover_color="#b02a37", command=self.on_reset_steps).pack(pady=20)
def create_modes_frame(self, parent):
frame = ctk.CTkFrame(parent)
frame.grid(row=0, column=1, sticky="nsew", padx=10)
ctk.CTkLabel(frame, text="🔌 Режимы и Включение", font=ctk.CTkFont(size=18, weight="bold")).pack(pady=(10, 20))
self.sw_driver, self.led_driver_fb = self.create_switch_with_feedback(frame, "Аппаратное вкл. (Driver EN)", self.on_driver_change)
self.sw_tmc_enable, self.led_tmc_fb = self.create_switch_with_feedback(frame, "Программное вкл. (TMC Chip)", self.on_tmc_enable_change)
ctk.CTkFrame(frame, height=2, fg_color="#4a4a6a").pack(fill="x", padx=20, pady=15)
self.sw_stealth = self.create_switch(frame, "StealthChop (Тихий)", self.on_stealth_change)
self.sw_cool = self.create_switch(frame, "CoolStep (Энергосбер.)", self.on_cool_change)
def create_telemetry_frame(self, parent):
frame = ctk.CTkFrame(parent)
frame.grid(row=0, column=2, sticky="nsew", padx=(10, 0))
ctk.CTkLabel(frame, text="📊 Телеметрия и Статусы", font=ctk.CTkFont(size=18, weight="bold")).pack(pady=(10, 10))
tel_frame = ctk.CTkFrame(frame); tel_frame.pack(fill="x", padx=10, pady=5)
self.lbl_fb_rpm = self.create_telemetry_row(tel_frame, "Текущий RPM:")
self.lbl_fb_steps = self.create_telemetry_row(tel_frame, "Всего шагов:")
self.lbl_fb_run = self.create_telemetry_row(tel_frame, "Вращение:")
self.lbl_fb_sg = self.create_telemetry_row(tel_frame, "SG Result:")
self.lbl_fb_interstep = self.create_telemetry_row(tel_frame, "Interstep:")
self.lbl_fb_cscale = self.create_telemetry_row(tel_frame, "Current Scaling:")
ctk.CTkLabel(frame, text="🚨 Ошибки и Флаги", font=ctk.CTkFont(size=16, weight="bold")).pack(pady=(15, 5))
stat_frame = ctk.CTkFrame(frame); stat_frame.pack(fill="x", padx=10, pady=5)
self.led_over_temp = self.create_led_row(stat_frame, "Перегрев:")
self.led_short_gnd = self.create_led_row(stat_frame, "КЗ на землю:")
self.led_open_load = self.create_led_row(stat_frame, "Обрыв нагрузки:")
self.led_stealth_act = self.create_led_row(stat_frame, "StealthChop активен:")
self.led_standstill = self.create_led_row(stat_frame, "Остановка:")
# ================= ВКЛАДКА СЕРВОПРИВОДОВ =================
def create_servo_tab(self, parent):
grid = ctk.CTkFrame(parent, fg_color="transparent")
grid.pack(fill="both", expand=True, padx=10, pady=10)
grid.grid_columnconfigure((0, 1), weight=1, uniform="col")
grid.grid_rowconfigure((0, 1), weight=1, uniform="row")
self.servo_ui = {}
for i in range(MAX_SERVOS):
row, col = divmod(i, 2)
frame = ctk.CTkFrame(grid)
frame.grid(row=row, column=col, sticky="nsew", padx=10, pady=10)
self.servo_ui[i] = self.create_servo_card(frame, i)
def create_servo_card(self, parent, channel):
ui = {}
ctk.CTkLabel(parent, text=f"🦾 Сервопривод #{channel}", font=ctk.CTkFont(size=16, weight="bold")).pack(pady=(10, 10))
ctk.CTkLabel(parent, text="Угол (0-180°):").pack(anchor="w", padx=20)
ang_frame = ctk.CTkFrame(parent, fg_color="transparent"); ang_frame.pack(fill="x", padx=20, pady=5)
sld = ctk.CTkSlider(ang_frame, from_=0, to=180, command=lambda val, ch=channel: self.on_servo_ang_slider(ch, val))
sld.pack(side="left", fill="x", expand=True, padx=(0, 10)); sld.set(90)
ent = ctk.CTkEntry(ang_frame, width=60, justify="right"); ent.insert(0, "90"); ent.pack(side="left", padx=(0, 10))
ent.bind("<Return>", lambda event, ch=channel: self.on_servo_ang_entry(ch, event))
ent.bind("<FocusOut>", lambda event, ch=channel: self.on_servo_ang_entry(ch, event))
lbl_val = ctk.CTkLabel(ang_frame, text="90", width=40); lbl_val.pack(side="right")
ui['slider_ang'], ui['entry_ang'], ui['lbl_ang_val'] = sld, ent, lbl_val
sw, led_fb = self.create_switch_with_feedback(parent, "Включить серво", lambda ch=channel: self.on_servo_enable_change(ch))
ui['switch_en'], ui['led_fb'] = sw, led_fb
tel_frame = ctk.CTkFrame(parent); tel_frame.pack(fill="x", padx=10, pady=15)
ui['lbl_fb_ang'] = self.create_telemetry_row(tel_frame, "Текущий угол:")
stat_frame = ctk.CTkFrame(parent); stat_frame.pack(fill="x", padx=10, pady=5)
ui['led_status'] = self.create_led_row(stat_frame, "Статус:")
return ui
# ================= ВКЛАДКА ДАТЧИКОВ VL53L0X =================
def create_sensor_tab(self, parent):
main_frame = ctk.CTkFrame(parent, fg_color="transparent")
main_frame.pack(fill="both", expand=True, padx=10, pady=10)
# --- Глобальное управление ---
global_frame = ctk.CTkFrame(main_frame)
global_frame.pack(fill="x", padx=10, pady=(0, 10))
ctk.CTkLabel(global_frame, text="📏 Глобальные настройки VL53L0X", font=ctk.CTkFont(size=18, weight="bold")).pack(pady=(10, 5))
info_frame = ctk.CTkFrame(global_frame, fg_color="transparent")
info_frame.pack(fill="x", padx=20, pady=10)
self.lbl_sensor_mode = self.create_telemetry_row(info_frame, "Режим:")
self.lbl_sensor_mode_id = self.create_telemetry_row(info_frame, "ID режима:")
self.lbl_sensor_max_range = self.create_telemetry_row(info_frame, "Макс. дальность (мм):")
ctrl_frame = ctk.CTkFrame(global_frame, fg_color="transparent")
ctrl_frame.pack(fill="x", padx=20, pady=(0, 10))
ctk.CTkLabel(ctrl_frame, text="Выбрать режим:").pack(side="left", padx=(0, 10))
# Предполагаем, что режимов от 0 до 4 (Default, HighAccuracy, LongRange, HighSpeed)
self.opt_sensor_mode = ctk.CTkOptionMenu(ctrl_frame, values=["0", "1", "2", "3", "4"], width=100, command=self.on_sensor_mode_change)
self.opt_sensor_mode.set("0"); self.opt_sensor_mode.pack(side="left", padx=(0, 20))
ctk.CTkButton(ctrl_frame, text="🔄 Принудительно обновить все", fg_color="#007bff", hover_color="#0056b3", command=self.on_sensor_publish_all).pack(side="right")
# --- Сетка каналов (Scrollable) ---
scroll_frame = ctk.CTkScrollableFrame(main_frame)
scroll_frame.pack(fill="both", expand=True, padx=10, pady=10)
scroll_frame.grid_columnconfigure((0, 1, 2, 3), weight=1, uniform="col")
self.sensor_ui = {}
for i in range(MAX_SENSORS):
row, col = divmod(i, 4)
frame = ctk.CTkFrame(scroll_frame)
frame.grid(row=row, column=col, sticky="nsew", padx=5, pady=5)
self.sensor_ui[i] = self.create_sensor_card(frame, i)
def create_sensor_card(self, parent, channel):
ui = {}
ctk.CTkLabel(parent, text=f"📡 Канал #{channel}", font=ctk.CTkFont(size=14, weight="bold")).pack(pady=(5, 5))
# Включение и калибровка
sw, led_fb = self.create_switch_with_feedback(parent, "Включить", lambda ch=channel: self.on_sensor_enable_change(ch))
ui['switch_en'], ui['led_fb'] = sw, led_fb
cal_stat_frame = ctk.CTkFrame(parent, fg_color="transparent")
cal_stat_frame.pack(fill="x", padx=10, pady=5)
ctk.CTkLabel(cal_stat_frame, text="Калибровка:").pack(side="left")
ui['led_calibrated'] = ctk.CTkLabel(cal_stat_frame, text="", font=ctk.CTkFont(size=16), text_color=COLOR_OFF)
ui['led_calibrated'].pack(side="right")
# Поля калибровки
cal_ctrl_frame = ctk.CTkFrame(parent, fg_color="transparent")
cal_ctrl_frame.pack(fill="x", padx=10, pady=5)
ctk.CTkLabel(cal_ctrl_frame, text="Ближняя (мм):").pack(anchor="w")
ent_near = ctk.CTkEntry(cal_ctrl_frame, width=60, justify="right"); ent_near.insert(0, "50"); ent_near.pack(side="left", padx=(0, 5))
btn_start = ctk.CTkButton(cal_ctrl_frame, text="Старт", width=60, height=28, command=lambda ch=channel, e=ent_near: self.on_sensor_cal_start(ch, e))
btn_start.pack(side="right")
ctk.CTkLabel(cal_ctrl_frame, text="Дальняя (мм):").pack(anchor="w", pady=(5,0))
ent_far = ctk.CTkEntry(cal_ctrl_frame, width=60, justify="right"); ent_far.insert(0, "500"); ent_far.pack(side="left", padx=(0, 5), pady=(5,0))
btn_finish = ctk.CTkButton(cal_ctrl_frame, text="Финиш", width=60, height=28, command=lambda ch=channel, e=ent_far: self.on_sensor_cal_finish(ch, e))
btn_finish.pack(side="right")
ctk.CTkButton(parent, text="Сбросить калибровку", fg_color="#6c757d", hover_color="#5a6268", height=28, command=lambda ch=channel: self.on_sensor_clear_cal(ch)).pack(pady=5)
ui['ent_near'], ui['ent_far'] = ent_near, ent_far
# Телеметрия
tel_frame = ctk.CTkFrame(parent)
tel_frame.pack(fill="x", padx=5, pady=5)
ui['lbl_dist'] = self.create_telemetry_row(tel_frame, "Дист. (мм):")
ui['lbl_raw'] = self.create_telemetry_row(tel_frame, "Сырое (мм):")
return ui
# ================= ВСПОМОГАТЕЛЬНЫЕ МЕТОДЫ GUI =================
def create_switch(self, parent, text, command):
frame = ctk.CTkFrame(parent, fg_color="transparent"); frame.pack(fill="x", padx=10, pady=5)
ctk.CTkLabel(frame, text=text).pack(side="left")
switch = ctk.CTkSwitch(frame, text="", command=command); switch.pack(side="right")
return switch
def create_switch_with_feedback(self, parent, text, command):
frame = ctk.CTkFrame(parent, fg_color="transparent"); frame.pack(fill="x", padx=10, pady=5)
ctk.CTkLabel(frame, text=text).pack(side="left")
feedback_led = ctk.CTkLabel(frame, text="", font=ctk.CTkFont(size=18), text_color=COLOR_OFF)
feedback_led.pack(side="right", padx=(10, 0))
switch = ctk.CTkSwitch(frame, text="", command=command); switch.pack(side="right", padx=(10, 0))
return switch, feedback_led
def create_telemetry_row(self, parent, text):
frame = ctk.CTkFrame(parent, fg_color="transparent"); frame.pack(fill="x", pady=2)
ctk.CTkLabel(frame, text=text, anchor="w", font=ctk.CTkFont(size=12)).pack(side="left")
val_lbl = ctk.CTkLabel(frame, text="-", font=ctk.CTkFont(weight="bold", size=12), text_color=COLOR_ACTIVE, anchor="e")
val_lbl.pack(side="right")
return val_lbl
def create_led_row(self, parent, text):
frame = ctk.CTkFrame(parent, fg_color="transparent"); frame.pack(fill="x", pady=2)
ctk.CTkLabel(frame, text=text, anchor="w", font=ctk.CTkFont(size=12)).pack(side="left")
led_lbl = ctk.CTkLabel(frame, text="", font=ctk.CTkFont(size=16), text_color=COLOR_OFF)
led_lbl.pack(side="right")
return led_lbl
# ================= MQTT =================
def setup_mqtt(self):
self.client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="python_scada")
self.client.username_pw_set(MQTT_USER, MQTT_PASSWORD)
self.client.on_connect = self.on_mqtt_connect
self.client.on_disconnect = self.on_mqtt_disconnect
self.client.on_message = self.on_mqtt_message
try:
self.client.connect(MQTT_BROKER, MQTT_PORT, 60)
self.client.loop_start()
except Exception as e:
print(f"Ошибка подключения: {e}")
self.update_status(False)
def on_mqtt_connect(self, client, userdata, flags, reason_code, properties):
if reason_code == 0:
self.root.after(0, self.update_status, True)
client.subscribe("motor/feedback/#")
client.subscribe("servo/+/feedback/#")
client.subscribe("sensor/feedback/#") # Подписка на датчики
else:
self.root.after(0, self.update_status, False)
def on_mqtt_disconnect(self, client, userdata, flags, reason_code, properties):
self.root.after(0, self.update_status, False)
def on_mqtt_message(self, client, userdata, msg):
self.root.after(0, self.process_feedback, msg.topic, msg.payload.decode('utf-8'))
def process_feedback(self, topic, val):
parts = topic.split('/')
# --- MOTOR ---
if parts[0] == 'motor' and parts[1] == 'feedback':
if topic == "motor/feedback/rpm": self.lbl_fb_rpm.configure(text=val)
elif topic == "motor/feedback/totalsteps": self.lbl_fb_steps.configure(text=val)
elif topic == "motor/feedback/is_run":
is_run = val == "true"
self.lbl_fb_run.configure(text="Да" if is_run else "Нет", text_color=COLOR_OK if is_run else COLOR_ERR)
elif topic == "motor/feedback/tmc/current_percent": self.lbl_cur_val.configure(text=val); self.sld_current.set(int(val))
elif topic == "motor/feedback/tmc/microsteps": self.opt_msteps.set(val)
elif topic == "motor/feedback/tmc/sg_result": self.lbl_fb_sg.configure(text=val)
elif topic == "motor/feedback/tmc/interstep_duration": self.lbl_fb_interstep.configure(text=val)
elif topic == "motor/feedback/tmc/status/current_scaling": self.lbl_fb_cscale.configure(text=val)
elif topic == "motor/feedback/tmc/status/over_temp": self.update_led(self.led_over_temp, val, True)
elif topic == "motor/feedback/tmc/status/short_to_ground": self.update_led(self.led_short_gnd, val, True)
elif topic == "motor/feedback/tmc/status/open_load": self.update_led(self.led_open_load, val, True)
elif topic == "motor/feedback/tmc/status/stealth_chop_active": self.update_led(self.led_stealth_act, val, False)
elif topic == "motor/feedback/tmc/status/standstill": self.update_led(self.led_standstill, val, False)
elif topic == "motor/feedback/driver/status":
is_on = val == "on"; self.driver_pending = False
if self.sw_driver.get() != is_on: self.sw_driver.select() if is_on else self.sw_driver.deselect()
self.led_driver_fb.configure(text_color=COLOR_OK if is_on else COLOR_OFF)
elif topic == "motor/feedback/tmc/status":
is_on = val == "on"; self.tmc_pending = False
if self.sw_tmc_enable.get() != is_on: self.sw_tmc_enable.select() if is_on else self.sw_tmc_enable.deselect()
self.led_tmc_fb.configure(text_color=COLOR_OK if is_on else COLOR_OFF)
# --- SERVO ---
elif parts[0] == 'servo' and len(parts) == 4 and parts[2] == 'feedback':
try:
ch = int(parts[1]); param = parts[3]
if ch in self.servo_ui:
ui = self.servo_ui[ch]
if param == 'angle': ui['lbl_fb_ang'].configure(text=val)
elif param == 'status':
is_on = val == "on"; self.servo_pending[ch] = False
if ui['switch_en'].get() != is_on: ui['switch_en'].select() if is_on else ui['switch_en'].deselect()
ui['led_fb'].configure(text_color=COLOR_OK if is_on else COLOR_OFF)
self.update_led(ui['led_status'], val, False)
except ValueError: pass
# --- SENSOR ---
elif parts[0] == 'sensor' and parts[1] == 'feedback':
if len(parts) == 3: # Глобальные sensor/feedback/mode...
param = parts[2]
if param == 'mode': self.lbl_sensor_mode.configure(text=val)
elif param == 'mode_id': self.lbl_sensor_mode_id.configure(text=val); self.opt_sensor_mode.set(val)
elif param == 'max_range': self.lbl_sensor_max_range.configure(text=val)
elif len(parts) == 4: # Канальные sensor/feedback/{ch}/...
try:
ch = int(parts[2]); param = parts[3]
if ch in self.sensor_ui:
ui = self.sensor_ui[ch]
if param == 'status':
is_on = val == "on"; self.sensor_pending[ch] = False
if ui['switch_en'].get() != is_on: ui['switch_en'].select() if is_on else ui['switch_en'].deselect()
ui['led_fb'].configure(text_color=COLOR_OK if is_on else COLOR_OFF)
elif param == 'calibrated':
is_cal = val == "true"
ui['led_calibrated'].configure(text_color=COLOR_OK if is_cal else COLOR_OFF)
elif param == 'distance':
if val == "out_of_range":
ui['lbl_dist'].configure(text="Вне диапазона", text_color=COLOR_WARN)
else:
ui['lbl_dist'].configure(text=val, text_color=COLOR_ACTIVE)
elif param == 'raw':
ui['lbl_raw'].configure(text=val, text_color=COLOR_ACTIVE)
except ValueError: pass
# ================= ОБРАБОТЧИКИ СОБЫТИЙ =================
def publish(self, topic, payload):
if self.client.is_connected(): self.client.publish(topic, str(payload), qos=1)
def update_status(self, is_online):
self.lbl_status.configure(text="● Подключено" if is_online else "● Отключено", text_color=COLOR_OK if is_online else COLOR_ERR)
def update_led(self, label, val, is_error):
is_true = val in ["true", "1", "on"]
label.configure(text_color=COLOR_ERR if (is_true and is_error) else (COLOR_OK if is_true else COLOR_OFF))
# --- Motor Handlers ---
def on_rpm_slider_change(self, value):
int_val = int(value); self.ent_rpm.delete(0, ctk.END); self.ent_rpm.insert(0, str(int_val))
self.lbl_rpm_val.configure(text=str(int_val)); self.publish("motor/control/rpm", int_val)
def on_rpm_entry_apply(self, event=None):
try:
int_val = max(-1000, min(1000, int(self.ent_rpm.get())))
self.sld_rpm.set(int_val); self.lbl_rpm_val.configure(text=str(int_val)); self.publish("motor/control/rpm", int_val)
except ValueError: self.ent_rpm.delete(0, ctk.END); self.ent_rpm.insert(0, str(int(self.sld_rpm.get())))
def on_current_change(self, value):
int_val = int(value); self.lbl_cur_val.configure(text=str(int_val)); self.publish("motor/control/tmc/current_percent", int_val)
def on_sg_apply(self):
val = self.ent_sg.get()
if val.isdigit() and 0 <= int(val) <= 255: self.publish("motor/control/tmc/stallguard", int(val))
def on_msteps_change(self, choice): self.publish("motor/control/tmc/microsteps", int(choice))
def on_reset_steps(self): self.publish("motor/control/totalsteps/reset", "1")
def on_driver_change(self):
is_on = self.sw_driver.get(); self.driver_pending = True; self.led_driver_fb.configure(text_color=COLOR_PENDING)
self.publish("motor/control/driver", "on" if is_on else "off")
def on_tmc_enable_change(self):
is_on = self.sw_tmc_enable.get(); self.tmc_pending = True; self.led_tmc_fb.configure(text_color=COLOR_PENDING)
self.publish("motor/control/tmc/enable", "on" if is_on else "off")
def on_stealth_change(self): self.publish("motor/control/tmc/stealthchop", "on" if self.sw_stealth.get() else "off")
def on_cool_change(self): self.publish("motor/control/tmc/coolstep", "on" if self.sw_cool.get() else "off")
# --- Servo Handlers ---
def on_servo_ang_slider(self, channel, value):
int_val = int(value); ui = self.servo_ui[channel]
ui['entry_ang'].delete(0, ctk.END); ui['entry_ang'].insert(0, str(int_val))
ui['lbl_ang_val'].configure(text=str(int_val)); self.publish(f"servo/control/{channel}/angle", int_val)
def on_servo_ang_entry(self, channel, event=None):
ui = self.servo_ui[channel]
try:
int_val = max(0, min(180, int(ui['entry_ang'].get())))
ui['slider_ang'].set(int_val); ui['lbl_ang_val'].configure(text=str(int_val)); self.publish(f"servo/control/{channel}/angle", int_val)
except ValueError: ui['entry_ang'].delete(0, ctk.END); ui['entry_ang'].insert(0, str(int(ui['slider_ang'].get())))
def on_servo_enable_change(self, channel):
ui = self.servo_ui[channel]; is_on = ui['switch_en'].get()
self.servo_pending[channel] = True; ui['led_fb'].configure(text_color=COLOR_PENDING)
self.publish(f"servo/control/{channel}/enable", "on" if is_on else "off")
# --- Sensor Handlers ---
def on_sensor_mode_change(self, choice):
self.publish("sensor/control/mode", int(choice))
def on_sensor_publish_all(self):
self.publish("sensor/control/publish_all", "1")
def on_sensor_enable_change(self, channel):
ui = self.sensor_ui[channel]; is_on = ui['switch_en'].get()
self.sensor_pending[channel] = True; ui['led_fb'].configure(text_color=COLOR_PENDING)
self.publish(f"sensor/control/enable/{channel}", "on" if is_on else "off")
def on_sensor_cal_start(self, channel, entry_widget):
val = entry_widget.get()
if val.isdigit():
self.publish(f"sensor/control/calibrate/start/{channel}", int(val))
def on_sensor_cal_finish(self, channel, entry_widget):
val = entry_widget.get()
if val.isdigit():
self.publish(f"sensor/control/calibrate/finish/{channel}", int(val))
def on_sensor_clear_cal(self, channel):
self.publish(f"sensor/control/clear_cal/{channel}", "1")
def run(self):
self.root.mainloop()
self.client.loop_stop()
self.client.disconnect()
if __name__ == "__main__":
app = MotorSCADA()
app.run()

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

View File

@@ -0,0 +1,2 @@
paho-mqtt
customtkinter

View File

@@ -1,176 +0,0 @@
# Real CV prototype — RealSense D415 + OpenCV
**Status:** WORKING_PROTOTYPE
**Production integrated:** NO (https://arhipovdan.ru does **not** consume this pipeline)
**Source:** consolidated from branch `drho1y-mvp_1` (`vision_classifier/`) into `cv/`
Same Track 3 B/C/D rules as the web twin; different input path (real depth camera vs simulated sensor).
## Purpose
Measure parcels on a conveyor with an Intel RealSense D415 (depth + color), estimate L×W×H and circularity, classify into zones **B / C / D**, optionally publish results over MQTT for hardware routing.
## Data flow
```
RealSense D415 (V4L2 depth + color)
→ OpenCV segmentation on depth (optional RGB flat detect)
→ measure L×W×H + circle_ratio
→ stabilize (median / vote → LOCK)
→ classify B/C/D
→ optional MQTT (category, dimensions, servo/motor topics)
```
## Entrypoints (start here)
| Command | Role |
|---|---|
| `./demo.sh` | Browser HUD demo on `:8080` (needs camera for live view) |
| `./run.sh --preview` | Live pipeline with JPEG preview frames |
| `./run.sh --once --no-mqtt --no-motor` | Single-shot / dry hardware |
| `.venv/bin/python test_classify.py` | Classifier unit checks **without camera** |
| `.venv/bin/python test_geometry.py` | Geometry helpers **without camera** |
Primary modules: `main.py` (live), `demo.py` (HUD), `classify.py` (rules), `measure.py` (depth metrics), `camera.py` (V4L2 RealSense).
## Classification rules (Track 3)
1. Dimensions must be strictly **> 10×10×10 mm** and **< 450×320×320 mm** → else **C**
2. Else if `circle_ratio ≥ 0.8`**D**
3. Else → **B**
Stabilization: median window + vote → **LOCK**. Uncertain cases fall back to zone **C** after N frames.
## File structure
```
cv/
main.py # live pipeline entry
demo.py / demo.sh # browser demo
run.sh # venv bootstrap + main.py
camera.py # RealSense via V4L2 + ffmpeg depth
measure.py # segmentation + dimensions
classify.py # B/C/D rules
stabilize.py # temporal LOCK
mqtt_bridge.py # optional MQTT (disabled by default)
calibrate.py # fx/fy + belt height calibration
align_color.py # RGB↔depth alignment helper
tracker.py # multi-object tracking assist
journal.py # decisions JSONL writer
demo_hud.py # HUD rendering
collect_log.py # log helper
test_classify.py # no-camera tests
test_geometry.py # no-camera tests
config.example.yaml # safe defaults (commit)
config.yaml # local only (gitignored)
requirements.txt
Dockerfile / docker-compose.yml
```
## Dependencies
**Software**
- Python **3.10+** (3.11 recommended; Docker image uses 3.11)
- `opencv-python-headless`, `numpy`, `PyYAML`, `pillow`, `paho-mqtt` — see `requirements.txt`
- System: **ffmpeg**, V4L2 (`v4l-utils` useful)
**Hardware (live mode)**
- Intel **RealSense D415** on USB3
- Linux host with `/dev/video*` depth+color nodes (Orange PI / x86)
`npm` / Node packages are **not** used here.
## Installation
```bash
cd cv
python3 -m venv .venv
source .venv/bin/activate
pip install -U pip
pip install -r requirements.txt
cp config.example.yaml config.yaml # optional; scripts auto-copy
```
Or simply:
```bash
cd cv
./demo.sh # creates .venv and config.yaml on first run
```
## Demo / tests without claiming live camera
Classifier and geometry (no RealSense required):
```bash
cd cv
python3 -m venv .venv && .venv/bin/pip install -r requirements.txt
.venv/bin/python test_classify.py
.venv/bin/python test_geometry.py
python3 -m compileall .
```
Live HUD (requires D415):
```bash
./demo.sh
# open http://127.0.0.1:8080/
```
Live pipeline:
```bash
./run.sh --preview --no-mqtt --no-motor
# or full hardware once MQTT/routing configured in local config.yaml:
./run.sh --preview
```
## Configuration
| File | Role |
|---|---|
| `config.example.yaml` | Committed safe defaults; **MQTT/motor/routing disabled** |
| `config.yaml` | Local overrides — **gitignored**; never commit credentials |
Optional MQTT (enable only locally):
```yaml
mqtt:
enabled: true
broker: "127.0.0.1"
port: 1883
user: "<your-user>"
password: "<your-password>"
```
CLI overrides: `--no-mqtt`, `--no-motor`, `--dry-route`, `--once`, `--preview`.
## Output schema (LOCK)
- Zone: `B` | `C` | `D`
- Dimensions mm: L×W×H
- `circle_ratio`
- Optional MQTT topics (when enabled): `vision/feedback/category`, `…/dimensions`, `…/circle_ratio`
- Optional JSONL: `logs/decisions.jsonl` (local, gitignored)
## Limitations
- Not connected to the web digital twin runtime.
- Requires calibrated intrinsics / belt height for accurate mm.
- Live demo needs a physical D415; CI hosts usually lack it.
- MQTT/servo/motor path is optional and site-specific.
## Troubleshooting
| Symptom | Check |
|---|---|
| No `/dev/video*` | USB3, `lsusb`, `v4l2-ctl --list-devices` |
| Depth empty | ffmpeg installed; correct `depth_device` |
| Wrong sizes | run `calibrate.py --length … --width …` |
| MQTT offline | expected when `mqtt.enabled: false` |
## Relation to web twin
Web (`src/domain/classifier.ts`) and CV (`classify.py`) implement the **same official bounds**. The public site uses a **digital sensor simulation**; this folder is the **hardware prototype** for future integration.

View File

@@ -1,90 +0,0 @@
# Vision classifier config EXAMPLE — copy to config.yaml and edit locally.
# Do not commit config.yaml (it may contain site-specific credentials).
camera:
depth_device: /dev/video0
color_device: /dev/video4
width: 640
height: 480
fps: 30
depth_scale_mm: 1.0
# D415 @ 640x480 approximate intrinsics — calibrate on your setup
fx: 564.0
fy: 564.0
cx: 320.0
cy: 240.0
color_dx: -40.0
color_dy: -8.0
color_scale: 1.030
belt_distance_mm: 594
belt_tolerance_mm: 25
min_object_height_mm: 20
min_object_area_px: 800
roi_margin:
top: 0.12
bottom: 0.02
left: 0.05
right: 0.12
max_objects_in_frame: 3
use_color: true
use_background_map: false
detect_flat_rgb: false
rgb_diff_threshold: 35
classification:
min_mm: [10, 10, 10]
max_mm: [450, 320, 320]
circle_ratio_threshold: 0.8
uncertain_after_frames: 45
uncertain_fallback_zone: C
# MQTT is OPTIONAL and disabled by default for safe local runs.
mqtt:
broker: "127.0.0.1"
port: 1883
user: ""
password: ""
client_id: "vision_classifier"
topic_result: "vision/feedback/category"
topic_dims: "vision/feedback/dimensions"
topic_circle: "vision/feedback/circle_ratio"
topic_debug: "vision/feedback/debug"
enabled: false
motor:
enabled: false
rpm: -200
current_percent: 50
microsteps: 16
stealthchop: true
disable_on_stop: false
routing:
enabled: false
zones:
B:
servo: 0
idle_angle: 0
divert_angle: 0
hold_ms: 500
C:
servo: 1
idle_angle: 0
divert_angle: 90
hold_ms: 800
D:
servo: 2
idle_angle: 0
divert_angle: 90
hold_ms: 800
cooldown_ms: 1500
runtime:
show_preview: false
save_debug_frames: false
debug_dir: "debug_frames"
decisions_log: "logs/decisions.jsonl"
preview_every_n: 3
process_every_n: 1
confirm_frames: 8

View File

@@ -1,6 +0,0 @@
services:
web:
build: .
ports:
- "127.0.0.1:3100:80"
restart: unless-stopped

View File

@@ -1,83 +0,0 @@
# Engineering notes (canonical)
Companion to `README.md` and `/documentation`. Not a stage changelog.
## Coordinate conventions
- World units: **1 unit = 1 meter**.
- Belt travel primarily along **+X**; belt top Y ≈ **0.70 m**.
- Lateral: **+Z** = physical LEFT (category C), **Z** = physical RIGHT (category D).
- Sorter CAD module origin X = `0`; camera module `2.01`; clean module `4.02`.
- Longitudinal plane S for diverter timing is world X along the sorter module.
## Canonical constants (code)
| Symbol | Value | File |
|---|---|---|
| `DIVERTER_LEFT_SIGNED_DEG` | 45 | `src/domain/pusherMotion.ts` |
| `DIVERTER_RIGHT_SIGNED_DEG` | +45 | same |
| `rotationDurationSec()` | 0.50 | same (45° / 90°/s) |
| `OPENING_SAFETY_MARGIN_SEC` | 0.15 | same |
| Contact / clear planes | ≈1.0538 / 1.6000 | `buildDiverterPlanes` + mount hinge |
| Classifier min/max | exclusive 10³ / 450×320×320 | `src/domain/classifier.ts` |
| Roundness | K > 0.8 | same |
| `CONVEYOR_CAD_URL` | `/models/sorter/conveyor-clean.glb` | `ConveyorCadModel.tsx` |
## Active source tree (runtime)
```
src/main.tsx
src/App.tsx
src/pages/{MainPage,DocumentationPage}.tsx
src/components/{AppNav,SorterScene,CVInspectionOverlay,BuildIdentityBadge}.tsx
src/components/ThreeD/* (active twin only)
src/domain/* (classifier, playback, layout, diverter, physics helpers)
src/data/{items,modelAssets,resolveItem,productionStatusSummary,demoPlaylist,scenarios}.ts
src/styles.css
```
## Asset provenance
| Role | Path |
|---|---|
| Author CAD | `3d_models/conveer.FCStd` |
| Runtime conveyor | `public/models/sorter/conveyor-clean.glb` |
| Products | `public/models/*.stl` from official STL ZIP |
| Classifier PDF | `official_sources/doc-1783095831.pdf` |
| Workspace / scoring PDFs | `input_info/doc-1783009942.pdf`, `doc-1783011400.pdf` |
## Physics roadmap (not completed)
1. Surface-velocity belt at 1 m/s with visual loop.
2. Contact-validated CAD diverter deflection for all playlist SKUs.
3. Calibrated per-SKU mass, COM, friction, damping.
4. Receiver capture verification under dynamic drops.
CCD for light/thin items exists in runtime/sim; that alone is **not** full contact validation.
## Compliance evidence rules
- Prefer present official files under `input_info/` and `official_sources/doc-1783095831.pdf`.
- Missing: `input_info/extracted/Постановка_Задача_3_сжато_2.pdf` — never claim it is available.
- Internal engineering criteria are not automatic Ozon pass/fail.
## Соответствие подтверждённым требованиям
Grounded only in present sources + current `main` code/tests (not a full official scorecard):
| Area | Official source present | Current implementation | Evidence | Status |
|---|---|---|---|---|
| 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) |
| CAD conveyor | author pack / STL references | `3d_models/`, `conveyor-clean.glb` | checksums in README | 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) |
| 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) |
## Real CV prototype
Path `cv/` — OpenCV + RealSense D415 depth pipeline. Same B/C/D domain as the web twin. **Not** consumed by https://arhipovdan.ru. Details: `cv/README.md`.
## Layout drawing
`docs/engineering/work-area-layout-source.png` — workspace layout provenance image retained for engineering reference.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 178 KiB

View File

@@ -1,55 +0,0 @@
/** Shared e2e helpers — Stage 2B autostart means / opens already running. */
import { expect, type Page } from '@playwright/test';
/** Close the Demo Complete overlay if it is covering the controls. */
export async function dismissFinished(page: Page) {
const finished = page.getByTestId('demo-finished');
if (await finished.isVisible().catch(() => false)) {
await finished.getByRole('button', { name: /Replay/i }).click({ force: true });
await expect(finished).toHaveCount(0, { timeout: 10_000 });
}
}
/** Ensure playback is running (noop if already on Pause button). */
export async function ensureRunning(page: Page) {
await dismissFinished(page);
const pause = page.getByTestId('demo-pause');
if (await pause.isVisible().catch(() => false)) return;
const play = page.getByTestId('demo-play');
await expect(play.or(pause)).toBeVisible({ timeout: 15_000 });
if (await play.isVisible().catch(() => false)) {
await play.click({ force: true });
}
await expect(page.getByTestId('demo-pause')).toBeVisible({ timeout: 10_000 });
}
export async function ensurePaused(page: Page) {
await dismissFinished(page);
const play = page.getByTestId('demo-play');
if (await play.isVisible().catch(() => false)) return;
await page.getByTestId('demo-pause').click({ force: true });
await expect(page.getByTestId('demo-play')).toBeVisible({ timeout: 10_000 });
}
/** Assert either Play or Pause control is present (autostart-safe). */
export async function expectPlaybackControl(page: Page) {
await expect(
page.getByTestId('demo-play').or(page.getByTestId('demo-pause')),
).toBeVisible({ timeout: 15_000 });
}
/**
* Open debug demo, pause, then seek to a case WHILE paused (seek preserves
* paused status — unlike seek from idle which auto-starts).
*/
export async function openPausedCase(page: Page, caseIndex: number, speed: '0.5' | '1' | '1.5' | '2' = '1') {
await page.goto('/?debug=1');
await expect(page.locator('canvas')).toBeVisible({ timeout: 60_000 });
await expect(page.getByTestId('demo-hud')).toBeVisible();
await ensurePaused(page);
await page.getByTestId(`demo-speed-${speed}`).click();
await page.getByTestId(`demo-case-${caseIndex}`).click();
await dismissFinished(page);
await ensurePaused(page);
await expect(page.getByTestId('demo-case-label')).toHaveText(`${caseIndex + 1}/12`);
}

View File

@@ -1,33 +0,0 @@
import { test, expect } from '@playwright/test';
test.describe('routes', () => {
test('two-page UI: simulation, documentation, unknown redirect', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveURL(/\/$/);
await expect(page.getByTestId('app-nav').first()).toBeVisible({ timeout: 30_000 });
await expect(page.getByTestId('nav-simulation').first()).toHaveClass(/active/);
await page.getByTestId('nav-documentation').first().click();
await expect(page).toHaveURL(/\/documentation\/?$/);
await expect(page.getByTestId('documentation-page')).toBeVisible();
await expect(page.getByTestId('docs-production-status')).toContainText('DATA_ACQUISITION_PACK_READY');
await expect(page.getByTestId('nav-documentation').first()).toHaveClass(/active/);
await page.reload();
await expect(page).toHaveURL(/\/documentation\/?$/);
await expect(page.getByTestId('documentation-page')).toBeVisible();
await page.getByTestId('nav-simulation').first().click();
await expect(page).toHaveURL(/\/$/);
await expect(page.getByTestId('demo-hud')).toBeVisible({ timeout: 60_000 });
await page.goto('/details');
await expect(page).toHaveURL(/\/$/);
await page.goto('/device-test');
await expect(page).toHaveURL(/\/$/);
await page.goto('/old-route');
await expect(page).toHaveURL(/\/$/);
});
});

View File

@@ -1,31 +0,0 @@
import { test, expect } from '@playwright/test';
import { ensureRunning, expectPlaybackControl } from './helpers';
test.describe('smoke', () => {
test('home opens, canvas loads, play works', async ({ page }) => {
const pageErrors: Error[] = [];
page.on('pageerror', (err) => pageErrors.push(err));
await page.goto('/');
const loading = page.locator('.three-loading');
const canvas = page.locator('canvas');
await expect(loading.or(canvas).first()).toBeVisible({ timeout: 30_000 });
await expect(canvas).toBeVisible({ timeout: 60_000 });
await expect(page.getByTestId('demo-hud')).toBeVisible();
// Stage 2B: demo autostarts — Pause is present immediately; Play after pause.
await expectPlaybackControl(page);
await ensureRunning(page);
await expect(page.getByTestId('demo-status')).not.toHaveText('FINISHED');
const finished = page.getByTestId('demo-finished');
if (await finished.isVisible().catch(() => false)) {
await page.getByTestId('demo-play').click();
await expect(finished).toBeHidden({ timeout: 10_000 });
await expect(page.getByTestId('demo-pause')).toBeVisible({ timeout: 10_000 });
}
expect(pageErrors, `pageerrors: ${pageErrors.map((e) => e.message).join('; ')}`).toEqual([]);
});
});

View File

@@ -1,14 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#070b12" />
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect width='16' height='16' rx='3' fill='%230b1220'/%3E%3Crect x='3' y='7' width='10' height='2' fill='%233b82f6'/%3E%3C/svg%3E" />
<title>OZON Tech Sorter Simulation</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

2
kicad/ozon/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
*.lck
.history

1
kicad/ozon/.history Submodule

Submodule kicad/ozon/.history added at 2a8a462227

View File

@@ -0,0 +1,2 @@
(kicad_pcb (version 20260206) (generator "pcbnew") (generator_version "10.0")
)

105
kicad/ozon/ozon.kicad_prl Normal file
View File

@@ -0,0 +1,105 @@
{
"board": {
"active_layer": 0,
"active_layer_preset": "",
"auto_track_width": true,
"hidden_netclasses": [],
"hidden_nets": [],
"high_contrast_mode": 0,
"net_color_mode": 1,
"opacity": {
"images": 0.6,
"pads": 1.0,
"shapes": 1.0,
"tracks": 1.0,
"vias": 1.0,
"zones": 0.6
},
"prototype_zone_fills": false,
"selection_filter": {
"dimensions": true,
"footprints": true,
"graphics": true,
"keepouts": true,
"lockedItems": false,
"otherItems": true,
"pads": true,
"text": true,
"tracks": true,
"vias": true,
"zones": true
},
"visible_items": [
"vias",
"footprint_text",
"footprint_anchors",
"ratsnest",
"grid",
"footprints_front",
"footprints_back",
"footprint_values",
"footprint_references",
"tracks",
"drc_errors",
"drawing_sheet",
"bitmaps",
"pads",
"zones",
"drc_warnings",
"drc_exclusions",
"locked_item_shadows",
"conflict_shadows",
"shapes",
"board_outline_area",
"ly_points"
],
"visible_layers": "ffffffff_ffffffff_ffffffff_ffffffff",
"zone_display_mode": 0
},
"git": {
"integration_disabled": false,
"repo_type": "",
"repo_username": "",
"ssh_key": ""
},
"meta": {
"filename": "ozon.kicad_prl",
"version": 5
},
"net_inspector_panel": {
"col_hidden": [],
"col_order": [],
"col_widths": [],
"custom_group_rules": [],
"expanded_rows": [],
"filter_by_net_name": true,
"filter_by_netclass": true,
"filter_text": "",
"group_by_constraint": false,
"group_by_netclass": false,
"show_time_domain_details": false,
"show_unconnected_nets": false,
"show_zero_pad_nets": false,
"sort_ascending": true,
"sorting_column": -1
},
"open_jobsets": [],
"project": {
"files": []
},
"schematic": {
"hierarchy_collapsed": [],
"selection_filter": {
"graphics": true,
"images": true,
"labels": true,
"lockedItems": false,
"otherItems": true,
"pins": true,
"ruleAreas": true,
"symbols": true,
"text": true,
"wires": true
}
}
}

435
kicad/ozon/ozon.kicad_pro Normal file
View File

@@ -0,0 +1,435 @@
{
"board": {
"3dviewports": [],
"ipc2581": {
"bom_rev": "",
"dist": "",
"distpn": "",
"internal_id": "",
"mfg": "",
"mpn": "",
"sch_revision": ""
},
"layer_pairs": [],
"layer_presets": [],
"viewports": []
},
"boards": [],
"component_class_settings": {
"assignments": [],
"meta": {
"version": 0
},
"sheet_component_classes": {
"enabled": false
}
},
"cvpcb": {
"equivalence_files": []
},
"erc": {
"erc_exclusions": [],
"meta": {
"version": 0
},
"pin_map": [
[
0,
0,
0,
0,
0,
0,
1,
0,
0,
0,
0,
2
],
[
0,
2,
0,
1,
0,
0,
1,
0,
2,
2,
2,
2
],
[
0,
0,
0,
0,
0,
0,
1,
0,
1,
0,
1,
2
],
[
0,
1,
0,
0,
0,
0,
1,
1,
2,
1,
1,
2
],
[
0,
0,
0,
0,
0,
0,
1,
0,
0,
0,
0,
2
],
[
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
2
],
[
1,
1,
1,
1,
1,
0,
1,
1,
1,
1,
1,
2
],
[
0,
0,
0,
1,
0,
0,
1,
0,
0,
0,
0,
2
],
[
0,
2,
1,
2,
0,
0,
1,
0,
2,
2,
2,
2
],
[
0,
2,
0,
1,
0,
0,
1,
0,
2,
0,
0,
2
],
[
0,
2,
1,
1,
0,
0,
1,
0,
2,
0,
0,
2
],
[
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2
]
],
"rule_severities": {
"bus_definition_conflict": "error",
"bus_entry_needed": "error",
"bus_to_bus_conflict": "error",
"bus_to_net_conflict": "error",
"different_unit_footprint": "error",
"different_unit_net": "error",
"duplicate_reference": "error",
"duplicate_sheet_names": "error",
"endpoint_off_grid": "warning",
"extra_units": "error",
"field_name_whitespace": "warning",
"footprint_filter": "ignore",
"footprint_link_issues": "warning",
"four_way_junction": "ignore",
"ground_pin_not_ground": "warning",
"hier_label_mismatch": "error",
"isolated_pin_label": "warning",
"label_dangling": "error",
"label_multiple_wires": "warning",
"lib_symbol_issues": "warning",
"lib_symbol_mismatch": "warning",
"missing_bidi_pin": "warning",
"missing_input_pin": "warning",
"missing_power_pin": "error",
"missing_unit": "warning",
"multiple_net_names": "warning",
"net_not_bus_member": "warning",
"no_connect_connected": "warning",
"no_connect_dangling": "warning",
"pin_not_connected": "error",
"pin_not_driven": "error",
"pin_to_pin": "warning",
"power_pin_not_driven": "error",
"same_local_global_label": "warning",
"similar_label_and_power": "warning",
"similar_labels": "warning",
"similar_power": "warning",
"simulation_model_issue": "ignore",
"single_global_label": "ignore",
"stacked_pin_name": "warning",
"unannotated": "error",
"unconnected_wire_endpoint": "warning",
"undefined_netclass": "error",
"unit_value_mismatch": "error",
"unresolved_variable": "error",
"wire_dangling": "error"
}
},
"libraries": {
"pinned_footprint_libs": [],
"pinned_symbol_libs": []
},
"meta": {
"filename": "ozon.kicad_pro",
"version": 3
},
"net_settings": {
"classes": [
{
"bus_width": 12,
"clearance": 0.2,
"diff_pair_gap": 0.25,
"diff_pair_via_gap": 0.25,
"diff_pair_width": 0.2,
"line_style": 0,
"microvia_diameter": 0.3,
"microvia_drill": 0.1,
"name": "Default",
"pcb_color": "rgba(0, 0, 0, 0.000)",
"priority": 2147483647,
"schematic_color": "rgba(0, 0, 0, 0.000)",
"track_width": 0.2,
"tuning_profile": "",
"via_diameter": 0.6,
"via_drill": 0.3,
"wire_width": 6
}
],
"meta": {
"version": 5
},
"net_colors": null,
"netclass_assignments": null,
"netclass_patterns": []
},
"pcbnew": {
"last_paths": {
"idf": "",
"netlist": "",
"plot": "",
"specctra_dsn": "",
"vrml": ""
},
"page_layout_descr_file": ""
},
"schematic": {
"annotate_start_num": 0,
"annotation": {
"method": 0,
"sort_order": 0
},
"bom_export_filename": "${PROJECTNAME}.csv",
"bom_fmt_presets": [],
"bom_fmt_settings": {
"field_delimiter": ",",
"keep_line_breaks": false,
"keep_tabs": false,
"name": "CSV",
"ref_delimiter": ",",
"ref_range_delimiter": "",
"string_delimiter": "\""
},
"bom_presets": [],
"bom_settings": {
"exclude_dnp": false,
"fields_ordered": [
{
"group_by": false,
"label": "Reference",
"name": "Reference",
"show": true
},
{
"group_by": false,
"label": "Qty",
"name": "${QUANTITY}",
"show": true
},
{
"group_by": true,
"label": "Value",
"name": "Value",
"show": true
},
{
"group_by": true,
"label": "DNP",
"name": "${DNP}",
"show": true
},
{
"group_by": true,
"label": "Exclude from BOM",
"name": "${EXCLUDE_FROM_BOM}",
"show": true
},
{
"group_by": true,
"label": "Exclude from Board",
"name": "${EXCLUDE_FROM_BOARD}",
"show": true
},
{
"group_by": true,
"label": "Footprint",
"name": "Footprint",
"show": true
},
{
"group_by": false,
"label": "Datasheet",
"name": "Datasheet",
"show": true
}
],
"filter_string": "",
"group_symbols": true,
"include_excluded_from_bom": true,
"name": "Default Editing",
"sort_asc": true,
"sort_field": "Обозначение"
},
"bus_aliases": {},
"connection_grid_size": 50.0,
"drawing": {
"dashed_lines_dash_length_ratio": 12.0,
"dashed_lines_gap_length_ratio": 3.0,
"default_line_thickness": 6.0,
"default_text_size": 50.0,
"field_names": [],
"hop_over_size_choice": 0,
"intersheets_ref_own_page": false,
"intersheets_ref_prefix": "",
"intersheets_ref_short": false,
"intersheets_ref_show": false,
"intersheets_ref_suffix": "",
"junction_size_choice": 3,
"label_size_ratio": 0.375,
"operating_point_overlay_i_precision": 3,
"operating_point_overlay_i_range": "~A",
"operating_point_overlay_v_precision": 3,
"operating_point_overlay_v_range": "~V",
"overbar_offset_ratio": 1.23,
"pin_symbol_size": 25.0,
"text_offset_ratio": 0.15
},
"legacy_lib_dir": "",
"legacy_lib_list": [],
"meta": {
"version": 1
},
"page_layout_descr_file": "",
"plot_directory": "",
"reuse_designators": true,
"subpart_first_id": 65,
"subpart_id_separator": 0,
"top_level_sheets": [
{
"filename": "ozon.kicad_sch",
"name": "Корневой лист",
"uuid": "8eb30ef0-8ef0-4108-b9d0-4c3ae489af42"
}
],
"used_designators": "R1,M1-5,#PWR1-10,U1-6",
"variants": []
},
"sheets": [
[
"8eb30ef0-8ef0-4108-b9d0-4c3ae489af42",
"Корневой лист"
]
],
"text_variables": {},
"tuning_profiles": {
"meta": {
"version": 0
},
"tuning_profiles_impedance_geometric": []
}
}

6460
kicad/ozon/ozon.kicad_sch Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,56 +0,0 @@
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# Keep default MIME map (html/js/css). Do NOT put a server-level `types {}`
# here — it replaces mime.types and forces downloads (octet-stream + nosniff).
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Security headers (Stage 2B §24.3)
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
# gzip for text payloads (GLB/STL are already compressed/binary)
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_comp_level 6;
gzip_types text/plain text/css application/json application/javascript text/javascript image/svg+xml;
location = /version.json {
add_header Cache-Control "no-store, no-cache, must-revalidate";
add_header X-Content-Type-Options "nosniff" always;
try_files $uri =404;
}
location / {
try_files $uri $uri/ /index.html;
}
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# 3D models only — scoped types override (does not wipe html/js MIME)
location /models/ {
types {
model/gltf-binary glb;
model/stl stl;
application/octet-stream bin;
}
default_type application/octet-stream;
expires 1y;
add_header Cache-Control "public, immutable";
}
location /draco/ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}

File diff suppressed because one or more lines are too long

2227
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,37 +0,0 @@
{
"name": "ozon-tech-sorter-simulation",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite --host 127.0.0.1 --port 3100",
"build": "tsc -b && vite build",
"preview": "vite preview --host 127.0.0.1 --port 3100",
"test": "vitest run --config vitest.config.ts",
"test:e2e": "playwright test",
"test:e2e:headed": "playwright test --headed"
},
"dependencies": {
"@dimforge/rapier3d-compat": "^0.19.3",
"@react-three/drei": "^10.7.7",
"@react-three/fiber": "^9.6.1",
"@react-three/postprocessing": "^3.0.4",
"@react-three/rapier": "^2.2.0",
"@vitejs/plugin-react": "latest",
"postprocessing": "^6.39.4",
"react": "latest",
"react-dom": "latest",
"react-router-dom": "^7.18.1",
"three": "^0.185.1",
"typescript": "latest",
"vite": "latest"
},
"devDependencies": {
"@playwright/test": "^1.61.1",
"@types/node": "^26.1.0",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"@types/three": "^0.185.0",
"vitest": "^4.1.9"
}
}

View File

@@ -1,42 +0,0 @@
import { defineConfig, devices } from '@playwright/test';
const baseURL = process.env.PLAYWRIGHT_BASE_URL ?? 'http://127.0.0.1:3101';
const startServer = process.env.PLAYWRIGHT_START_SERVER === '1';
export default defineConfig({
testDir: './e2e',
fullyParallel: false,
workers: 1,
retries: 1,
forbidOnly: !!process.env.CI,
reporter: [['list'], ['html', { open: 'never' }]],
use: {
baseURL,
trace: 'on-first-retry',
screenshot: 'only-on-failure',
viewport: { width: 1280, height: 720 },
},
expect: {
toHaveScreenshot: {
// Soft thresholds — WebGL/fonts can vary slightly across environments
threshold: 0.35,
maxDiffPixelRatio: 0.08,
},
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
// Keep visual/e2e deterministic on one worker (CI + local)
// Production smoke is excluded via package.json --grep-invert @production
webServer: startServer
? {
command: 'npm run preview -- --host 127.0.0.1 --port 3101',
url: baseURL,
reuseExistingServer: !process.env.CI,
timeout: 120_000,
}
: undefined,
});

View File

@@ -1,31 +0,0 @@
# Draco 3D Data Compression
Draco is an open-source library for compressing and decompressing 3D geometric meshes and point clouds. It is intended to improve the storage and transmission of 3D graphics.
[Website](https://google.github.io/draco/) | [GitHub](https://github.com/google/draco)
## Contents
This folder contains three utilities:
* `draco_decoder.js` — Emscripten-compiled decoder, compatible with any modern browser.
* `draco_decoder.wasm` — WebAssembly decoder, compatible with newer browsers and devices.
* `draco_wasm_wrapper.js` — JavaScript wrapper for the WASM decoder.
Each file is provided in two variations:
* **Default:** Latest stable builds, tracking the project's [master branch](https://github.com/google/draco).
* **glTF:** Builds targeted by the [glTF mesh compression extension](https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_draco_mesh_compression), tracking the [corresponding Draco branch](https://github.com/google/draco/tree/gltf_2.0_draco_extension).
Either variation may be used with `DRACOLoader`:
```js
var dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath('path/to/decoders/');
```
Further [documentation on GitHub](https://github.com/google/draco/tree/master/javascript/example#static-loading-javascript-decoder).
## License
[Apache License 2.0](https://github.com/google/draco/blob/master/LICENSE)

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@@ -1,117 +0,0 @@
var $jscomp=$jscomp||{};$jscomp.scope={};$jscomp.arrayIteratorImpl=function(k){var n=0;return function(){return n<k.length?{done:!1,value:k[n++]}:{done:!0}}};$jscomp.arrayIterator=function(k){return{next:$jscomp.arrayIteratorImpl(k)}};$jscomp.makeIterator=function(k){var n="undefined"!=typeof Symbol&&Symbol.iterator&&k[Symbol.iterator];return n?n.call(k):$jscomp.arrayIterator(k)};$jscomp.ASSUME_ES5=!1;$jscomp.ASSUME_NO_NATIVE_MAP=!1;$jscomp.ASSUME_NO_NATIVE_SET=!1;$jscomp.SIMPLE_FROUND_POLYFILL=!1;
$jscomp.ISOLATE_POLYFILLS=!1;$jscomp.FORCE_POLYFILL_PROMISE=!1;$jscomp.FORCE_POLYFILL_PROMISE_WHEN_NO_UNHANDLED_REJECTION=!1;$jscomp.getGlobal=function(k){k=["object"==typeof globalThis&&globalThis,k,"object"==typeof window&&window,"object"==typeof self&&self,"object"==typeof global&&global];for(var n=0;n<k.length;++n){var l=k[n];if(l&&l.Math==Math)return l}throw Error("Cannot find global object");};$jscomp.global=$jscomp.getGlobal(this);
$jscomp.defineProperty=$jscomp.ASSUME_ES5||"function"==typeof Object.defineProperties?Object.defineProperty:function(k,n,l){if(k==Array.prototype||k==Object.prototype)return k;k[n]=l.value;return k};$jscomp.IS_SYMBOL_NATIVE="function"===typeof Symbol&&"symbol"===typeof Symbol("x");$jscomp.TRUST_ES6_POLYFILLS=!$jscomp.ISOLATE_POLYFILLS||$jscomp.IS_SYMBOL_NATIVE;$jscomp.polyfills={};$jscomp.propertyToPolyfillSymbol={};$jscomp.POLYFILL_PREFIX="$jscp$";
var $jscomp$lookupPolyfilledValue=function(k,n){var l=$jscomp.propertyToPolyfillSymbol[n];if(null==l)return k[n];l=k[l];return void 0!==l?l:k[n]};$jscomp.polyfill=function(k,n,l,p){n&&($jscomp.ISOLATE_POLYFILLS?$jscomp.polyfillIsolated(k,n,l,p):$jscomp.polyfillUnisolated(k,n,l,p))};
$jscomp.polyfillUnisolated=function(k,n,l,p){l=$jscomp.global;k=k.split(".");for(p=0;p<k.length-1;p++){var h=k[p];if(!(h in l))return;l=l[h]}k=k[k.length-1];p=l[k];n=n(p);n!=p&&null!=n&&$jscomp.defineProperty(l,k,{configurable:!0,writable:!0,value:n})};
$jscomp.polyfillIsolated=function(k,n,l,p){var h=k.split(".");k=1===h.length;p=h[0];p=!k&&p in $jscomp.polyfills?$jscomp.polyfills:$jscomp.global;for(var A=0;A<h.length-1;A++){var f=h[A];if(!(f in p))return;p=p[f]}h=h[h.length-1];l=$jscomp.IS_SYMBOL_NATIVE&&"es6"===l?p[h]:null;n=n(l);null!=n&&(k?$jscomp.defineProperty($jscomp.polyfills,h,{configurable:!0,writable:!0,value:n}):n!==l&&(void 0===$jscomp.propertyToPolyfillSymbol[h]&&(l=1E9*Math.random()>>>0,$jscomp.propertyToPolyfillSymbol[h]=$jscomp.IS_SYMBOL_NATIVE?
$jscomp.global.Symbol(h):$jscomp.POLYFILL_PREFIX+l+"$"+h),$jscomp.defineProperty(p,$jscomp.propertyToPolyfillSymbol[h],{configurable:!0,writable:!0,value:n})))};
$jscomp.polyfill("Promise",function(k){function n(){this.batch_=null}function l(f){return f instanceof h?f:new h(function(q,v){q(f)})}if(k&&(!($jscomp.FORCE_POLYFILL_PROMISE||$jscomp.FORCE_POLYFILL_PROMISE_WHEN_NO_UNHANDLED_REJECTION&&"undefined"===typeof $jscomp.global.PromiseRejectionEvent)||!$jscomp.global.Promise||-1===$jscomp.global.Promise.toString().indexOf("[native code]")))return k;n.prototype.asyncExecute=function(f){if(null==this.batch_){this.batch_=[];var q=this;this.asyncExecuteFunction(function(){q.executeBatch_()})}this.batch_.push(f)};
var p=$jscomp.global.setTimeout;n.prototype.asyncExecuteFunction=function(f){p(f,0)};n.prototype.executeBatch_=function(){for(;this.batch_&&this.batch_.length;){var f=this.batch_;this.batch_=[];for(var q=0;q<f.length;++q){var v=f[q];f[q]=null;try{v()}catch(z){this.asyncThrow_(z)}}}this.batch_=null};n.prototype.asyncThrow_=function(f){this.asyncExecuteFunction(function(){throw f;})};var h=function(f){this.state_=0;this.result_=void 0;this.onSettledCallbacks_=[];this.isRejectionHandled_=!1;var q=this.createResolveAndReject_();
try{f(q.resolve,q.reject)}catch(v){q.reject(v)}};h.prototype.createResolveAndReject_=function(){function f(z){return function(O){v||(v=!0,z.call(q,O))}}var q=this,v=!1;return{resolve:f(this.resolveTo_),reject:f(this.reject_)}};h.prototype.resolveTo_=function(f){if(f===this)this.reject_(new TypeError("A Promise cannot resolve to itself"));else if(f instanceof h)this.settleSameAsPromise_(f);else{a:switch(typeof f){case "object":var q=null!=f;break a;case "function":q=!0;break a;default:q=!1}q?this.resolveToNonPromiseObj_(f):
this.fulfill_(f)}};h.prototype.resolveToNonPromiseObj_=function(f){var q=void 0;try{q=f.then}catch(v){this.reject_(v);return}"function"==typeof q?this.settleSameAsThenable_(q,f):this.fulfill_(f)};h.prototype.reject_=function(f){this.settle_(2,f)};h.prototype.fulfill_=function(f){this.settle_(1,f)};h.prototype.settle_=function(f,q){if(0!=this.state_)throw Error("Cannot settle("+f+", "+q+"): Promise already settled in state"+this.state_);this.state_=f;this.result_=q;2===this.state_&&this.scheduleUnhandledRejectionCheck_();
this.executeOnSettledCallbacks_()};h.prototype.scheduleUnhandledRejectionCheck_=function(){var f=this;p(function(){if(f.notifyUnhandledRejection_()){var q=$jscomp.global.console;"undefined"!==typeof q&&q.error(f.result_)}},1)};h.prototype.notifyUnhandledRejection_=function(){if(this.isRejectionHandled_)return!1;var f=$jscomp.global.CustomEvent,q=$jscomp.global.Event,v=$jscomp.global.dispatchEvent;if("undefined"===typeof v)return!0;"function"===typeof f?f=new f("unhandledrejection",{cancelable:!0}):
"function"===typeof q?f=new q("unhandledrejection",{cancelable:!0}):(f=$jscomp.global.document.createEvent("CustomEvent"),f.initCustomEvent("unhandledrejection",!1,!0,f));f.promise=this;f.reason=this.result_;return v(f)};h.prototype.executeOnSettledCallbacks_=function(){if(null!=this.onSettledCallbacks_){for(var f=0;f<this.onSettledCallbacks_.length;++f)A.asyncExecute(this.onSettledCallbacks_[f]);this.onSettledCallbacks_=null}};var A=new n;h.prototype.settleSameAsPromise_=function(f){var q=this.createResolveAndReject_();
f.callWhenSettled_(q.resolve,q.reject)};h.prototype.settleSameAsThenable_=function(f,q){var v=this.createResolveAndReject_();try{f.call(q,v.resolve,v.reject)}catch(z){v.reject(z)}};h.prototype.then=function(f,q){function v(t,x){return"function"==typeof t?function(D){try{z(t(D))}catch(R){O(R)}}:x}var z,O,ba=new h(function(t,x){z=t;O=x});this.callWhenSettled_(v(f,z),v(q,O));return ba};h.prototype.catch=function(f){return this.then(void 0,f)};h.prototype.callWhenSettled_=function(f,q){function v(){switch(z.state_){case 1:f(z.result_);
break;case 2:q(z.result_);break;default:throw Error("Unexpected state: "+z.state_);}}var z=this;null==this.onSettledCallbacks_?A.asyncExecute(v):this.onSettledCallbacks_.push(v);this.isRejectionHandled_=!0};h.resolve=l;h.reject=function(f){return new h(function(q,v){v(f)})};h.race=function(f){return new h(function(q,v){for(var z=$jscomp.makeIterator(f),O=z.next();!O.done;O=z.next())l(O.value).callWhenSettled_(q,v)})};h.all=function(f){var q=$jscomp.makeIterator(f),v=q.next();return v.done?l([]):new h(function(z,
O){function ba(D){return function(R){t[D]=R;x--;0==x&&z(t)}}var t=[],x=0;do t.push(void 0),x++,l(v.value).callWhenSettled_(ba(t.length-1),O),v=q.next();while(!v.done)})};return h},"es6","es3");$jscomp.owns=function(k,n){return Object.prototype.hasOwnProperty.call(k,n)};$jscomp.assign=$jscomp.TRUST_ES6_POLYFILLS&&"function"==typeof Object.assign?Object.assign:function(k,n){for(var l=1;l<arguments.length;l++){var p=arguments[l];if(p)for(var h in p)$jscomp.owns(p,h)&&(k[h]=p[h])}return k};
$jscomp.polyfill("Object.assign",function(k){return k||$jscomp.assign},"es6","es3");$jscomp.checkStringArgs=function(k,n,l){if(null==k)throw new TypeError("The 'this' value for String.prototype."+l+" must not be null or undefined");if(n instanceof RegExp)throw new TypeError("First argument to String.prototype."+l+" must not be a regular expression");return k+""};
$jscomp.polyfill("String.prototype.startsWith",function(k){return k?k:function(n,l){var p=$jscomp.checkStringArgs(this,n,"startsWith");n+="";var h=p.length,A=n.length;l=Math.max(0,Math.min(l|0,p.length));for(var f=0;f<A&&l<h;)if(p[l++]!=n[f++])return!1;return f>=A}},"es6","es3");
$jscomp.polyfill("Array.prototype.copyWithin",function(k){function n(l){l=Number(l);return Infinity===l||-Infinity===l?l:l|0}return k?k:function(l,p,h){var A=this.length;l=n(l);p=n(p);h=void 0===h?A:n(h);l=0>l?Math.max(A+l,0):Math.min(l,A);p=0>p?Math.max(A+p,0):Math.min(p,A);h=0>h?Math.max(A+h,0):Math.min(h,A);if(l<p)for(;p<h;)p in this?this[l++]=this[p++]:(delete this[l++],p++);else for(h=Math.min(h,A+p-l),l+=h-p;h>p;)--h in this?this[--l]=this[h]:delete this[--l];return this}},"es6","es3");
$jscomp.typedArrayCopyWithin=function(k){return k?k:Array.prototype.copyWithin};$jscomp.polyfill("Int8Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Uint8Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Uint8ClampedArray.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Int16Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");
$jscomp.polyfill("Uint16Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Int32Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Uint32Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Float32Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Float64Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");
var DracoDecoderModule=function(){var k="undefined"!==typeof document&&document.currentScript?document.currentScript.src:void 0;"undefined"!==typeof __filename&&(k=k||__filename);return function(n){function l(e){return a.locateFile?a.locateFile(e,U):U+e}function p(e,b,c){var d=b+c;for(c=b;e[c]&&!(c>=d);)++c;if(16<c-b&&e.buffer&&va)return va.decode(e.subarray(b,c));for(d="";b<c;){var g=e[b++];if(g&128){var u=e[b++]&63;if(192==(g&224))d+=String.fromCharCode((g&31)<<6|u);else{var X=e[b++]&63;g=224==
(g&240)?(g&15)<<12|u<<6|X:(g&7)<<18|u<<12|X<<6|e[b++]&63;65536>g?d+=String.fromCharCode(g):(g-=65536,d+=String.fromCharCode(55296|g>>10,56320|g&1023))}}else d+=String.fromCharCode(g)}return d}function h(e,b){return e?p(ea,e,b):""}function A(){var e=ja.buffer;a.HEAP8=Y=new Int8Array(e);a.HEAP16=new Int16Array(e);a.HEAP32=ca=new Int32Array(e);a.HEAPU8=ea=new Uint8Array(e);a.HEAPU16=new Uint16Array(e);a.HEAPU32=V=new Uint32Array(e);a.HEAPF32=new Float32Array(e);a.HEAPF64=new Float64Array(e)}function f(e){if(a.onAbort)a.onAbort(e);
e="Aborted("+e+")";da(e);wa=!0;e=new WebAssembly.RuntimeError(e+". Build with -sASSERTIONS for more info.");ka(e);throw e;}function q(e){try{if(e==P&&fa)return new Uint8Array(fa);if(ma)return ma(e);throw"both async and sync fetching of the wasm failed";}catch(b){f(b)}}function v(){if(!fa&&(xa||ha)){if("function"==typeof fetch&&!P.startsWith("file://"))return fetch(P,{credentials:"same-origin"}).then(function(e){if(!e.ok)throw"failed to load wasm binary file at '"+P+"'";return e.arrayBuffer()}).catch(function(){return q(P)});
if(na)return new Promise(function(e,b){na(P,function(c){e(new Uint8Array(c))},b)})}return Promise.resolve().then(function(){return q(P)})}function z(e){for(;0<e.length;)e.shift()(a)}function O(e){this.excPtr=e;this.ptr=e-24;this.set_type=function(b){V[this.ptr+4>>2]=b};this.get_type=function(){return V[this.ptr+4>>2]};this.set_destructor=function(b){V[this.ptr+8>>2]=b};this.get_destructor=function(){return V[this.ptr+8>>2]};this.set_refcount=function(b){ca[this.ptr>>2]=b};this.set_caught=function(b){Y[this.ptr+
12>>0]=b?1:0};this.get_caught=function(){return 0!=Y[this.ptr+12>>0]};this.set_rethrown=function(b){Y[this.ptr+13>>0]=b?1:0};this.get_rethrown=function(){return 0!=Y[this.ptr+13>>0]};this.init=function(b,c){this.set_adjusted_ptr(0);this.set_type(b);this.set_destructor(c);this.set_refcount(0);this.set_caught(!1);this.set_rethrown(!1)};this.add_ref=function(){ca[this.ptr>>2]+=1};this.release_ref=function(){var b=ca[this.ptr>>2];ca[this.ptr>>2]=b-1;return 1===b};this.set_adjusted_ptr=function(b){V[this.ptr+
16>>2]=b};this.get_adjusted_ptr=function(){return V[this.ptr+16>>2]};this.get_exception_ptr=function(){if(ya(this.get_type()))return V[this.excPtr>>2];var b=this.get_adjusted_ptr();return 0!==b?b:this.excPtr}}function ba(){function e(){if(!la&&(la=!0,a.calledRun=!0,!wa)){za=!0;z(oa);Aa(a);if(a.onRuntimeInitialized)a.onRuntimeInitialized();if(a.postRun)for("function"==typeof a.postRun&&(a.postRun=[a.postRun]);a.postRun.length;)Ba.unshift(a.postRun.shift());z(Ba)}}if(!(0<aa)){if(a.preRun)for("function"==
typeof a.preRun&&(a.preRun=[a.preRun]);a.preRun.length;)Ca.unshift(a.preRun.shift());z(Ca);0<aa||(a.setStatus?(a.setStatus("Running..."),setTimeout(function(){setTimeout(function(){a.setStatus("")},1);e()},1)):e())}}function t(){}function x(e){return(e||t).__cache__}function D(e,b){var c=x(b),d=c[e];if(d)return d;d=Object.create((b||t).prototype);d.ptr=e;return c[e]=d}function R(e){if("string"===typeof e){for(var b=0,c=0;c<e.length;++c){var d=e.charCodeAt(c);127>=d?b++:2047>=d?b+=2:55296<=d&&57343>=
d?(b+=4,++c):b+=3}b=Array(b+1);c=0;d=b.length;if(0<d){d=c+d-1;for(var g=0;g<e.length;++g){var u=e.charCodeAt(g);if(55296<=u&&57343>=u){var X=e.charCodeAt(++g);u=65536+((u&1023)<<10)|X&1023}if(127>=u){if(c>=d)break;b[c++]=u}else{if(2047>=u){if(c+1>=d)break;b[c++]=192|u>>6}else{if(65535>=u){if(c+2>=d)break;b[c++]=224|u>>12}else{if(c+3>=d)break;b[c++]=240|u>>18;b[c++]=128|u>>12&63}b[c++]=128|u>>6&63}b[c++]=128|u&63}}b[c]=0}e=r.alloc(b,Y);r.copy(b,Y,e);return e}return e}function pa(e){if("object"===typeof e){var b=
r.alloc(e,Y);r.copy(e,Y,b);return b}return e}function Z(){throw"cannot construct a VoidPtr, no constructor in IDL";}function S(){this.ptr=Da();x(S)[this.ptr]=this}function Q(){this.ptr=Ea();x(Q)[this.ptr]=this}function W(){this.ptr=Fa();x(W)[this.ptr]=this}function w(){this.ptr=Ga();x(w)[this.ptr]=this}function C(){this.ptr=Ha();x(C)[this.ptr]=this}function F(){this.ptr=Ia();x(F)[this.ptr]=this}function G(){this.ptr=Ja();x(G)[this.ptr]=this}function E(){this.ptr=Ka();x(E)[this.ptr]=this}function T(){this.ptr=
La();x(T)[this.ptr]=this}function B(){throw"cannot construct a Status, no constructor in IDL";}function H(){this.ptr=Ma();x(H)[this.ptr]=this}function I(){this.ptr=Na();x(I)[this.ptr]=this}function J(){this.ptr=Oa();x(J)[this.ptr]=this}function K(){this.ptr=Pa();x(K)[this.ptr]=this}function L(){this.ptr=Qa();x(L)[this.ptr]=this}function M(){this.ptr=Ra();x(M)[this.ptr]=this}function N(){this.ptr=Sa();x(N)[this.ptr]=this}function y(){this.ptr=Ta();x(y)[this.ptr]=this}function m(){this.ptr=Ua();x(m)[this.ptr]=
this}n=void 0===n?{}:n;var a="undefined"!=typeof n?n:{},Aa,ka;a.ready=new Promise(function(e,b){Aa=e;ka=b});var Va=!1,Wa=!1;a.onRuntimeInitialized=function(){Va=!0;if(Wa&&"function"===typeof a.onModuleLoaded)a.onModuleLoaded(a)};a.onModuleParsed=function(){Wa=!0;if(Va&&"function"===typeof a.onModuleLoaded)a.onModuleLoaded(a)};a.isVersionSupported=function(e){if("string"!==typeof e)return!1;e=e.split(".");return 2>e.length||3<e.length?!1:1==e[0]&&0<=e[1]&&5>=e[1]?!0:0!=e[0]||10<e[1]?!1:!0};var Xa=
Object.assign({},a),xa="object"==typeof window,ha="function"==typeof importScripts,Ya="object"==typeof process&&"object"==typeof process.versions&&"string"==typeof process.versions.node,U="";if(Ya){var Za=require("fs"),qa=require("path");U=ha?qa.dirname(U)+"/":__dirname+"/";var $a=function(e,b){e=e.startsWith("file://")?new URL(e):qa.normalize(e);return Za.readFileSync(e,b?void 0:"utf8")};var ma=function(e){e=$a(e,!0);e.buffer||(e=new Uint8Array(e));return e};var na=function(e,b,c){e=e.startsWith("file://")?
new URL(e):qa.normalize(e);Za.readFile(e,function(d,g){d?c(d):b(g.buffer)})};1<process.argv.length&&process.argv[1].replace(/\\/g,"/");process.argv.slice(2);a.inspect=function(){return"[Emscripten Module object]"}}else if(xa||ha)ha?U=self.location.href:"undefined"!=typeof document&&document.currentScript&&(U=document.currentScript.src),k&&(U=k),U=0!==U.indexOf("blob:")?U.substr(0,U.replace(/[?#].*/,"").lastIndexOf("/")+1):"",$a=function(e){var b=new XMLHttpRequest;b.open("GET",e,!1);b.send(null);
return b.responseText},ha&&(ma=function(e){var b=new XMLHttpRequest;b.open("GET",e,!1);b.responseType="arraybuffer";b.send(null);return new Uint8Array(b.response)}),na=function(e,b,c){var d=new XMLHttpRequest;d.open("GET",e,!0);d.responseType="arraybuffer";d.onload=function(){200==d.status||0==d.status&&d.response?b(d.response):c()};d.onerror=c;d.send(null)};var ud=a.print||console.log.bind(console),da=a.printErr||console.warn.bind(console);Object.assign(a,Xa);Xa=null;var fa;a.wasmBinary&&(fa=a.wasmBinary);
"object"!=typeof WebAssembly&&f("no native wasm support detected");var ja,wa=!1,va="undefined"!=typeof TextDecoder?new TextDecoder("utf8"):void 0,Y,ea,ca,V,Ca=[],oa=[],Ba=[],za=!1,aa=0,ra=null,ia=null;var P="draco_decoder.wasm";P.startsWith("data:application/octet-stream;base64,")||(P=l(P));var vd=0,wd=[null,[],[]],xd={b:function(e,b,c){(new O(e)).init(b,c);vd++;throw e;},a:function(){f("")},g:function(e,b,c){ea.copyWithin(e,b,b+c)},e:function(e){var b=ea.length;e>>>=0;if(2147483648<e)return!1;for(var c=
1;4>=c;c*=2){var d=b*(1+.2/c);d=Math.min(d,e+100663296);var g=Math;d=Math.max(e,d);g=g.min.call(g,2147483648,d+(65536-d%65536)%65536);a:{d=ja.buffer;try{ja.grow(g-d.byteLength+65535>>>16);A();var u=1;break a}catch(X){}u=void 0}if(u)return!0}return!1},f:function(e){return 52},d:function(e,b,c,d,g){return 70},c:function(e,b,c,d){for(var g=0,u=0;u<c;u++){var X=V[b>>2],ab=V[b+4>>2];b+=8;for(var sa=0;sa<ab;sa++){var ta=ea[X+sa],ua=wd[e];0===ta||10===ta?((1===e?ud:da)(p(ua,0)),ua.length=0):ua.push(ta)}g+=
ab}V[d>>2]=g;return 0}};(function(){function e(g,u){a.asm=g.exports;ja=a.asm.h;A();oa.unshift(a.asm.i);aa--;a.monitorRunDependencies&&a.monitorRunDependencies(aa);0==aa&&(null!==ra&&(clearInterval(ra),ra=null),ia&&(g=ia,ia=null,g()))}function b(g){e(g.instance)}function c(g){return v().then(function(u){return WebAssembly.instantiate(u,d)}).then(function(u){return u}).then(g,function(u){da("failed to asynchronously prepare wasm: "+u);f(u)})}var d={a:xd};aa++;a.monitorRunDependencies&&a.monitorRunDependencies(aa);
if(a.instantiateWasm)try{return a.instantiateWasm(d,e)}catch(g){da("Module.instantiateWasm callback failed with error: "+g),ka(g)}(function(){return fa||"function"!=typeof WebAssembly.instantiateStreaming||P.startsWith("data:application/octet-stream;base64,")||P.startsWith("file://")||Ya||"function"!=typeof fetch?c(b):fetch(P,{credentials:"same-origin"}).then(function(g){return WebAssembly.instantiateStreaming(g,d).then(b,function(u){da("wasm streaming compile failed: "+u);da("falling back to ArrayBuffer instantiation");
return c(b)})})})().catch(ka);return{}})();var bb=a._emscripten_bind_VoidPtr___destroy___0=function(){return(bb=a._emscripten_bind_VoidPtr___destroy___0=a.asm.k).apply(null,arguments)},Da=a._emscripten_bind_DecoderBuffer_DecoderBuffer_0=function(){return(Da=a._emscripten_bind_DecoderBuffer_DecoderBuffer_0=a.asm.l).apply(null,arguments)},cb=a._emscripten_bind_DecoderBuffer_Init_2=function(){return(cb=a._emscripten_bind_DecoderBuffer_Init_2=a.asm.m).apply(null,arguments)},db=a._emscripten_bind_DecoderBuffer___destroy___0=
function(){return(db=a._emscripten_bind_DecoderBuffer___destroy___0=a.asm.n).apply(null,arguments)},Ea=a._emscripten_bind_AttributeTransformData_AttributeTransformData_0=function(){return(Ea=a._emscripten_bind_AttributeTransformData_AttributeTransformData_0=a.asm.o).apply(null,arguments)},eb=a._emscripten_bind_AttributeTransformData_transform_type_0=function(){return(eb=a._emscripten_bind_AttributeTransformData_transform_type_0=a.asm.p).apply(null,arguments)},fb=a._emscripten_bind_AttributeTransformData___destroy___0=
function(){return(fb=a._emscripten_bind_AttributeTransformData___destroy___0=a.asm.q).apply(null,arguments)},Fa=a._emscripten_bind_GeometryAttribute_GeometryAttribute_0=function(){return(Fa=a._emscripten_bind_GeometryAttribute_GeometryAttribute_0=a.asm.r).apply(null,arguments)},gb=a._emscripten_bind_GeometryAttribute___destroy___0=function(){return(gb=a._emscripten_bind_GeometryAttribute___destroy___0=a.asm.s).apply(null,arguments)},Ga=a._emscripten_bind_PointAttribute_PointAttribute_0=function(){return(Ga=
a._emscripten_bind_PointAttribute_PointAttribute_0=a.asm.t).apply(null,arguments)},hb=a._emscripten_bind_PointAttribute_size_0=function(){return(hb=a._emscripten_bind_PointAttribute_size_0=a.asm.u).apply(null,arguments)},ib=a._emscripten_bind_PointAttribute_GetAttributeTransformData_0=function(){return(ib=a._emscripten_bind_PointAttribute_GetAttributeTransformData_0=a.asm.v).apply(null,arguments)},jb=a._emscripten_bind_PointAttribute_attribute_type_0=function(){return(jb=a._emscripten_bind_PointAttribute_attribute_type_0=
a.asm.w).apply(null,arguments)},kb=a._emscripten_bind_PointAttribute_data_type_0=function(){return(kb=a._emscripten_bind_PointAttribute_data_type_0=a.asm.x).apply(null,arguments)},lb=a._emscripten_bind_PointAttribute_num_components_0=function(){return(lb=a._emscripten_bind_PointAttribute_num_components_0=a.asm.y).apply(null,arguments)},mb=a._emscripten_bind_PointAttribute_normalized_0=function(){return(mb=a._emscripten_bind_PointAttribute_normalized_0=a.asm.z).apply(null,arguments)},nb=a._emscripten_bind_PointAttribute_byte_stride_0=
function(){return(nb=a._emscripten_bind_PointAttribute_byte_stride_0=a.asm.A).apply(null,arguments)},ob=a._emscripten_bind_PointAttribute_byte_offset_0=function(){return(ob=a._emscripten_bind_PointAttribute_byte_offset_0=a.asm.B).apply(null,arguments)},pb=a._emscripten_bind_PointAttribute_unique_id_0=function(){return(pb=a._emscripten_bind_PointAttribute_unique_id_0=a.asm.C).apply(null,arguments)},qb=a._emscripten_bind_PointAttribute___destroy___0=function(){return(qb=a._emscripten_bind_PointAttribute___destroy___0=
a.asm.D).apply(null,arguments)},Ha=a._emscripten_bind_AttributeQuantizationTransform_AttributeQuantizationTransform_0=function(){return(Ha=a._emscripten_bind_AttributeQuantizationTransform_AttributeQuantizationTransform_0=a.asm.E).apply(null,arguments)},rb=a._emscripten_bind_AttributeQuantizationTransform_InitFromAttribute_1=function(){return(rb=a._emscripten_bind_AttributeQuantizationTransform_InitFromAttribute_1=a.asm.F).apply(null,arguments)},sb=a._emscripten_bind_AttributeQuantizationTransform_quantization_bits_0=
function(){return(sb=a._emscripten_bind_AttributeQuantizationTransform_quantization_bits_0=a.asm.G).apply(null,arguments)},tb=a._emscripten_bind_AttributeQuantizationTransform_min_value_1=function(){return(tb=a._emscripten_bind_AttributeQuantizationTransform_min_value_1=a.asm.H).apply(null,arguments)},ub=a._emscripten_bind_AttributeQuantizationTransform_range_0=function(){return(ub=a._emscripten_bind_AttributeQuantizationTransform_range_0=a.asm.I).apply(null,arguments)},vb=a._emscripten_bind_AttributeQuantizationTransform___destroy___0=
function(){return(vb=a._emscripten_bind_AttributeQuantizationTransform___destroy___0=a.asm.J).apply(null,arguments)},Ia=a._emscripten_bind_AttributeOctahedronTransform_AttributeOctahedronTransform_0=function(){return(Ia=a._emscripten_bind_AttributeOctahedronTransform_AttributeOctahedronTransform_0=a.asm.K).apply(null,arguments)},wb=a._emscripten_bind_AttributeOctahedronTransform_InitFromAttribute_1=function(){return(wb=a._emscripten_bind_AttributeOctahedronTransform_InitFromAttribute_1=a.asm.L).apply(null,
arguments)},xb=a._emscripten_bind_AttributeOctahedronTransform_quantization_bits_0=function(){return(xb=a._emscripten_bind_AttributeOctahedronTransform_quantization_bits_0=a.asm.M).apply(null,arguments)},yb=a._emscripten_bind_AttributeOctahedronTransform___destroy___0=function(){return(yb=a._emscripten_bind_AttributeOctahedronTransform___destroy___0=a.asm.N).apply(null,arguments)},Ja=a._emscripten_bind_PointCloud_PointCloud_0=function(){return(Ja=a._emscripten_bind_PointCloud_PointCloud_0=a.asm.O).apply(null,
arguments)},zb=a._emscripten_bind_PointCloud_num_attributes_0=function(){return(zb=a._emscripten_bind_PointCloud_num_attributes_0=a.asm.P).apply(null,arguments)},Ab=a._emscripten_bind_PointCloud_num_points_0=function(){return(Ab=a._emscripten_bind_PointCloud_num_points_0=a.asm.Q).apply(null,arguments)},Bb=a._emscripten_bind_PointCloud___destroy___0=function(){return(Bb=a._emscripten_bind_PointCloud___destroy___0=a.asm.R).apply(null,arguments)},Ka=a._emscripten_bind_Mesh_Mesh_0=function(){return(Ka=
a._emscripten_bind_Mesh_Mesh_0=a.asm.S).apply(null,arguments)},Cb=a._emscripten_bind_Mesh_num_faces_0=function(){return(Cb=a._emscripten_bind_Mesh_num_faces_0=a.asm.T).apply(null,arguments)},Db=a._emscripten_bind_Mesh_num_attributes_0=function(){return(Db=a._emscripten_bind_Mesh_num_attributes_0=a.asm.U).apply(null,arguments)},Eb=a._emscripten_bind_Mesh_num_points_0=function(){return(Eb=a._emscripten_bind_Mesh_num_points_0=a.asm.V).apply(null,arguments)},Fb=a._emscripten_bind_Mesh___destroy___0=function(){return(Fb=
a._emscripten_bind_Mesh___destroy___0=a.asm.W).apply(null,arguments)},La=a._emscripten_bind_Metadata_Metadata_0=function(){return(La=a._emscripten_bind_Metadata_Metadata_0=a.asm.X).apply(null,arguments)},Gb=a._emscripten_bind_Metadata___destroy___0=function(){return(Gb=a._emscripten_bind_Metadata___destroy___0=a.asm.Y).apply(null,arguments)},Hb=a._emscripten_bind_Status_code_0=function(){return(Hb=a._emscripten_bind_Status_code_0=a.asm.Z).apply(null,arguments)},Ib=a._emscripten_bind_Status_ok_0=function(){return(Ib=
a._emscripten_bind_Status_ok_0=a.asm._).apply(null,arguments)},Jb=a._emscripten_bind_Status_error_msg_0=function(){return(Jb=a._emscripten_bind_Status_error_msg_0=a.asm.$).apply(null,arguments)},Kb=a._emscripten_bind_Status___destroy___0=function(){return(Kb=a._emscripten_bind_Status___destroy___0=a.asm.aa).apply(null,arguments)},Ma=a._emscripten_bind_DracoFloat32Array_DracoFloat32Array_0=function(){return(Ma=a._emscripten_bind_DracoFloat32Array_DracoFloat32Array_0=a.asm.ba).apply(null,arguments)},
Lb=a._emscripten_bind_DracoFloat32Array_GetValue_1=function(){return(Lb=a._emscripten_bind_DracoFloat32Array_GetValue_1=a.asm.ca).apply(null,arguments)},Mb=a._emscripten_bind_DracoFloat32Array_size_0=function(){return(Mb=a._emscripten_bind_DracoFloat32Array_size_0=a.asm.da).apply(null,arguments)},Nb=a._emscripten_bind_DracoFloat32Array___destroy___0=function(){return(Nb=a._emscripten_bind_DracoFloat32Array___destroy___0=a.asm.ea).apply(null,arguments)},Na=a._emscripten_bind_DracoInt8Array_DracoInt8Array_0=
function(){return(Na=a._emscripten_bind_DracoInt8Array_DracoInt8Array_0=a.asm.fa).apply(null,arguments)},Ob=a._emscripten_bind_DracoInt8Array_GetValue_1=function(){return(Ob=a._emscripten_bind_DracoInt8Array_GetValue_1=a.asm.ga).apply(null,arguments)},Pb=a._emscripten_bind_DracoInt8Array_size_0=function(){return(Pb=a._emscripten_bind_DracoInt8Array_size_0=a.asm.ha).apply(null,arguments)},Qb=a._emscripten_bind_DracoInt8Array___destroy___0=function(){return(Qb=a._emscripten_bind_DracoInt8Array___destroy___0=
a.asm.ia).apply(null,arguments)},Oa=a._emscripten_bind_DracoUInt8Array_DracoUInt8Array_0=function(){return(Oa=a._emscripten_bind_DracoUInt8Array_DracoUInt8Array_0=a.asm.ja).apply(null,arguments)},Rb=a._emscripten_bind_DracoUInt8Array_GetValue_1=function(){return(Rb=a._emscripten_bind_DracoUInt8Array_GetValue_1=a.asm.ka).apply(null,arguments)},Sb=a._emscripten_bind_DracoUInt8Array_size_0=function(){return(Sb=a._emscripten_bind_DracoUInt8Array_size_0=a.asm.la).apply(null,arguments)},Tb=a._emscripten_bind_DracoUInt8Array___destroy___0=
function(){return(Tb=a._emscripten_bind_DracoUInt8Array___destroy___0=a.asm.ma).apply(null,arguments)},Pa=a._emscripten_bind_DracoInt16Array_DracoInt16Array_0=function(){return(Pa=a._emscripten_bind_DracoInt16Array_DracoInt16Array_0=a.asm.na).apply(null,arguments)},Ub=a._emscripten_bind_DracoInt16Array_GetValue_1=function(){return(Ub=a._emscripten_bind_DracoInt16Array_GetValue_1=a.asm.oa).apply(null,arguments)},Vb=a._emscripten_bind_DracoInt16Array_size_0=function(){return(Vb=a._emscripten_bind_DracoInt16Array_size_0=
a.asm.pa).apply(null,arguments)},Wb=a._emscripten_bind_DracoInt16Array___destroy___0=function(){return(Wb=a._emscripten_bind_DracoInt16Array___destroy___0=a.asm.qa).apply(null,arguments)},Qa=a._emscripten_bind_DracoUInt16Array_DracoUInt16Array_0=function(){return(Qa=a._emscripten_bind_DracoUInt16Array_DracoUInt16Array_0=a.asm.ra).apply(null,arguments)},Xb=a._emscripten_bind_DracoUInt16Array_GetValue_1=function(){return(Xb=a._emscripten_bind_DracoUInt16Array_GetValue_1=a.asm.sa).apply(null,arguments)},
Yb=a._emscripten_bind_DracoUInt16Array_size_0=function(){return(Yb=a._emscripten_bind_DracoUInt16Array_size_0=a.asm.ta).apply(null,arguments)},Zb=a._emscripten_bind_DracoUInt16Array___destroy___0=function(){return(Zb=a._emscripten_bind_DracoUInt16Array___destroy___0=a.asm.ua).apply(null,arguments)},Ra=a._emscripten_bind_DracoInt32Array_DracoInt32Array_0=function(){return(Ra=a._emscripten_bind_DracoInt32Array_DracoInt32Array_0=a.asm.va).apply(null,arguments)},$b=a._emscripten_bind_DracoInt32Array_GetValue_1=
function(){return($b=a._emscripten_bind_DracoInt32Array_GetValue_1=a.asm.wa).apply(null,arguments)},ac=a._emscripten_bind_DracoInt32Array_size_0=function(){return(ac=a._emscripten_bind_DracoInt32Array_size_0=a.asm.xa).apply(null,arguments)},bc=a._emscripten_bind_DracoInt32Array___destroy___0=function(){return(bc=a._emscripten_bind_DracoInt32Array___destroy___0=a.asm.ya).apply(null,arguments)},Sa=a._emscripten_bind_DracoUInt32Array_DracoUInt32Array_0=function(){return(Sa=a._emscripten_bind_DracoUInt32Array_DracoUInt32Array_0=
a.asm.za).apply(null,arguments)},cc=a._emscripten_bind_DracoUInt32Array_GetValue_1=function(){return(cc=a._emscripten_bind_DracoUInt32Array_GetValue_1=a.asm.Aa).apply(null,arguments)},dc=a._emscripten_bind_DracoUInt32Array_size_0=function(){return(dc=a._emscripten_bind_DracoUInt32Array_size_0=a.asm.Ba).apply(null,arguments)},ec=a._emscripten_bind_DracoUInt32Array___destroy___0=function(){return(ec=a._emscripten_bind_DracoUInt32Array___destroy___0=a.asm.Ca).apply(null,arguments)},Ta=a._emscripten_bind_MetadataQuerier_MetadataQuerier_0=
function(){return(Ta=a._emscripten_bind_MetadataQuerier_MetadataQuerier_0=a.asm.Da).apply(null,arguments)},fc=a._emscripten_bind_MetadataQuerier_HasEntry_2=function(){return(fc=a._emscripten_bind_MetadataQuerier_HasEntry_2=a.asm.Ea).apply(null,arguments)},gc=a._emscripten_bind_MetadataQuerier_GetIntEntry_2=function(){return(gc=a._emscripten_bind_MetadataQuerier_GetIntEntry_2=a.asm.Fa).apply(null,arguments)},hc=a._emscripten_bind_MetadataQuerier_GetIntEntryArray_3=function(){return(hc=a._emscripten_bind_MetadataQuerier_GetIntEntryArray_3=
a.asm.Ga).apply(null,arguments)},ic=a._emscripten_bind_MetadataQuerier_GetDoubleEntry_2=function(){return(ic=a._emscripten_bind_MetadataQuerier_GetDoubleEntry_2=a.asm.Ha).apply(null,arguments)},jc=a._emscripten_bind_MetadataQuerier_GetStringEntry_2=function(){return(jc=a._emscripten_bind_MetadataQuerier_GetStringEntry_2=a.asm.Ia).apply(null,arguments)},kc=a._emscripten_bind_MetadataQuerier_NumEntries_1=function(){return(kc=a._emscripten_bind_MetadataQuerier_NumEntries_1=a.asm.Ja).apply(null,arguments)},
lc=a._emscripten_bind_MetadataQuerier_GetEntryName_2=function(){return(lc=a._emscripten_bind_MetadataQuerier_GetEntryName_2=a.asm.Ka).apply(null,arguments)},mc=a._emscripten_bind_MetadataQuerier___destroy___0=function(){return(mc=a._emscripten_bind_MetadataQuerier___destroy___0=a.asm.La).apply(null,arguments)},Ua=a._emscripten_bind_Decoder_Decoder_0=function(){return(Ua=a._emscripten_bind_Decoder_Decoder_0=a.asm.Ma).apply(null,arguments)},nc=a._emscripten_bind_Decoder_DecodeArrayToPointCloud_3=function(){return(nc=
a._emscripten_bind_Decoder_DecodeArrayToPointCloud_3=a.asm.Na).apply(null,arguments)},oc=a._emscripten_bind_Decoder_DecodeArrayToMesh_3=function(){return(oc=a._emscripten_bind_Decoder_DecodeArrayToMesh_3=a.asm.Oa).apply(null,arguments)},pc=a._emscripten_bind_Decoder_GetAttributeId_2=function(){return(pc=a._emscripten_bind_Decoder_GetAttributeId_2=a.asm.Pa).apply(null,arguments)},qc=a._emscripten_bind_Decoder_GetAttributeIdByName_2=function(){return(qc=a._emscripten_bind_Decoder_GetAttributeIdByName_2=
a.asm.Qa).apply(null,arguments)},rc=a._emscripten_bind_Decoder_GetAttributeIdByMetadataEntry_3=function(){return(rc=a._emscripten_bind_Decoder_GetAttributeIdByMetadataEntry_3=a.asm.Ra).apply(null,arguments)},sc=a._emscripten_bind_Decoder_GetAttribute_2=function(){return(sc=a._emscripten_bind_Decoder_GetAttribute_2=a.asm.Sa).apply(null,arguments)},tc=a._emscripten_bind_Decoder_GetAttributeByUniqueId_2=function(){return(tc=a._emscripten_bind_Decoder_GetAttributeByUniqueId_2=a.asm.Ta).apply(null,arguments)},
uc=a._emscripten_bind_Decoder_GetMetadata_1=function(){return(uc=a._emscripten_bind_Decoder_GetMetadata_1=a.asm.Ua).apply(null,arguments)},vc=a._emscripten_bind_Decoder_GetAttributeMetadata_2=function(){return(vc=a._emscripten_bind_Decoder_GetAttributeMetadata_2=a.asm.Va).apply(null,arguments)},wc=a._emscripten_bind_Decoder_GetFaceFromMesh_3=function(){return(wc=a._emscripten_bind_Decoder_GetFaceFromMesh_3=a.asm.Wa).apply(null,arguments)},xc=a._emscripten_bind_Decoder_GetTriangleStripsFromMesh_2=
function(){return(xc=a._emscripten_bind_Decoder_GetTriangleStripsFromMesh_2=a.asm.Xa).apply(null,arguments)},yc=a._emscripten_bind_Decoder_GetTrianglesUInt16Array_3=function(){return(yc=a._emscripten_bind_Decoder_GetTrianglesUInt16Array_3=a.asm.Ya).apply(null,arguments)},zc=a._emscripten_bind_Decoder_GetTrianglesUInt32Array_3=function(){return(zc=a._emscripten_bind_Decoder_GetTrianglesUInt32Array_3=a.asm.Za).apply(null,arguments)},Ac=a._emscripten_bind_Decoder_GetAttributeFloat_3=function(){return(Ac=
a._emscripten_bind_Decoder_GetAttributeFloat_3=a.asm._a).apply(null,arguments)},Bc=a._emscripten_bind_Decoder_GetAttributeFloatForAllPoints_3=function(){return(Bc=a._emscripten_bind_Decoder_GetAttributeFloatForAllPoints_3=a.asm.$a).apply(null,arguments)},Cc=a._emscripten_bind_Decoder_GetAttributeIntForAllPoints_3=function(){return(Cc=a._emscripten_bind_Decoder_GetAttributeIntForAllPoints_3=a.asm.ab).apply(null,arguments)},Dc=a._emscripten_bind_Decoder_GetAttributeInt8ForAllPoints_3=function(){return(Dc=
a._emscripten_bind_Decoder_GetAttributeInt8ForAllPoints_3=a.asm.bb).apply(null,arguments)},Ec=a._emscripten_bind_Decoder_GetAttributeUInt8ForAllPoints_3=function(){return(Ec=a._emscripten_bind_Decoder_GetAttributeUInt8ForAllPoints_3=a.asm.cb).apply(null,arguments)},Fc=a._emscripten_bind_Decoder_GetAttributeInt16ForAllPoints_3=function(){return(Fc=a._emscripten_bind_Decoder_GetAttributeInt16ForAllPoints_3=a.asm.db).apply(null,arguments)},Gc=a._emscripten_bind_Decoder_GetAttributeUInt16ForAllPoints_3=
function(){return(Gc=a._emscripten_bind_Decoder_GetAttributeUInt16ForAllPoints_3=a.asm.eb).apply(null,arguments)},Hc=a._emscripten_bind_Decoder_GetAttributeInt32ForAllPoints_3=function(){return(Hc=a._emscripten_bind_Decoder_GetAttributeInt32ForAllPoints_3=a.asm.fb).apply(null,arguments)},Ic=a._emscripten_bind_Decoder_GetAttributeUInt32ForAllPoints_3=function(){return(Ic=a._emscripten_bind_Decoder_GetAttributeUInt32ForAllPoints_3=a.asm.gb).apply(null,arguments)},Jc=a._emscripten_bind_Decoder_GetAttributeDataArrayForAllPoints_5=
function(){return(Jc=a._emscripten_bind_Decoder_GetAttributeDataArrayForAllPoints_5=a.asm.hb).apply(null,arguments)},Kc=a._emscripten_bind_Decoder_SkipAttributeTransform_1=function(){return(Kc=a._emscripten_bind_Decoder_SkipAttributeTransform_1=a.asm.ib).apply(null,arguments)},Lc=a._emscripten_bind_Decoder_GetEncodedGeometryType_Deprecated_1=function(){return(Lc=a._emscripten_bind_Decoder_GetEncodedGeometryType_Deprecated_1=a.asm.jb).apply(null,arguments)},Mc=a._emscripten_bind_Decoder_DecodeBufferToPointCloud_2=
function(){return(Mc=a._emscripten_bind_Decoder_DecodeBufferToPointCloud_2=a.asm.kb).apply(null,arguments)},Nc=a._emscripten_bind_Decoder_DecodeBufferToMesh_2=function(){return(Nc=a._emscripten_bind_Decoder_DecodeBufferToMesh_2=a.asm.lb).apply(null,arguments)},Oc=a._emscripten_bind_Decoder___destroy___0=function(){return(Oc=a._emscripten_bind_Decoder___destroy___0=a.asm.mb).apply(null,arguments)},Pc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_INVALID_TRANSFORM=function(){return(Pc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_INVALID_TRANSFORM=
a.asm.nb).apply(null,arguments)},Qc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_NO_TRANSFORM=function(){return(Qc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_NO_TRANSFORM=a.asm.ob).apply(null,arguments)},Rc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_QUANTIZATION_TRANSFORM=function(){return(Rc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_QUANTIZATION_TRANSFORM=a.asm.pb).apply(null,arguments)},Sc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_OCTAHEDRON_TRANSFORM=
function(){return(Sc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_OCTAHEDRON_TRANSFORM=a.asm.qb).apply(null,arguments)},Tc=a._emscripten_enum_draco_GeometryAttribute_Type_INVALID=function(){return(Tc=a._emscripten_enum_draco_GeometryAttribute_Type_INVALID=a.asm.rb).apply(null,arguments)},Uc=a._emscripten_enum_draco_GeometryAttribute_Type_POSITION=function(){return(Uc=a._emscripten_enum_draco_GeometryAttribute_Type_POSITION=a.asm.sb).apply(null,arguments)},Vc=a._emscripten_enum_draco_GeometryAttribute_Type_NORMAL=
function(){return(Vc=a._emscripten_enum_draco_GeometryAttribute_Type_NORMAL=a.asm.tb).apply(null,arguments)},Wc=a._emscripten_enum_draco_GeometryAttribute_Type_COLOR=function(){return(Wc=a._emscripten_enum_draco_GeometryAttribute_Type_COLOR=a.asm.ub).apply(null,arguments)},Xc=a._emscripten_enum_draco_GeometryAttribute_Type_TEX_COORD=function(){return(Xc=a._emscripten_enum_draco_GeometryAttribute_Type_TEX_COORD=a.asm.vb).apply(null,arguments)},Yc=a._emscripten_enum_draco_GeometryAttribute_Type_GENERIC=
function(){return(Yc=a._emscripten_enum_draco_GeometryAttribute_Type_GENERIC=a.asm.wb).apply(null,arguments)},Zc=a._emscripten_enum_draco_EncodedGeometryType_INVALID_GEOMETRY_TYPE=function(){return(Zc=a._emscripten_enum_draco_EncodedGeometryType_INVALID_GEOMETRY_TYPE=a.asm.xb).apply(null,arguments)},$c=a._emscripten_enum_draco_EncodedGeometryType_POINT_CLOUD=function(){return($c=a._emscripten_enum_draco_EncodedGeometryType_POINT_CLOUD=a.asm.yb).apply(null,arguments)},ad=a._emscripten_enum_draco_EncodedGeometryType_TRIANGULAR_MESH=
function(){return(ad=a._emscripten_enum_draco_EncodedGeometryType_TRIANGULAR_MESH=a.asm.zb).apply(null,arguments)},bd=a._emscripten_enum_draco_DataType_DT_INVALID=function(){return(bd=a._emscripten_enum_draco_DataType_DT_INVALID=a.asm.Ab).apply(null,arguments)},cd=a._emscripten_enum_draco_DataType_DT_INT8=function(){return(cd=a._emscripten_enum_draco_DataType_DT_INT8=a.asm.Bb).apply(null,arguments)},dd=a._emscripten_enum_draco_DataType_DT_UINT8=function(){return(dd=a._emscripten_enum_draco_DataType_DT_UINT8=
a.asm.Cb).apply(null,arguments)},ed=a._emscripten_enum_draco_DataType_DT_INT16=function(){return(ed=a._emscripten_enum_draco_DataType_DT_INT16=a.asm.Db).apply(null,arguments)},fd=a._emscripten_enum_draco_DataType_DT_UINT16=function(){return(fd=a._emscripten_enum_draco_DataType_DT_UINT16=a.asm.Eb).apply(null,arguments)},gd=a._emscripten_enum_draco_DataType_DT_INT32=function(){return(gd=a._emscripten_enum_draco_DataType_DT_INT32=a.asm.Fb).apply(null,arguments)},hd=a._emscripten_enum_draco_DataType_DT_UINT32=
function(){return(hd=a._emscripten_enum_draco_DataType_DT_UINT32=a.asm.Gb).apply(null,arguments)},id=a._emscripten_enum_draco_DataType_DT_INT64=function(){return(id=a._emscripten_enum_draco_DataType_DT_INT64=a.asm.Hb).apply(null,arguments)},jd=a._emscripten_enum_draco_DataType_DT_UINT64=function(){return(jd=a._emscripten_enum_draco_DataType_DT_UINT64=a.asm.Ib).apply(null,arguments)},kd=a._emscripten_enum_draco_DataType_DT_FLOAT32=function(){return(kd=a._emscripten_enum_draco_DataType_DT_FLOAT32=a.asm.Jb).apply(null,
arguments)},ld=a._emscripten_enum_draco_DataType_DT_FLOAT64=function(){return(ld=a._emscripten_enum_draco_DataType_DT_FLOAT64=a.asm.Kb).apply(null,arguments)},md=a._emscripten_enum_draco_DataType_DT_BOOL=function(){return(md=a._emscripten_enum_draco_DataType_DT_BOOL=a.asm.Lb).apply(null,arguments)},nd=a._emscripten_enum_draco_DataType_DT_TYPES_COUNT=function(){return(nd=a._emscripten_enum_draco_DataType_DT_TYPES_COUNT=a.asm.Mb).apply(null,arguments)},od=a._emscripten_enum_draco_StatusCode_OK=function(){return(od=
a._emscripten_enum_draco_StatusCode_OK=a.asm.Nb).apply(null,arguments)},pd=a._emscripten_enum_draco_StatusCode_DRACO_ERROR=function(){return(pd=a._emscripten_enum_draco_StatusCode_DRACO_ERROR=a.asm.Ob).apply(null,arguments)},qd=a._emscripten_enum_draco_StatusCode_IO_ERROR=function(){return(qd=a._emscripten_enum_draco_StatusCode_IO_ERROR=a.asm.Pb).apply(null,arguments)},rd=a._emscripten_enum_draco_StatusCode_INVALID_PARAMETER=function(){return(rd=a._emscripten_enum_draco_StatusCode_INVALID_PARAMETER=
a.asm.Qb).apply(null,arguments)},sd=a._emscripten_enum_draco_StatusCode_UNSUPPORTED_VERSION=function(){return(sd=a._emscripten_enum_draco_StatusCode_UNSUPPORTED_VERSION=a.asm.Rb).apply(null,arguments)},td=a._emscripten_enum_draco_StatusCode_UNKNOWN_VERSION=function(){return(td=a._emscripten_enum_draco_StatusCode_UNKNOWN_VERSION=a.asm.Sb).apply(null,arguments)};a._malloc=function(){return(a._malloc=a.asm.Tb).apply(null,arguments)};a._free=function(){return(a._free=a.asm.Ub).apply(null,arguments)};
var ya=function(){return(ya=a.asm.Vb).apply(null,arguments)};a.___start_em_js=15856;a.___stop_em_js=15954;var la;ia=function b(){la||ba();la||(ia=b)};if(a.preInit)for("function"==typeof a.preInit&&(a.preInit=[a.preInit]);0<a.preInit.length;)a.preInit.pop()();ba();t.prototype=Object.create(t.prototype);t.prototype.constructor=t;t.prototype.__class__=t;t.__cache__={};a.WrapperObject=t;a.getCache=x;a.wrapPointer=D;a.castObject=function(b,c){return D(b.ptr,c)};a.NULL=D(0);a.destroy=function(b){if(!b.__destroy__)throw"Error: Cannot destroy object. (Did you create it yourself?)";
b.__destroy__();delete x(b.__class__)[b.ptr]};a.compare=function(b,c){return b.ptr===c.ptr};a.getPointer=function(b){return b.ptr};a.getClass=function(b){return b.__class__};var r={buffer:0,size:0,pos:0,temps:[],needed:0,prepare:function(){if(r.needed){for(var b=0;b<r.temps.length;b++)a._free(r.temps[b]);r.temps.length=0;a._free(r.buffer);r.buffer=0;r.size+=r.needed;r.needed=0}r.buffer||(r.size+=128,r.buffer=a._malloc(r.size),r.buffer||f(void 0));r.pos=0},alloc:function(b,c){r.buffer||f(void 0);b=
b.length*c.BYTES_PER_ELEMENT;b=b+7&-8;r.pos+b>=r.size?(0<b||f(void 0),r.needed+=b,c=a._malloc(b),r.temps.push(c)):(c=r.buffer+r.pos,r.pos+=b);return c},copy:function(b,c,d){d>>>=0;switch(c.BYTES_PER_ELEMENT){case 2:d>>>=1;break;case 4:d>>>=2;break;case 8:d>>>=3}for(var g=0;g<b.length;g++)c[d+g]=b[g]}};Z.prototype=Object.create(t.prototype);Z.prototype.constructor=Z;Z.prototype.__class__=Z;Z.__cache__={};a.VoidPtr=Z;Z.prototype.__destroy__=Z.prototype.__destroy__=function(){bb(this.ptr)};S.prototype=
Object.create(t.prototype);S.prototype.constructor=S;S.prototype.__class__=S;S.__cache__={};a.DecoderBuffer=S;S.prototype.Init=S.prototype.Init=function(b,c){var d=this.ptr;r.prepare();"object"==typeof b&&(b=pa(b));c&&"object"===typeof c&&(c=c.ptr);cb(d,b,c)};S.prototype.__destroy__=S.prototype.__destroy__=function(){db(this.ptr)};Q.prototype=Object.create(t.prototype);Q.prototype.constructor=Q;Q.prototype.__class__=Q;Q.__cache__={};a.AttributeTransformData=Q;Q.prototype.transform_type=Q.prototype.transform_type=
function(){return eb(this.ptr)};Q.prototype.__destroy__=Q.prototype.__destroy__=function(){fb(this.ptr)};W.prototype=Object.create(t.prototype);W.prototype.constructor=W;W.prototype.__class__=W;W.__cache__={};a.GeometryAttribute=W;W.prototype.__destroy__=W.prototype.__destroy__=function(){gb(this.ptr)};w.prototype=Object.create(t.prototype);w.prototype.constructor=w;w.prototype.__class__=w;w.__cache__={};a.PointAttribute=w;w.prototype.size=w.prototype.size=function(){return hb(this.ptr)};w.prototype.GetAttributeTransformData=
w.prototype.GetAttributeTransformData=function(){return D(ib(this.ptr),Q)};w.prototype.attribute_type=w.prototype.attribute_type=function(){return jb(this.ptr)};w.prototype.data_type=w.prototype.data_type=function(){return kb(this.ptr)};w.prototype.num_components=w.prototype.num_components=function(){return lb(this.ptr)};w.prototype.normalized=w.prototype.normalized=function(){return!!mb(this.ptr)};w.prototype.byte_stride=w.prototype.byte_stride=function(){return nb(this.ptr)};w.prototype.byte_offset=
w.prototype.byte_offset=function(){return ob(this.ptr)};w.prototype.unique_id=w.prototype.unique_id=function(){return pb(this.ptr)};w.prototype.__destroy__=w.prototype.__destroy__=function(){qb(this.ptr)};C.prototype=Object.create(t.prototype);C.prototype.constructor=C;C.prototype.__class__=C;C.__cache__={};a.AttributeQuantizationTransform=C;C.prototype.InitFromAttribute=C.prototype.InitFromAttribute=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return!!rb(c,b)};C.prototype.quantization_bits=
C.prototype.quantization_bits=function(){return sb(this.ptr)};C.prototype.min_value=C.prototype.min_value=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return tb(c,b)};C.prototype.range=C.prototype.range=function(){return ub(this.ptr)};C.prototype.__destroy__=C.prototype.__destroy__=function(){vb(this.ptr)};F.prototype=Object.create(t.prototype);F.prototype.constructor=F;F.prototype.__class__=F;F.__cache__={};a.AttributeOctahedronTransform=F;F.prototype.InitFromAttribute=F.prototype.InitFromAttribute=
function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return!!wb(c,b)};F.prototype.quantization_bits=F.prototype.quantization_bits=function(){return xb(this.ptr)};F.prototype.__destroy__=F.prototype.__destroy__=function(){yb(this.ptr)};G.prototype=Object.create(t.prototype);G.prototype.constructor=G;G.prototype.__class__=G;G.__cache__={};a.PointCloud=G;G.prototype.num_attributes=G.prototype.num_attributes=function(){return zb(this.ptr)};G.prototype.num_points=G.prototype.num_points=function(){return Ab(this.ptr)};
G.prototype.__destroy__=G.prototype.__destroy__=function(){Bb(this.ptr)};E.prototype=Object.create(t.prototype);E.prototype.constructor=E;E.prototype.__class__=E;E.__cache__={};a.Mesh=E;E.prototype.num_faces=E.prototype.num_faces=function(){return Cb(this.ptr)};E.prototype.num_attributes=E.prototype.num_attributes=function(){return Db(this.ptr)};E.prototype.num_points=E.prototype.num_points=function(){return Eb(this.ptr)};E.prototype.__destroy__=E.prototype.__destroy__=function(){Fb(this.ptr)};T.prototype=
Object.create(t.prototype);T.prototype.constructor=T;T.prototype.__class__=T;T.__cache__={};a.Metadata=T;T.prototype.__destroy__=T.prototype.__destroy__=function(){Gb(this.ptr)};B.prototype=Object.create(t.prototype);B.prototype.constructor=B;B.prototype.__class__=B;B.__cache__={};a.Status=B;B.prototype.code=B.prototype.code=function(){return Hb(this.ptr)};B.prototype.ok=B.prototype.ok=function(){return!!Ib(this.ptr)};B.prototype.error_msg=B.prototype.error_msg=function(){return h(Jb(this.ptr))};
B.prototype.__destroy__=B.prototype.__destroy__=function(){Kb(this.ptr)};H.prototype=Object.create(t.prototype);H.prototype.constructor=H;H.prototype.__class__=H;H.__cache__={};a.DracoFloat32Array=H;H.prototype.GetValue=H.prototype.GetValue=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return Lb(c,b)};H.prototype.size=H.prototype.size=function(){return Mb(this.ptr)};H.prototype.__destroy__=H.prototype.__destroy__=function(){Nb(this.ptr)};I.prototype=Object.create(t.prototype);I.prototype.constructor=
I;I.prototype.__class__=I;I.__cache__={};a.DracoInt8Array=I;I.prototype.GetValue=I.prototype.GetValue=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return Ob(c,b)};I.prototype.size=I.prototype.size=function(){return Pb(this.ptr)};I.prototype.__destroy__=I.prototype.__destroy__=function(){Qb(this.ptr)};J.prototype=Object.create(t.prototype);J.prototype.constructor=J;J.prototype.__class__=J;J.__cache__={};a.DracoUInt8Array=J;J.prototype.GetValue=J.prototype.GetValue=function(b){var c=
this.ptr;b&&"object"===typeof b&&(b=b.ptr);return Rb(c,b)};J.prototype.size=J.prototype.size=function(){return Sb(this.ptr)};J.prototype.__destroy__=J.prototype.__destroy__=function(){Tb(this.ptr)};K.prototype=Object.create(t.prototype);K.prototype.constructor=K;K.prototype.__class__=K;K.__cache__={};a.DracoInt16Array=K;K.prototype.GetValue=K.prototype.GetValue=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return Ub(c,b)};K.prototype.size=K.prototype.size=function(){return Vb(this.ptr)};
K.prototype.__destroy__=K.prototype.__destroy__=function(){Wb(this.ptr)};L.prototype=Object.create(t.prototype);L.prototype.constructor=L;L.prototype.__class__=L;L.__cache__={};a.DracoUInt16Array=L;L.prototype.GetValue=L.prototype.GetValue=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return Xb(c,b)};L.prototype.size=L.prototype.size=function(){return Yb(this.ptr)};L.prototype.__destroy__=L.prototype.__destroy__=function(){Zb(this.ptr)};M.prototype=Object.create(t.prototype);M.prototype.constructor=
M;M.prototype.__class__=M;M.__cache__={};a.DracoInt32Array=M;M.prototype.GetValue=M.prototype.GetValue=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return $b(c,b)};M.prototype.size=M.prototype.size=function(){return ac(this.ptr)};M.prototype.__destroy__=M.prototype.__destroy__=function(){bc(this.ptr)};N.prototype=Object.create(t.prototype);N.prototype.constructor=N;N.prototype.__class__=N;N.__cache__={};a.DracoUInt32Array=N;N.prototype.GetValue=N.prototype.GetValue=function(b){var c=
this.ptr;b&&"object"===typeof b&&(b=b.ptr);return cc(c,b)};N.prototype.size=N.prototype.size=function(){return dc(this.ptr)};N.prototype.__destroy__=N.prototype.__destroy__=function(){ec(this.ptr)};y.prototype=Object.create(t.prototype);y.prototype.constructor=y;y.prototype.__class__=y;y.__cache__={};a.MetadataQuerier=y;y.prototype.HasEntry=y.prototype.HasEntry=function(b,c){var d=this.ptr;r.prepare();b&&"object"===typeof b&&(b=b.ptr);c=c&&"object"===typeof c?c.ptr:R(c);return!!fc(d,b,c)};y.prototype.GetIntEntry=
y.prototype.GetIntEntry=function(b,c){var d=this.ptr;r.prepare();b&&"object"===typeof b&&(b=b.ptr);c=c&&"object"===typeof c?c.ptr:R(c);return gc(d,b,c)};y.prototype.GetIntEntryArray=y.prototype.GetIntEntryArray=function(b,c,d){var g=this.ptr;r.prepare();b&&"object"===typeof b&&(b=b.ptr);c=c&&"object"===typeof c?c.ptr:R(c);d&&"object"===typeof d&&(d=d.ptr);hc(g,b,c,d)};y.prototype.GetDoubleEntry=y.prototype.GetDoubleEntry=function(b,c){var d=this.ptr;r.prepare();b&&"object"===typeof b&&(b=b.ptr);c=
c&&"object"===typeof c?c.ptr:R(c);return ic(d,b,c)};y.prototype.GetStringEntry=y.prototype.GetStringEntry=function(b,c){var d=this.ptr;r.prepare();b&&"object"===typeof b&&(b=b.ptr);c=c&&"object"===typeof c?c.ptr:R(c);return h(jc(d,b,c))};y.prototype.NumEntries=y.prototype.NumEntries=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return kc(c,b)};y.prototype.GetEntryName=y.prototype.GetEntryName=function(b,c){var d=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=
c.ptr);return h(lc(d,b,c))};y.prototype.__destroy__=y.prototype.__destroy__=function(){mc(this.ptr)};m.prototype=Object.create(t.prototype);m.prototype.constructor=m;m.prototype.__class__=m;m.__cache__={};a.Decoder=m;m.prototype.DecodeArrayToPointCloud=m.prototype.DecodeArrayToPointCloud=function(b,c,d){var g=this.ptr;r.prepare();"object"==typeof b&&(b=pa(b));c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return D(nc(g,b,c,d),B)};m.prototype.DecodeArrayToMesh=m.prototype.DecodeArrayToMesh=
function(b,c,d){var g=this.ptr;r.prepare();"object"==typeof b&&(b=pa(b));c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return D(oc(g,b,c,d),B)};m.prototype.GetAttributeId=m.prototype.GetAttributeId=function(b,c){var d=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);return pc(d,b,c)};m.prototype.GetAttributeIdByName=m.prototype.GetAttributeIdByName=function(b,c){var d=this.ptr;r.prepare();b&&"object"===typeof b&&(b=b.ptr);c=c&&"object"===typeof c?
c.ptr:R(c);return qc(d,b,c)};m.prototype.GetAttributeIdByMetadataEntry=m.prototype.GetAttributeIdByMetadataEntry=function(b,c,d){var g=this.ptr;r.prepare();b&&"object"===typeof b&&(b=b.ptr);c=c&&"object"===typeof c?c.ptr:R(c);d=d&&"object"===typeof d?d.ptr:R(d);return rc(g,b,c,d)};m.prototype.GetAttribute=m.prototype.GetAttribute=function(b,c){var d=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);return D(sc(d,b,c),w)};m.prototype.GetAttributeByUniqueId=m.prototype.GetAttributeByUniqueId=
function(b,c){var d=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);return D(tc(d,b,c),w)};m.prototype.GetMetadata=m.prototype.GetMetadata=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return D(uc(c,b),T)};m.prototype.GetAttributeMetadata=m.prototype.GetAttributeMetadata=function(b,c){var d=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);return D(vc(d,b,c),T)};m.prototype.GetFaceFromMesh=m.prototype.GetFaceFromMesh=function(b,
c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!wc(g,b,c,d)};m.prototype.GetTriangleStripsFromMesh=m.prototype.GetTriangleStripsFromMesh=function(b,c){var d=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);return xc(d,b,c)};m.prototype.GetTrianglesUInt16Array=m.prototype.GetTrianglesUInt16Array=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);
d&&"object"===typeof d&&(d=d.ptr);return!!yc(g,b,c,d)};m.prototype.GetTrianglesUInt32Array=m.prototype.GetTrianglesUInt32Array=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!zc(g,b,c,d)};m.prototype.GetAttributeFloat=m.prototype.GetAttributeFloat=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!Ac(g,b,c,d)};m.prototype.GetAttributeFloatForAllPoints=
m.prototype.GetAttributeFloatForAllPoints=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!Bc(g,b,c,d)};m.prototype.GetAttributeIntForAllPoints=m.prototype.GetAttributeIntForAllPoints=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!Cc(g,b,c,d)};m.prototype.GetAttributeInt8ForAllPoints=m.prototype.GetAttributeInt8ForAllPoints=
function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!Dc(g,b,c,d)};m.prototype.GetAttributeUInt8ForAllPoints=m.prototype.GetAttributeUInt8ForAllPoints=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!Ec(g,b,c,d)};m.prototype.GetAttributeInt16ForAllPoints=m.prototype.GetAttributeInt16ForAllPoints=function(b,c,d){var g=this.ptr;
b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!Fc(g,b,c,d)};m.prototype.GetAttributeUInt16ForAllPoints=m.prototype.GetAttributeUInt16ForAllPoints=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!Gc(g,b,c,d)};m.prototype.GetAttributeInt32ForAllPoints=m.prototype.GetAttributeInt32ForAllPoints=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&
(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!Hc(g,b,c,d)};m.prototype.GetAttributeUInt32ForAllPoints=m.prototype.GetAttributeUInt32ForAllPoints=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!Ic(g,b,c,d)};m.prototype.GetAttributeDataArrayForAllPoints=m.prototype.GetAttributeDataArrayForAllPoints=function(b,c,d,g,u){var X=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&
"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);g&&"object"===typeof g&&(g=g.ptr);u&&"object"===typeof u&&(u=u.ptr);return!!Jc(X,b,c,d,g,u)};m.prototype.SkipAttributeTransform=m.prototype.SkipAttributeTransform=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);Kc(c,b)};m.prototype.GetEncodedGeometryType_Deprecated=m.prototype.GetEncodedGeometryType_Deprecated=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return Lc(c,b)};m.prototype.DecodeBufferToPointCloud=
m.prototype.DecodeBufferToPointCloud=function(b,c){var d=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);return D(Mc(d,b,c),B)};m.prototype.DecodeBufferToMesh=m.prototype.DecodeBufferToMesh=function(b,c){var d=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);return D(Nc(d,b,c),B)};m.prototype.__destroy__=m.prototype.__destroy__=function(){Oc(this.ptr)};(function(){function b(){a.ATTRIBUTE_INVALID_TRANSFORM=Pc();a.ATTRIBUTE_NO_TRANSFORM=Qc();
a.ATTRIBUTE_QUANTIZATION_TRANSFORM=Rc();a.ATTRIBUTE_OCTAHEDRON_TRANSFORM=Sc();a.INVALID=Tc();a.POSITION=Uc();a.NORMAL=Vc();a.COLOR=Wc();a.TEX_COORD=Xc();a.GENERIC=Yc();a.INVALID_GEOMETRY_TYPE=Zc();a.POINT_CLOUD=$c();a.TRIANGULAR_MESH=ad();a.DT_INVALID=bd();a.DT_INT8=cd();a.DT_UINT8=dd();a.DT_INT16=ed();a.DT_UINT16=fd();a.DT_INT32=gd();a.DT_UINT32=hd();a.DT_INT64=id();a.DT_UINT64=jd();a.DT_FLOAT32=kd();a.DT_FLOAT64=ld();a.DT_BOOL=md();a.DT_TYPES_COUNT=nd();a.OK=od();a.DRACO_ERROR=pd();a.IO_ERROR=qd();
a.INVALID_PARAMETER=rd();a.UNSUPPORTED_VERSION=sd();a.UNKNOWN_VERSION=td()}za?b():oa.unshift(b)})();if("function"===typeof a.onModuleParsed)a.onModuleParsed();a.Decoder.prototype.GetEncodedGeometryType=function(b){if(b.__class__&&b.__class__===a.DecoderBuffer)return a.Decoder.prototype.GetEncodedGeometryType_Deprecated(b);if(8>b.byteLength)return a.INVALID_GEOMETRY_TYPE;switch(b[7]){case 0:return a.POINT_CLOUD;case 1:return a.TRIANGULAR_MESH;default:return a.INVALID_GEOMETRY_TYPE}};return n.ready}}();
"object"===typeof exports&&"object"===typeof module?module.exports=DracoDecoderModule:"function"===typeof define&&define.amd?define([],function(){return DracoDecoderModule}):"object"===typeof exports&&(exports.DracoDecoderModule=DracoDecoderModule);

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@@ -1,116 +0,0 @@
var $jscomp=$jscomp||{};$jscomp.scope={};$jscomp.arrayIteratorImpl=function(h){var n=0;return function(){return n<h.length?{done:!1,value:h[n++]}:{done:!0}}};$jscomp.arrayIterator=function(h){return{next:$jscomp.arrayIteratorImpl(h)}};$jscomp.makeIterator=function(h){var n="undefined"!=typeof Symbol&&Symbol.iterator&&h[Symbol.iterator];return n?n.call(h):$jscomp.arrayIterator(h)};$jscomp.ASSUME_ES5=!1;$jscomp.ASSUME_NO_NATIVE_MAP=!1;$jscomp.ASSUME_NO_NATIVE_SET=!1;$jscomp.SIMPLE_FROUND_POLYFILL=!1;
$jscomp.ISOLATE_POLYFILLS=!1;$jscomp.FORCE_POLYFILL_PROMISE=!1;$jscomp.FORCE_POLYFILL_PROMISE_WHEN_NO_UNHANDLED_REJECTION=!1;$jscomp.getGlobal=function(h){h=["object"==typeof globalThis&&globalThis,h,"object"==typeof window&&window,"object"==typeof self&&self,"object"==typeof global&&global];for(var n=0;n<h.length;++n){var k=h[n];if(k&&k.Math==Math)return k}throw Error("Cannot find global object");};$jscomp.global=$jscomp.getGlobal(this);
$jscomp.defineProperty=$jscomp.ASSUME_ES5||"function"==typeof Object.defineProperties?Object.defineProperty:function(h,n,k){if(h==Array.prototype||h==Object.prototype)return h;h[n]=k.value;return h};$jscomp.IS_SYMBOL_NATIVE="function"===typeof Symbol&&"symbol"===typeof Symbol("x");$jscomp.TRUST_ES6_POLYFILLS=!$jscomp.ISOLATE_POLYFILLS||$jscomp.IS_SYMBOL_NATIVE;$jscomp.polyfills={};$jscomp.propertyToPolyfillSymbol={};$jscomp.POLYFILL_PREFIX="$jscp$";
var $jscomp$lookupPolyfilledValue=function(h,n){var k=$jscomp.propertyToPolyfillSymbol[n];if(null==k)return h[n];k=h[k];return void 0!==k?k:h[n]};$jscomp.polyfill=function(h,n,k,p){n&&($jscomp.ISOLATE_POLYFILLS?$jscomp.polyfillIsolated(h,n,k,p):$jscomp.polyfillUnisolated(h,n,k,p))};
$jscomp.polyfillUnisolated=function(h,n,k,p){k=$jscomp.global;h=h.split(".");for(p=0;p<h.length-1;p++){var l=h[p];if(!(l in k))return;k=k[l]}h=h[h.length-1];p=k[h];n=n(p);n!=p&&null!=n&&$jscomp.defineProperty(k,h,{configurable:!0,writable:!0,value:n})};
$jscomp.polyfillIsolated=function(h,n,k,p){var l=h.split(".");h=1===l.length;p=l[0];p=!h&&p in $jscomp.polyfills?$jscomp.polyfills:$jscomp.global;for(var y=0;y<l.length-1;y++){var f=l[y];if(!(f in p))return;p=p[f]}l=l[l.length-1];k=$jscomp.IS_SYMBOL_NATIVE&&"es6"===k?p[l]:null;n=n(k);null!=n&&(h?$jscomp.defineProperty($jscomp.polyfills,l,{configurable:!0,writable:!0,value:n}):n!==k&&(void 0===$jscomp.propertyToPolyfillSymbol[l]&&(k=1E9*Math.random()>>>0,$jscomp.propertyToPolyfillSymbol[l]=$jscomp.IS_SYMBOL_NATIVE?
$jscomp.global.Symbol(l):$jscomp.POLYFILL_PREFIX+k+"$"+l),$jscomp.defineProperty(p,$jscomp.propertyToPolyfillSymbol[l],{configurable:!0,writable:!0,value:n})))};
$jscomp.polyfill("Promise",function(h){function n(){this.batch_=null}function k(f){return f instanceof l?f:new l(function(q,u){q(f)})}if(h&&(!($jscomp.FORCE_POLYFILL_PROMISE||$jscomp.FORCE_POLYFILL_PROMISE_WHEN_NO_UNHANDLED_REJECTION&&"undefined"===typeof $jscomp.global.PromiseRejectionEvent)||!$jscomp.global.Promise||-1===$jscomp.global.Promise.toString().indexOf("[native code]")))return h;n.prototype.asyncExecute=function(f){if(null==this.batch_){this.batch_=[];var q=this;this.asyncExecuteFunction(function(){q.executeBatch_()})}this.batch_.push(f)};
var p=$jscomp.global.setTimeout;n.prototype.asyncExecuteFunction=function(f){p(f,0)};n.prototype.executeBatch_=function(){for(;this.batch_&&this.batch_.length;){var f=this.batch_;this.batch_=[];for(var q=0;q<f.length;++q){var u=f[q];f[q]=null;try{u()}catch(A){this.asyncThrow_(A)}}}this.batch_=null};n.prototype.asyncThrow_=function(f){this.asyncExecuteFunction(function(){throw f;})};var l=function(f){this.state_=0;this.result_=void 0;this.onSettledCallbacks_=[];this.isRejectionHandled_=!1;var q=this.createResolveAndReject_();
try{f(q.resolve,q.reject)}catch(u){q.reject(u)}};l.prototype.createResolveAndReject_=function(){function f(A){return function(F){u||(u=!0,A.call(q,F))}}var q=this,u=!1;return{resolve:f(this.resolveTo_),reject:f(this.reject_)}};l.prototype.resolveTo_=function(f){if(f===this)this.reject_(new TypeError("A Promise cannot resolve to itself"));else if(f instanceof l)this.settleSameAsPromise_(f);else{a:switch(typeof f){case "object":var q=null!=f;break a;case "function":q=!0;break a;default:q=!1}q?this.resolveToNonPromiseObj_(f):
this.fulfill_(f)}};l.prototype.resolveToNonPromiseObj_=function(f){var q=void 0;try{q=f.then}catch(u){this.reject_(u);return}"function"==typeof q?this.settleSameAsThenable_(q,f):this.fulfill_(f)};l.prototype.reject_=function(f){this.settle_(2,f)};l.prototype.fulfill_=function(f){this.settle_(1,f)};l.prototype.settle_=function(f,q){if(0!=this.state_)throw Error("Cannot settle("+f+", "+q+"): Promise already settled in state"+this.state_);this.state_=f;this.result_=q;2===this.state_&&this.scheduleUnhandledRejectionCheck_();
this.executeOnSettledCallbacks_()};l.prototype.scheduleUnhandledRejectionCheck_=function(){var f=this;p(function(){if(f.notifyUnhandledRejection_()){var q=$jscomp.global.console;"undefined"!==typeof q&&q.error(f.result_)}},1)};l.prototype.notifyUnhandledRejection_=function(){if(this.isRejectionHandled_)return!1;var f=$jscomp.global.CustomEvent,q=$jscomp.global.Event,u=$jscomp.global.dispatchEvent;if("undefined"===typeof u)return!0;"function"===typeof f?f=new f("unhandledrejection",{cancelable:!0}):
"function"===typeof q?f=new q("unhandledrejection",{cancelable:!0}):(f=$jscomp.global.document.createEvent("CustomEvent"),f.initCustomEvent("unhandledrejection",!1,!0,f));f.promise=this;f.reason=this.result_;return u(f)};l.prototype.executeOnSettledCallbacks_=function(){if(null!=this.onSettledCallbacks_){for(var f=0;f<this.onSettledCallbacks_.length;++f)y.asyncExecute(this.onSettledCallbacks_[f]);this.onSettledCallbacks_=null}};var y=new n;l.prototype.settleSameAsPromise_=function(f){var q=this.createResolveAndReject_();
f.callWhenSettled_(q.resolve,q.reject)};l.prototype.settleSameAsThenable_=function(f,q){var u=this.createResolveAndReject_();try{f.call(q,u.resolve,u.reject)}catch(A){u.reject(A)}};l.prototype.then=function(f,q){function u(w,B){return"function"==typeof w?function(R){try{A(w(R))}catch(Z){F(Z)}}:B}var A,F,v=new l(function(w,B){A=w;F=B});this.callWhenSettled_(u(f,A),u(q,F));return v};l.prototype.catch=function(f){return this.then(void 0,f)};l.prototype.callWhenSettled_=function(f,q){function u(){switch(A.state_){case 1:f(A.result_);
break;case 2:q(A.result_);break;default:throw Error("Unexpected state: "+A.state_);}}var A=this;null==this.onSettledCallbacks_?y.asyncExecute(u):this.onSettledCallbacks_.push(u);this.isRejectionHandled_=!0};l.resolve=k;l.reject=function(f){return new l(function(q,u){u(f)})};l.race=function(f){return new l(function(q,u){for(var A=$jscomp.makeIterator(f),F=A.next();!F.done;F=A.next())k(F.value).callWhenSettled_(q,u)})};l.all=function(f){var q=$jscomp.makeIterator(f),u=q.next();return u.done?k([]):new l(function(A,
F){function v(R){return function(Z){w[R]=Z;B--;0==B&&A(w)}}var w=[],B=0;do w.push(void 0),B++,k(u.value).callWhenSettled_(v(w.length-1),F),u=q.next();while(!u.done)})};return l},"es6","es3");$jscomp.owns=function(h,n){return Object.prototype.hasOwnProperty.call(h,n)};$jscomp.assign=$jscomp.TRUST_ES6_POLYFILLS&&"function"==typeof Object.assign?Object.assign:function(h,n){for(var k=1;k<arguments.length;k++){var p=arguments[k];if(p)for(var l in p)$jscomp.owns(p,l)&&(h[l]=p[l])}return h};
$jscomp.polyfill("Object.assign",function(h){return h||$jscomp.assign},"es6","es3");$jscomp.checkStringArgs=function(h,n,k){if(null==h)throw new TypeError("The 'this' value for String.prototype."+k+" must not be null or undefined");if(n instanceof RegExp)throw new TypeError("First argument to String.prototype."+k+" must not be a regular expression");return h+""};
$jscomp.polyfill("String.prototype.startsWith",function(h){return h?h:function(n,k){var p=$jscomp.checkStringArgs(this,n,"startsWith");n+="";var l=p.length,y=n.length;k=Math.max(0,Math.min(k|0,p.length));for(var f=0;f<y&&k<l;)if(p[k++]!=n[f++])return!1;return f>=y}},"es6","es3");
$jscomp.polyfill("Array.prototype.copyWithin",function(h){function n(k){k=Number(k);return Infinity===k||-Infinity===k?k:k|0}return h?h:function(k,p,l){var y=this.length;k=n(k);p=n(p);l=void 0===l?y:n(l);k=0>k?Math.max(y+k,0):Math.min(k,y);p=0>p?Math.max(y+p,0):Math.min(p,y);l=0>l?Math.max(y+l,0):Math.min(l,y);if(k<p)for(;p<l;)p in this?this[k++]=this[p++]:(delete this[k++],p++);else for(l=Math.min(l,y+p-k),k+=l-p;l>p;)--l in this?this[--k]=this[l]:delete this[--k];return this}},"es6","es3");
$jscomp.typedArrayCopyWithin=function(h){return h?h:Array.prototype.copyWithin};$jscomp.polyfill("Int8Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Uint8Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Uint8ClampedArray.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Int16Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");
$jscomp.polyfill("Uint16Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Int32Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Uint32Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Float32Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Float64Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");
var DracoDecoderModule=function(){var h="undefined"!==typeof document&&document.currentScript?document.currentScript.src:void 0;"undefined"!==typeof __filename&&(h=h||__filename);return function(n){function k(e){return a.locateFile?a.locateFile(e,U):U+e}function p(e,b){if(e){var c=ia;var d=e+b;for(b=e;c[b]&&!(b>=d);)++b;if(16<b-e&&c.buffer&&ra)c=ra.decode(c.subarray(e,b));else{for(d="";e<b;){var g=c[e++];if(g&128){var t=c[e++]&63;if(192==(g&224))d+=String.fromCharCode((g&31)<<6|t);else{var aa=c[e++]&
63;g=224==(g&240)?(g&15)<<12|t<<6|aa:(g&7)<<18|t<<12|aa<<6|c[e++]&63;65536>g?d+=String.fromCharCode(g):(g-=65536,d+=String.fromCharCode(55296|g>>10,56320|g&1023))}}else d+=String.fromCharCode(g)}c=d}}else c="";return c}function l(){var e=ja.buffer;a.HEAP8=W=new Int8Array(e);a.HEAP16=new Int16Array(e);a.HEAP32=ca=new Int32Array(e);a.HEAPU8=ia=new Uint8Array(e);a.HEAPU16=new Uint16Array(e);a.HEAPU32=Y=new Uint32Array(e);a.HEAPF32=new Float32Array(e);a.HEAPF64=new Float64Array(e)}function y(e){if(a.onAbort)a.onAbort(e);
e="Aborted("+e+")";da(e);sa=!0;e=new WebAssembly.RuntimeError(e+". Build with -sASSERTIONS for more info.");ka(e);throw e;}function f(e){try{if(e==P&&ea)return new Uint8Array(ea);if(ma)return ma(e);throw"both async and sync fetching of the wasm failed";}catch(b){y(b)}}function q(){if(!ea&&(ta||fa)){if("function"==typeof fetch&&!P.startsWith("file://"))return fetch(P,{credentials:"same-origin"}).then(function(e){if(!e.ok)throw"failed to load wasm binary file at '"+P+"'";return e.arrayBuffer()}).catch(function(){return f(P)});
if(na)return new Promise(function(e,b){na(P,function(c){e(new Uint8Array(c))},b)})}return Promise.resolve().then(function(){return f(P)})}function u(e){for(;0<e.length;)e.shift()(a)}function A(e){this.excPtr=e;this.ptr=e-24;this.set_type=function(b){Y[this.ptr+4>>2]=b};this.get_type=function(){return Y[this.ptr+4>>2]};this.set_destructor=function(b){Y[this.ptr+8>>2]=b};this.get_destructor=function(){return Y[this.ptr+8>>2]};this.set_refcount=function(b){ca[this.ptr>>2]=b};this.set_caught=function(b){W[this.ptr+
12>>0]=b?1:0};this.get_caught=function(){return 0!=W[this.ptr+12>>0]};this.set_rethrown=function(b){W[this.ptr+13>>0]=b?1:0};this.get_rethrown=function(){return 0!=W[this.ptr+13>>0]};this.init=function(b,c){this.set_adjusted_ptr(0);this.set_type(b);this.set_destructor(c);this.set_refcount(0);this.set_caught(!1);this.set_rethrown(!1)};this.add_ref=function(){ca[this.ptr>>2]+=1};this.release_ref=function(){var b=ca[this.ptr>>2];ca[this.ptr>>2]=b-1;return 1===b};this.set_adjusted_ptr=function(b){Y[this.ptr+
16>>2]=b};this.get_adjusted_ptr=function(){return Y[this.ptr+16>>2]};this.get_exception_ptr=function(){if(ua(this.get_type()))return Y[this.excPtr>>2];var b=this.get_adjusted_ptr();return 0!==b?b:this.excPtr}}function F(){function e(){if(!la&&(la=!0,a.calledRun=!0,!sa)){va=!0;u(oa);wa(a);if(a.onRuntimeInitialized)a.onRuntimeInitialized();if(a.postRun)for("function"==typeof a.postRun&&(a.postRun=[a.postRun]);a.postRun.length;)xa.unshift(a.postRun.shift());u(xa)}}if(!(0<ba)){if(a.preRun)for("function"==
typeof a.preRun&&(a.preRun=[a.preRun]);a.preRun.length;)ya.unshift(a.preRun.shift());u(ya);0<ba||(a.setStatus?(a.setStatus("Running..."),setTimeout(function(){setTimeout(function(){a.setStatus("")},1);e()},1)):e())}}function v(){}function w(e){return(e||v).__cache__}function B(e,b){var c=w(b),d=c[e];if(d)return d;d=Object.create((b||v).prototype);d.ptr=e;return c[e]=d}function R(e){if("string"===typeof e){for(var b=0,c=0;c<e.length;++c){var d=e.charCodeAt(c);127>=d?b++:2047>=d?b+=2:55296<=d&&57343>=
d?(b+=4,++c):b+=3}b=Array(b+1);c=0;d=b.length;if(0<d){d=c+d-1;for(var g=0;g<e.length;++g){var t=e.charCodeAt(g);if(55296<=t&&57343>=t){var aa=e.charCodeAt(++g);t=65536+((t&1023)<<10)|aa&1023}if(127>=t){if(c>=d)break;b[c++]=t}else{if(2047>=t){if(c+1>=d)break;b[c++]=192|t>>6}else{if(65535>=t){if(c+2>=d)break;b[c++]=224|t>>12}else{if(c+3>=d)break;b[c++]=240|t>>18;b[c++]=128|t>>12&63}b[c++]=128|t>>6&63}b[c++]=128|t&63}}b[c]=0}e=r.alloc(b,W);r.copy(b,W,e);return e}return e}function Z(e){if("object"===
typeof e){var b=r.alloc(e,W);r.copy(e,W,b);return b}return e}function X(){throw"cannot construct a VoidPtr, no constructor in IDL";}function S(){this.ptr=za();w(S)[this.ptr]=this}function Q(){this.ptr=Aa();w(Q)[this.ptr]=this}function V(){this.ptr=Ba();w(V)[this.ptr]=this}function x(){this.ptr=Ca();w(x)[this.ptr]=this}function D(){this.ptr=Da();w(D)[this.ptr]=this}function G(){this.ptr=Ea();w(G)[this.ptr]=this}function H(){this.ptr=Fa();w(H)[this.ptr]=this}function E(){this.ptr=Ga();w(E)[this.ptr]=
this}function T(){this.ptr=Ha();w(T)[this.ptr]=this}function C(){throw"cannot construct a Status, no constructor in IDL";}function I(){this.ptr=Ia();w(I)[this.ptr]=this}function J(){this.ptr=Ja();w(J)[this.ptr]=this}function K(){this.ptr=Ka();w(K)[this.ptr]=this}function L(){this.ptr=La();w(L)[this.ptr]=this}function M(){this.ptr=Ma();w(M)[this.ptr]=this}function N(){this.ptr=Na();w(N)[this.ptr]=this}function O(){this.ptr=Oa();w(O)[this.ptr]=this}function z(){this.ptr=Pa();w(z)[this.ptr]=this}function m(){this.ptr=
Qa();w(m)[this.ptr]=this}n=void 0===n?{}:n;var a="undefined"!=typeof n?n:{},wa,ka;a.ready=new Promise(function(e,b){wa=e;ka=b});var Ra=!1,Sa=!1;a.onRuntimeInitialized=function(){Ra=!0;if(Sa&&"function"===typeof a.onModuleLoaded)a.onModuleLoaded(a)};a.onModuleParsed=function(){Sa=!0;if(Ra&&"function"===typeof a.onModuleLoaded)a.onModuleLoaded(a)};a.isVersionSupported=function(e){if("string"!==typeof e)return!1;e=e.split(".");return 2>e.length||3<e.length?!1:1==e[0]&&0<=e[1]&&5>=e[1]?!0:0!=e[0]||10<
e[1]?!1:!0};var Ta=Object.assign({},a),ta="object"==typeof window,fa="function"==typeof importScripts,Ua="object"==typeof process&&"object"==typeof process.versions&&"string"==typeof process.versions.node,U="";if(Ua){var Va=require("fs"),pa=require("path");U=fa?pa.dirname(U)+"/":__dirname+"/";var Wa=function(e,b){e=e.startsWith("file://")?new URL(e):pa.normalize(e);return Va.readFileSync(e,b?void 0:"utf8")};var ma=function(e){e=Wa(e,!0);e.buffer||(e=new Uint8Array(e));return e};var na=function(e,
b,c){e=e.startsWith("file://")?new URL(e):pa.normalize(e);Va.readFile(e,function(d,g){d?c(d):b(g.buffer)})};1<process.argv.length&&process.argv[1].replace(/\\/g,"/");process.argv.slice(2);a.inspect=function(){return"[Emscripten Module object]"}}else if(ta||fa)fa?U=self.location.href:"undefined"!=typeof document&&document.currentScript&&(U=document.currentScript.src),h&&(U=h),U=0!==U.indexOf("blob:")?U.substr(0,U.replace(/[?#].*/,"").lastIndexOf("/")+1):"",Wa=function(e){var b=new XMLHttpRequest;b.open("GET",
e,!1);b.send(null);return b.responseText},fa&&(ma=function(e){var b=new XMLHttpRequest;b.open("GET",e,!1);b.responseType="arraybuffer";b.send(null);return new Uint8Array(b.response)}),na=function(e,b,c){var d=new XMLHttpRequest;d.open("GET",e,!0);d.responseType="arraybuffer";d.onload=function(){200==d.status||0==d.status&&d.response?b(d.response):c()};d.onerror=c;d.send(null)};a.print||console.log.bind(console);var da=a.printErr||console.warn.bind(console);Object.assign(a,Ta);Ta=null;var ea;a.wasmBinary&&
(ea=a.wasmBinary);"object"!=typeof WebAssembly&&y("no native wasm support detected");var ja,sa=!1,ra="undefined"!=typeof TextDecoder?new TextDecoder("utf8"):void 0,W,ia,ca,Y,ya=[],oa=[],xa=[],va=!1,ba=0,qa=null,ha=null;var P="draco_decoder_gltf.wasm";P.startsWith("data:application/octet-stream;base64,")||(P=k(P));var pd=0,qd={b:function(e,b,c){(new A(e)).init(b,c);pd++;throw e;},a:function(){y("")},d:function(e,b,c){ia.copyWithin(e,b,b+c)},c:function(e){var b=ia.length;e>>>=0;if(2147483648<e)return!1;
for(var c=1;4>=c;c*=2){var d=b*(1+.2/c);d=Math.min(d,e+100663296);var g=Math;d=Math.max(e,d);g=g.min.call(g,2147483648,d+(65536-d%65536)%65536);a:{d=ja.buffer;try{ja.grow(g-d.byteLength+65535>>>16);l();var t=1;break a}catch(aa){}t=void 0}if(t)return!0}return!1}};(function(){function e(g,t){a.asm=g.exports;ja=a.asm.e;l();oa.unshift(a.asm.f);ba--;a.monitorRunDependencies&&a.monitorRunDependencies(ba);0==ba&&(null!==qa&&(clearInterval(qa),qa=null),ha&&(g=ha,ha=null,g()))}function b(g){e(g.instance)}
function c(g){return q().then(function(t){return WebAssembly.instantiate(t,d)}).then(function(t){return t}).then(g,function(t){da("failed to asynchronously prepare wasm: "+t);y(t)})}var d={a:qd};ba++;a.monitorRunDependencies&&a.monitorRunDependencies(ba);if(a.instantiateWasm)try{return a.instantiateWasm(d,e)}catch(g){da("Module.instantiateWasm callback failed with error: "+g),ka(g)}(function(){return ea||"function"!=typeof WebAssembly.instantiateStreaming||P.startsWith("data:application/octet-stream;base64,")||
P.startsWith("file://")||Ua||"function"!=typeof fetch?c(b):fetch(P,{credentials:"same-origin"}).then(function(g){return WebAssembly.instantiateStreaming(g,d).then(b,function(t){da("wasm streaming compile failed: "+t);da("falling back to ArrayBuffer instantiation");return c(b)})})})().catch(ka);return{}})();var Xa=a._emscripten_bind_VoidPtr___destroy___0=function(){return(Xa=a._emscripten_bind_VoidPtr___destroy___0=a.asm.h).apply(null,arguments)},za=a._emscripten_bind_DecoderBuffer_DecoderBuffer_0=
function(){return(za=a._emscripten_bind_DecoderBuffer_DecoderBuffer_0=a.asm.i).apply(null,arguments)},Ya=a._emscripten_bind_DecoderBuffer_Init_2=function(){return(Ya=a._emscripten_bind_DecoderBuffer_Init_2=a.asm.j).apply(null,arguments)},Za=a._emscripten_bind_DecoderBuffer___destroy___0=function(){return(Za=a._emscripten_bind_DecoderBuffer___destroy___0=a.asm.k).apply(null,arguments)},Aa=a._emscripten_bind_AttributeTransformData_AttributeTransformData_0=function(){return(Aa=a._emscripten_bind_AttributeTransformData_AttributeTransformData_0=
a.asm.l).apply(null,arguments)},$a=a._emscripten_bind_AttributeTransformData_transform_type_0=function(){return($a=a._emscripten_bind_AttributeTransformData_transform_type_0=a.asm.m).apply(null,arguments)},ab=a._emscripten_bind_AttributeTransformData___destroy___0=function(){return(ab=a._emscripten_bind_AttributeTransformData___destroy___0=a.asm.n).apply(null,arguments)},Ba=a._emscripten_bind_GeometryAttribute_GeometryAttribute_0=function(){return(Ba=a._emscripten_bind_GeometryAttribute_GeometryAttribute_0=
a.asm.o).apply(null,arguments)},bb=a._emscripten_bind_GeometryAttribute___destroy___0=function(){return(bb=a._emscripten_bind_GeometryAttribute___destroy___0=a.asm.p).apply(null,arguments)},Ca=a._emscripten_bind_PointAttribute_PointAttribute_0=function(){return(Ca=a._emscripten_bind_PointAttribute_PointAttribute_0=a.asm.q).apply(null,arguments)},cb=a._emscripten_bind_PointAttribute_size_0=function(){return(cb=a._emscripten_bind_PointAttribute_size_0=a.asm.r).apply(null,arguments)},db=a._emscripten_bind_PointAttribute_GetAttributeTransformData_0=
function(){return(db=a._emscripten_bind_PointAttribute_GetAttributeTransformData_0=a.asm.s).apply(null,arguments)},eb=a._emscripten_bind_PointAttribute_attribute_type_0=function(){return(eb=a._emscripten_bind_PointAttribute_attribute_type_0=a.asm.t).apply(null,arguments)},fb=a._emscripten_bind_PointAttribute_data_type_0=function(){return(fb=a._emscripten_bind_PointAttribute_data_type_0=a.asm.u).apply(null,arguments)},gb=a._emscripten_bind_PointAttribute_num_components_0=function(){return(gb=a._emscripten_bind_PointAttribute_num_components_0=
a.asm.v).apply(null,arguments)},hb=a._emscripten_bind_PointAttribute_normalized_0=function(){return(hb=a._emscripten_bind_PointAttribute_normalized_0=a.asm.w).apply(null,arguments)},ib=a._emscripten_bind_PointAttribute_byte_stride_0=function(){return(ib=a._emscripten_bind_PointAttribute_byte_stride_0=a.asm.x).apply(null,arguments)},jb=a._emscripten_bind_PointAttribute_byte_offset_0=function(){return(jb=a._emscripten_bind_PointAttribute_byte_offset_0=a.asm.y).apply(null,arguments)},kb=a._emscripten_bind_PointAttribute_unique_id_0=
function(){return(kb=a._emscripten_bind_PointAttribute_unique_id_0=a.asm.z).apply(null,arguments)},lb=a._emscripten_bind_PointAttribute___destroy___0=function(){return(lb=a._emscripten_bind_PointAttribute___destroy___0=a.asm.A).apply(null,arguments)},Da=a._emscripten_bind_AttributeQuantizationTransform_AttributeQuantizationTransform_0=function(){return(Da=a._emscripten_bind_AttributeQuantizationTransform_AttributeQuantizationTransform_0=a.asm.B).apply(null,arguments)},mb=a._emscripten_bind_AttributeQuantizationTransform_InitFromAttribute_1=
function(){return(mb=a._emscripten_bind_AttributeQuantizationTransform_InitFromAttribute_1=a.asm.C).apply(null,arguments)},nb=a._emscripten_bind_AttributeQuantizationTransform_quantization_bits_0=function(){return(nb=a._emscripten_bind_AttributeQuantizationTransform_quantization_bits_0=a.asm.D).apply(null,arguments)},ob=a._emscripten_bind_AttributeQuantizationTransform_min_value_1=function(){return(ob=a._emscripten_bind_AttributeQuantizationTransform_min_value_1=a.asm.E).apply(null,arguments)},pb=
a._emscripten_bind_AttributeQuantizationTransform_range_0=function(){return(pb=a._emscripten_bind_AttributeQuantizationTransform_range_0=a.asm.F).apply(null,arguments)},qb=a._emscripten_bind_AttributeQuantizationTransform___destroy___0=function(){return(qb=a._emscripten_bind_AttributeQuantizationTransform___destroy___0=a.asm.G).apply(null,arguments)},Ea=a._emscripten_bind_AttributeOctahedronTransform_AttributeOctahedronTransform_0=function(){return(Ea=a._emscripten_bind_AttributeOctahedronTransform_AttributeOctahedronTransform_0=
a.asm.H).apply(null,arguments)},rb=a._emscripten_bind_AttributeOctahedronTransform_InitFromAttribute_1=function(){return(rb=a._emscripten_bind_AttributeOctahedronTransform_InitFromAttribute_1=a.asm.I).apply(null,arguments)},sb=a._emscripten_bind_AttributeOctahedronTransform_quantization_bits_0=function(){return(sb=a._emscripten_bind_AttributeOctahedronTransform_quantization_bits_0=a.asm.J).apply(null,arguments)},tb=a._emscripten_bind_AttributeOctahedronTransform___destroy___0=function(){return(tb=
a._emscripten_bind_AttributeOctahedronTransform___destroy___0=a.asm.K).apply(null,arguments)},Fa=a._emscripten_bind_PointCloud_PointCloud_0=function(){return(Fa=a._emscripten_bind_PointCloud_PointCloud_0=a.asm.L).apply(null,arguments)},ub=a._emscripten_bind_PointCloud_num_attributes_0=function(){return(ub=a._emscripten_bind_PointCloud_num_attributes_0=a.asm.M).apply(null,arguments)},vb=a._emscripten_bind_PointCloud_num_points_0=function(){return(vb=a._emscripten_bind_PointCloud_num_points_0=a.asm.N).apply(null,
arguments)},wb=a._emscripten_bind_PointCloud___destroy___0=function(){return(wb=a._emscripten_bind_PointCloud___destroy___0=a.asm.O).apply(null,arguments)},Ga=a._emscripten_bind_Mesh_Mesh_0=function(){return(Ga=a._emscripten_bind_Mesh_Mesh_0=a.asm.P).apply(null,arguments)},xb=a._emscripten_bind_Mesh_num_faces_0=function(){return(xb=a._emscripten_bind_Mesh_num_faces_0=a.asm.Q).apply(null,arguments)},yb=a._emscripten_bind_Mesh_num_attributes_0=function(){return(yb=a._emscripten_bind_Mesh_num_attributes_0=
a.asm.R).apply(null,arguments)},zb=a._emscripten_bind_Mesh_num_points_0=function(){return(zb=a._emscripten_bind_Mesh_num_points_0=a.asm.S).apply(null,arguments)},Ab=a._emscripten_bind_Mesh___destroy___0=function(){return(Ab=a._emscripten_bind_Mesh___destroy___0=a.asm.T).apply(null,arguments)},Ha=a._emscripten_bind_Metadata_Metadata_0=function(){return(Ha=a._emscripten_bind_Metadata_Metadata_0=a.asm.U).apply(null,arguments)},Bb=a._emscripten_bind_Metadata___destroy___0=function(){return(Bb=a._emscripten_bind_Metadata___destroy___0=
a.asm.V).apply(null,arguments)},Cb=a._emscripten_bind_Status_code_0=function(){return(Cb=a._emscripten_bind_Status_code_0=a.asm.W).apply(null,arguments)},Db=a._emscripten_bind_Status_ok_0=function(){return(Db=a._emscripten_bind_Status_ok_0=a.asm.X).apply(null,arguments)},Eb=a._emscripten_bind_Status_error_msg_0=function(){return(Eb=a._emscripten_bind_Status_error_msg_0=a.asm.Y).apply(null,arguments)},Fb=a._emscripten_bind_Status___destroy___0=function(){return(Fb=a._emscripten_bind_Status___destroy___0=
a.asm.Z).apply(null,arguments)},Ia=a._emscripten_bind_DracoFloat32Array_DracoFloat32Array_0=function(){return(Ia=a._emscripten_bind_DracoFloat32Array_DracoFloat32Array_0=a.asm._).apply(null,arguments)},Gb=a._emscripten_bind_DracoFloat32Array_GetValue_1=function(){return(Gb=a._emscripten_bind_DracoFloat32Array_GetValue_1=a.asm.$).apply(null,arguments)},Hb=a._emscripten_bind_DracoFloat32Array_size_0=function(){return(Hb=a._emscripten_bind_DracoFloat32Array_size_0=a.asm.aa).apply(null,arguments)},Ib=
a._emscripten_bind_DracoFloat32Array___destroy___0=function(){return(Ib=a._emscripten_bind_DracoFloat32Array___destroy___0=a.asm.ba).apply(null,arguments)},Ja=a._emscripten_bind_DracoInt8Array_DracoInt8Array_0=function(){return(Ja=a._emscripten_bind_DracoInt8Array_DracoInt8Array_0=a.asm.ca).apply(null,arguments)},Jb=a._emscripten_bind_DracoInt8Array_GetValue_1=function(){return(Jb=a._emscripten_bind_DracoInt8Array_GetValue_1=a.asm.da).apply(null,arguments)},Kb=a._emscripten_bind_DracoInt8Array_size_0=
function(){return(Kb=a._emscripten_bind_DracoInt8Array_size_0=a.asm.ea).apply(null,arguments)},Lb=a._emscripten_bind_DracoInt8Array___destroy___0=function(){return(Lb=a._emscripten_bind_DracoInt8Array___destroy___0=a.asm.fa).apply(null,arguments)},Ka=a._emscripten_bind_DracoUInt8Array_DracoUInt8Array_0=function(){return(Ka=a._emscripten_bind_DracoUInt8Array_DracoUInt8Array_0=a.asm.ga).apply(null,arguments)},Mb=a._emscripten_bind_DracoUInt8Array_GetValue_1=function(){return(Mb=a._emscripten_bind_DracoUInt8Array_GetValue_1=
a.asm.ha).apply(null,arguments)},Nb=a._emscripten_bind_DracoUInt8Array_size_0=function(){return(Nb=a._emscripten_bind_DracoUInt8Array_size_0=a.asm.ia).apply(null,arguments)},Ob=a._emscripten_bind_DracoUInt8Array___destroy___0=function(){return(Ob=a._emscripten_bind_DracoUInt8Array___destroy___0=a.asm.ja).apply(null,arguments)},La=a._emscripten_bind_DracoInt16Array_DracoInt16Array_0=function(){return(La=a._emscripten_bind_DracoInt16Array_DracoInt16Array_0=a.asm.ka).apply(null,arguments)},Pb=a._emscripten_bind_DracoInt16Array_GetValue_1=
function(){return(Pb=a._emscripten_bind_DracoInt16Array_GetValue_1=a.asm.la).apply(null,arguments)},Qb=a._emscripten_bind_DracoInt16Array_size_0=function(){return(Qb=a._emscripten_bind_DracoInt16Array_size_0=a.asm.ma).apply(null,arguments)},Rb=a._emscripten_bind_DracoInt16Array___destroy___0=function(){return(Rb=a._emscripten_bind_DracoInt16Array___destroy___0=a.asm.na).apply(null,arguments)},Ma=a._emscripten_bind_DracoUInt16Array_DracoUInt16Array_0=function(){return(Ma=a._emscripten_bind_DracoUInt16Array_DracoUInt16Array_0=
a.asm.oa).apply(null,arguments)},Sb=a._emscripten_bind_DracoUInt16Array_GetValue_1=function(){return(Sb=a._emscripten_bind_DracoUInt16Array_GetValue_1=a.asm.pa).apply(null,arguments)},Tb=a._emscripten_bind_DracoUInt16Array_size_0=function(){return(Tb=a._emscripten_bind_DracoUInt16Array_size_0=a.asm.qa).apply(null,arguments)},Ub=a._emscripten_bind_DracoUInt16Array___destroy___0=function(){return(Ub=a._emscripten_bind_DracoUInt16Array___destroy___0=a.asm.ra).apply(null,arguments)},Na=a._emscripten_bind_DracoInt32Array_DracoInt32Array_0=
function(){return(Na=a._emscripten_bind_DracoInt32Array_DracoInt32Array_0=a.asm.sa).apply(null,arguments)},Vb=a._emscripten_bind_DracoInt32Array_GetValue_1=function(){return(Vb=a._emscripten_bind_DracoInt32Array_GetValue_1=a.asm.ta).apply(null,arguments)},Wb=a._emscripten_bind_DracoInt32Array_size_0=function(){return(Wb=a._emscripten_bind_DracoInt32Array_size_0=a.asm.ua).apply(null,arguments)},Xb=a._emscripten_bind_DracoInt32Array___destroy___0=function(){return(Xb=a._emscripten_bind_DracoInt32Array___destroy___0=
a.asm.va).apply(null,arguments)},Oa=a._emscripten_bind_DracoUInt32Array_DracoUInt32Array_0=function(){return(Oa=a._emscripten_bind_DracoUInt32Array_DracoUInt32Array_0=a.asm.wa).apply(null,arguments)},Yb=a._emscripten_bind_DracoUInt32Array_GetValue_1=function(){return(Yb=a._emscripten_bind_DracoUInt32Array_GetValue_1=a.asm.xa).apply(null,arguments)},Zb=a._emscripten_bind_DracoUInt32Array_size_0=function(){return(Zb=a._emscripten_bind_DracoUInt32Array_size_0=a.asm.ya).apply(null,arguments)},$b=a._emscripten_bind_DracoUInt32Array___destroy___0=
function(){return($b=a._emscripten_bind_DracoUInt32Array___destroy___0=a.asm.za).apply(null,arguments)},Pa=a._emscripten_bind_MetadataQuerier_MetadataQuerier_0=function(){return(Pa=a._emscripten_bind_MetadataQuerier_MetadataQuerier_0=a.asm.Aa).apply(null,arguments)},ac=a._emscripten_bind_MetadataQuerier_HasEntry_2=function(){return(ac=a._emscripten_bind_MetadataQuerier_HasEntry_2=a.asm.Ba).apply(null,arguments)},bc=a._emscripten_bind_MetadataQuerier_GetIntEntry_2=function(){return(bc=a._emscripten_bind_MetadataQuerier_GetIntEntry_2=
a.asm.Ca).apply(null,arguments)},cc=a._emscripten_bind_MetadataQuerier_GetIntEntryArray_3=function(){return(cc=a._emscripten_bind_MetadataQuerier_GetIntEntryArray_3=a.asm.Da).apply(null,arguments)},dc=a._emscripten_bind_MetadataQuerier_GetDoubleEntry_2=function(){return(dc=a._emscripten_bind_MetadataQuerier_GetDoubleEntry_2=a.asm.Ea).apply(null,arguments)},ec=a._emscripten_bind_MetadataQuerier_GetStringEntry_2=function(){return(ec=a._emscripten_bind_MetadataQuerier_GetStringEntry_2=a.asm.Fa).apply(null,
arguments)},fc=a._emscripten_bind_MetadataQuerier_NumEntries_1=function(){return(fc=a._emscripten_bind_MetadataQuerier_NumEntries_1=a.asm.Ga).apply(null,arguments)},gc=a._emscripten_bind_MetadataQuerier_GetEntryName_2=function(){return(gc=a._emscripten_bind_MetadataQuerier_GetEntryName_2=a.asm.Ha).apply(null,arguments)},hc=a._emscripten_bind_MetadataQuerier___destroy___0=function(){return(hc=a._emscripten_bind_MetadataQuerier___destroy___0=a.asm.Ia).apply(null,arguments)},Qa=a._emscripten_bind_Decoder_Decoder_0=
function(){return(Qa=a._emscripten_bind_Decoder_Decoder_0=a.asm.Ja).apply(null,arguments)},ic=a._emscripten_bind_Decoder_DecodeArrayToPointCloud_3=function(){return(ic=a._emscripten_bind_Decoder_DecodeArrayToPointCloud_3=a.asm.Ka).apply(null,arguments)},jc=a._emscripten_bind_Decoder_DecodeArrayToMesh_3=function(){return(jc=a._emscripten_bind_Decoder_DecodeArrayToMesh_3=a.asm.La).apply(null,arguments)},kc=a._emscripten_bind_Decoder_GetAttributeId_2=function(){return(kc=a._emscripten_bind_Decoder_GetAttributeId_2=
a.asm.Ma).apply(null,arguments)},lc=a._emscripten_bind_Decoder_GetAttributeIdByName_2=function(){return(lc=a._emscripten_bind_Decoder_GetAttributeIdByName_2=a.asm.Na).apply(null,arguments)},mc=a._emscripten_bind_Decoder_GetAttributeIdByMetadataEntry_3=function(){return(mc=a._emscripten_bind_Decoder_GetAttributeIdByMetadataEntry_3=a.asm.Oa).apply(null,arguments)},nc=a._emscripten_bind_Decoder_GetAttribute_2=function(){return(nc=a._emscripten_bind_Decoder_GetAttribute_2=a.asm.Pa).apply(null,arguments)},
oc=a._emscripten_bind_Decoder_GetAttributeByUniqueId_2=function(){return(oc=a._emscripten_bind_Decoder_GetAttributeByUniqueId_2=a.asm.Qa).apply(null,arguments)},pc=a._emscripten_bind_Decoder_GetMetadata_1=function(){return(pc=a._emscripten_bind_Decoder_GetMetadata_1=a.asm.Ra).apply(null,arguments)},qc=a._emscripten_bind_Decoder_GetAttributeMetadata_2=function(){return(qc=a._emscripten_bind_Decoder_GetAttributeMetadata_2=a.asm.Sa).apply(null,arguments)},rc=a._emscripten_bind_Decoder_GetFaceFromMesh_3=
function(){return(rc=a._emscripten_bind_Decoder_GetFaceFromMesh_3=a.asm.Ta).apply(null,arguments)},sc=a._emscripten_bind_Decoder_GetTriangleStripsFromMesh_2=function(){return(sc=a._emscripten_bind_Decoder_GetTriangleStripsFromMesh_2=a.asm.Ua).apply(null,arguments)},tc=a._emscripten_bind_Decoder_GetTrianglesUInt16Array_3=function(){return(tc=a._emscripten_bind_Decoder_GetTrianglesUInt16Array_3=a.asm.Va).apply(null,arguments)},uc=a._emscripten_bind_Decoder_GetTrianglesUInt32Array_3=function(){return(uc=
a._emscripten_bind_Decoder_GetTrianglesUInt32Array_3=a.asm.Wa).apply(null,arguments)},vc=a._emscripten_bind_Decoder_GetAttributeFloat_3=function(){return(vc=a._emscripten_bind_Decoder_GetAttributeFloat_3=a.asm.Xa).apply(null,arguments)},wc=a._emscripten_bind_Decoder_GetAttributeFloatForAllPoints_3=function(){return(wc=a._emscripten_bind_Decoder_GetAttributeFloatForAllPoints_3=a.asm.Ya).apply(null,arguments)},xc=a._emscripten_bind_Decoder_GetAttributeIntForAllPoints_3=function(){return(xc=a._emscripten_bind_Decoder_GetAttributeIntForAllPoints_3=
a.asm.Za).apply(null,arguments)},yc=a._emscripten_bind_Decoder_GetAttributeInt8ForAllPoints_3=function(){return(yc=a._emscripten_bind_Decoder_GetAttributeInt8ForAllPoints_3=a.asm._a).apply(null,arguments)},zc=a._emscripten_bind_Decoder_GetAttributeUInt8ForAllPoints_3=function(){return(zc=a._emscripten_bind_Decoder_GetAttributeUInt8ForAllPoints_3=a.asm.$a).apply(null,arguments)},Ac=a._emscripten_bind_Decoder_GetAttributeInt16ForAllPoints_3=function(){return(Ac=a._emscripten_bind_Decoder_GetAttributeInt16ForAllPoints_3=
a.asm.ab).apply(null,arguments)},Bc=a._emscripten_bind_Decoder_GetAttributeUInt16ForAllPoints_3=function(){return(Bc=a._emscripten_bind_Decoder_GetAttributeUInt16ForAllPoints_3=a.asm.bb).apply(null,arguments)},Cc=a._emscripten_bind_Decoder_GetAttributeInt32ForAllPoints_3=function(){return(Cc=a._emscripten_bind_Decoder_GetAttributeInt32ForAllPoints_3=a.asm.cb).apply(null,arguments)},Dc=a._emscripten_bind_Decoder_GetAttributeUInt32ForAllPoints_3=function(){return(Dc=a._emscripten_bind_Decoder_GetAttributeUInt32ForAllPoints_3=
a.asm.db).apply(null,arguments)},Ec=a._emscripten_bind_Decoder_GetAttributeDataArrayForAllPoints_5=function(){return(Ec=a._emscripten_bind_Decoder_GetAttributeDataArrayForAllPoints_5=a.asm.eb).apply(null,arguments)},Fc=a._emscripten_bind_Decoder_SkipAttributeTransform_1=function(){return(Fc=a._emscripten_bind_Decoder_SkipAttributeTransform_1=a.asm.fb).apply(null,arguments)},Gc=a._emscripten_bind_Decoder_GetEncodedGeometryType_Deprecated_1=function(){return(Gc=a._emscripten_bind_Decoder_GetEncodedGeometryType_Deprecated_1=
a.asm.gb).apply(null,arguments)},Hc=a._emscripten_bind_Decoder_DecodeBufferToPointCloud_2=function(){return(Hc=a._emscripten_bind_Decoder_DecodeBufferToPointCloud_2=a.asm.hb).apply(null,arguments)},Ic=a._emscripten_bind_Decoder_DecodeBufferToMesh_2=function(){return(Ic=a._emscripten_bind_Decoder_DecodeBufferToMesh_2=a.asm.ib).apply(null,arguments)},Jc=a._emscripten_bind_Decoder___destroy___0=function(){return(Jc=a._emscripten_bind_Decoder___destroy___0=a.asm.jb).apply(null,arguments)},Kc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_INVALID_TRANSFORM=
function(){return(Kc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_INVALID_TRANSFORM=a.asm.kb).apply(null,arguments)},Lc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_NO_TRANSFORM=function(){return(Lc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_NO_TRANSFORM=a.asm.lb).apply(null,arguments)},Mc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_QUANTIZATION_TRANSFORM=function(){return(Mc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_QUANTIZATION_TRANSFORM=
a.asm.mb).apply(null,arguments)},Nc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_OCTAHEDRON_TRANSFORM=function(){return(Nc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_OCTAHEDRON_TRANSFORM=a.asm.nb).apply(null,arguments)},Oc=a._emscripten_enum_draco_GeometryAttribute_Type_INVALID=function(){return(Oc=a._emscripten_enum_draco_GeometryAttribute_Type_INVALID=a.asm.ob).apply(null,arguments)},Pc=a._emscripten_enum_draco_GeometryAttribute_Type_POSITION=function(){return(Pc=a._emscripten_enum_draco_GeometryAttribute_Type_POSITION=
a.asm.pb).apply(null,arguments)},Qc=a._emscripten_enum_draco_GeometryAttribute_Type_NORMAL=function(){return(Qc=a._emscripten_enum_draco_GeometryAttribute_Type_NORMAL=a.asm.qb).apply(null,arguments)},Rc=a._emscripten_enum_draco_GeometryAttribute_Type_COLOR=function(){return(Rc=a._emscripten_enum_draco_GeometryAttribute_Type_COLOR=a.asm.rb).apply(null,arguments)},Sc=a._emscripten_enum_draco_GeometryAttribute_Type_TEX_COORD=function(){return(Sc=a._emscripten_enum_draco_GeometryAttribute_Type_TEX_COORD=
a.asm.sb).apply(null,arguments)},Tc=a._emscripten_enum_draco_GeometryAttribute_Type_GENERIC=function(){return(Tc=a._emscripten_enum_draco_GeometryAttribute_Type_GENERIC=a.asm.tb).apply(null,arguments)},Uc=a._emscripten_enum_draco_EncodedGeometryType_INVALID_GEOMETRY_TYPE=function(){return(Uc=a._emscripten_enum_draco_EncodedGeometryType_INVALID_GEOMETRY_TYPE=a.asm.ub).apply(null,arguments)},Vc=a._emscripten_enum_draco_EncodedGeometryType_POINT_CLOUD=function(){return(Vc=a._emscripten_enum_draco_EncodedGeometryType_POINT_CLOUD=
a.asm.vb).apply(null,arguments)},Wc=a._emscripten_enum_draco_EncodedGeometryType_TRIANGULAR_MESH=function(){return(Wc=a._emscripten_enum_draco_EncodedGeometryType_TRIANGULAR_MESH=a.asm.wb).apply(null,arguments)},Xc=a._emscripten_enum_draco_DataType_DT_INVALID=function(){return(Xc=a._emscripten_enum_draco_DataType_DT_INVALID=a.asm.xb).apply(null,arguments)},Yc=a._emscripten_enum_draco_DataType_DT_INT8=function(){return(Yc=a._emscripten_enum_draco_DataType_DT_INT8=a.asm.yb).apply(null,arguments)},Zc=
a._emscripten_enum_draco_DataType_DT_UINT8=function(){return(Zc=a._emscripten_enum_draco_DataType_DT_UINT8=a.asm.zb).apply(null,arguments)},$c=a._emscripten_enum_draco_DataType_DT_INT16=function(){return($c=a._emscripten_enum_draco_DataType_DT_INT16=a.asm.Ab).apply(null,arguments)},ad=a._emscripten_enum_draco_DataType_DT_UINT16=function(){return(ad=a._emscripten_enum_draco_DataType_DT_UINT16=a.asm.Bb).apply(null,arguments)},bd=a._emscripten_enum_draco_DataType_DT_INT32=function(){return(bd=a._emscripten_enum_draco_DataType_DT_INT32=
a.asm.Cb).apply(null,arguments)},cd=a._emscripten_enum_draco_DataType_DT_UINT32=function(){return(cd=a._emscripten_enum_draco_DataType_DT_UINT32=a.asm.Db).apply(null,arguments)},dd=a._emscripten_enum_draco_DataType_DT_INT64=function(){return(dd=a._emscripten_enum_draco_DataType_DT_INT64=a.asm.Eb).apply(null,arguments)},ed=a._emscripten_enum_draco_DataType_DT_UINT64=function(){return(ed=a._emscripten_enum_draco_DataType_DT_UINT64=a.asm.Fb).apply(null,arguments)},fd=a._emscripten_enum_draco_DataType_DT_FLOAT32=
function(){return(fd=a._emscripten_enum_draco_DataType_DT_FLOAT32=a.asm.Gb).apply(null,arguments)},gd=a._emscripten_enum_draco_DataType_DT_FLOAT64=function(){return(gd=a._emscripten_enum_draco_DataType_DT_FLOAT64=a.asm.Hb).apply(null,arguments)},hd=a._emscripten_enum_draco_DataType_DT_BOOL=function(){return(hd=a._emscripten_enum_draco_DataType_DT_BOOL=a.asm.Ib).apply(null,arguments)},id=a._emscripten_enum_draco_DataType_DT_TYPES_COUNT=function(){return(id=a._emscripten_enum_draco_DataType_DT_TYPES_COUNT=
a.asm.Jb).apply(null,arguments)},jd=a._emscripten_enum_draco_StatusCode_OK=function(){return(jd=a._emscripten_enum_draco_StatusCode_OK=a.asm.Kb).apply(null,arguments)},kd=a._emscripten_enum_draco_StatusCode_DRACO_ERROR=function(){return(kd=a._emscripten_enum_draco_StatusCode_DRACO_ERROR=a.asm.Lb).apply(null,arguments)},ld=a._emscripten_enum_draco_StatusCode_IO_ERROR=function(){return(ld=a._emscripten_enum_draco_StatusCode_IO_ERROR=a.asm.Mb).apply(null,arguments)},md=a._emscripten_enum_draco_StatusCode_INVALID_PARAMETER=
function(){return(md=a._emscripten_enum_draco_StatusCode_INVALID_PARAMETER=a.asm.Nb).apply(null,arguments)},nd=a._emscripten_enum_draco_StatusCode_UNSUPPORTED_VERSION=function(){return(nd=a._emscripten_enum_draco_StatusCode_UNSUPPORTED_VERSION=a.asm.Ob).apply(null,arguments)},od=a._emscripten_enum_draco_StatusCode_UNKNOWN_VERSION=function(){return(od=a._emscripten_enum_draco_StatusCode_UNKNOWN_VERSION=a.asm.Pb).apply(null,arguments)};a._malloc=function(){return(a._malloc=a.asm.Qb).apply(null,arguments)};
a._free=function(){return(a._free=a.asm.Rb).apply(null,arguments)};var ua=function(){return(ua=a.asm.Sb).apply(null,arguments)};a.___start_em_js=11660;a.___stop_em_js=11758;var la;ha=function b(){la||F();la||(ha=b)};if(a.preInit)for("function"==typeof a.preInit&&(a.preInit=[a.preInit]);0<a.preInit.length;)a.preInit.pop()();F();v.prototype=Object.create(v.prototype);v.prototype.constructor=v;v.prototype.__class__=v;v.__cache__={};a.WrapperObject=v;a.getCache=w;a.wrapPointer=B;a.castObject=function(b,
c){return B(b.ptr,c)};a.NULL=B(0);a.destroy=function(b){if(!b.__destroy__)throw"Error: Cannot destroy object. (Did you create it yourself?)";b.__destroy__();delete w(b.__class__)[b.ptr]};a.compare=function(b,c){return b.ptr===c.ptr};a.getPointer=function(b){return b.ptr};a.getClass=function(b){return b.__class__};var r={buffer:0,size:0,pos:0,temps:[],needed:0,prepare:function(){if(r.needed){for(var b=0;b<r.temps.length;b++)a._free(r.temps[b]);r.temps.length=0;a._free(r.buffer);r.buffer=0;r.size+=
r.needed;r.needed=0}r.buffer||(r.size+=128,r.buffer=a._malloc(r.size),r.buffer||y(void 0));r.pos=0},alloc:function(b,c){r.buffer||y(void 0);b=b.length*c.BYTES_PER_ELEMENT;b=b+7&-8;r.pos+b>=r.size?(0<b||y(void 0),r.needed+=b,c=a._malloc(b),r.temps.push(c)):(c=r.buffer+r.pos,r.pos+=b);return c},copy:function(b,c,d){d>>>=0;switch(c.BYTES_PER_ELEMENT){case 2:d>>>=1;break;case 4:d>>>=2;break;case 8:d>>>=3}for(var g=0;g<b.length;g++)c[d+g]=b[g]}};X.prototype=Object.create(v.prototype);X.prototype.constructor=
X;X.prototype.__class__=X;X.__cache__={};a.VoidPtr=X;X.prototype.__destroy__=X.prototype.__destroy__=function(){Xa(this.ptr)};S.prototype=Object.create(v.prototype);S.prototype.constructor=S;S.prototype.__class__=S;S.__cache__={};a.DecoderBuffer=S;S.prototype.Init=S.prototype.Init=function(b,c){var d=this.ptr;r.prepare();"object"==typeof b&&(b=Z(b));c&&"object"===typeof c&&(c=c.ptr);Ya(d,b,c)};S.prototype.__destroy__=S.prototype.__destroy__=function(){Za(this.ptr)};Q.prototype=Object.create(v.prototype);
Q.prototype.constructor=Q;Q.prototype.__class__=Q;Q.__cache__={};a.AttributeTransformData=Q;Q.prototype.transform_type=Q.prototype.transform_type=function(){return $a(this.ptr)};Q.prototype.__destroy__=Q.prototype.__destroy__=function(){ab(this.ptr)};V.prototype=Object.create(v.prototype);V.prototype.constructor=V;V.prototype.__class__=V;V.__cache__={};a.GeometryAttribute=V;V.prototype.__destroy__=V.prototype.__destroy__=function(){bb(this.ptr)};x.prototype=Object.create(v.prototype);x.prototype.constructor=
x;x.prototype.__class__=x;x.__cache__={};a.PointAttribute=x;x.prototype.size=x.prototype.size=function(){return cb(this.ptr)};x.prototype.GetAttributeTransformData=x.prototype.GetAttributeTransformData=function(){return B(db(this.ptr),Q)};x.prototype.attribute_type=x.prototype.attribute_type=function(){return eb(this.ptr)};x.prototype.data_type=x.prototype.data_type=function(){return fb(this.ptr)};x.prototype.num_components=x.prototype.num_components=function(){return gb(this.ptr)};x.prototype.normalized=
x.prototype.normalized=function(){return!!hb(this.ptr)};x.prototype.byte_stride=x.prototype.byte_stride=function(){return ib(this.ptr)};x.prototype.byte_offset=x.prototype.byte_offset=function(){return jb(this.ptr)};x.prototype.unique_id=x.prototype.unique_id=function(){return kb(this.ptr)};x.prototype.__destroy__=x.prototype.__destroy__=function(){lb(this.ptr)};D.prototype=Object.create(v.prototype);D.prototype.constructor=D;D.prototype.__class__=D;D.__cache__={};a.AttributeQuantizationTransform=
D;D.prototype.InitFromAttribute=D.prototype.InitFromAttribute=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return!!mb(c,b)};D.prototype.quantization_bits=D.prototype.quantization_bits=function(){return nb(this.ptr)};D.prototype.min_value=D.prototype.min_value=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return ob(c,b)};D.prototype.range=D.prototype.range=function(){return pb(this.ptr)};D.prototype.__destroy__=D.prototype.__destroy__=function(){qb(this.ptr)};G.prototype=
Object.create(v.prototype);G.prototype.constructor=G;G.prototype.__class__=G;G.__cache__={};a.AttributeOctahedronTransform=G;G.prototype.InitFromAttribute=G.prototype.InitFromAttribute=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return!!rb(c,b)};G.prototype.quantization_bits=G.prototype.quantization_bits=function(){return sb(this.ptr)};G.prototype.__destroy__=G.prototype.__destroy__=function(){tb(this.ptr)};H.prototype=Object.create(v.prototype);H.prototype.constructor=H;H.prototype.__class__=
H;H.__cache__={};a.PointCloud=H;H.prototype.num_attributes=H.prototype.num_attributes=function(){return ub(this.ptr)};H.prototype.num_points=H.prototype.num_points=function(){return vb(this.ptr)};H.prototype.__destroy__=H.prototype.__destroy__=function(){wb(this.ptr)};E.prototype=Object.create(v.prototype);E.prototype.constructor=E;E.prototype.__class__=E;E.__cache__={};a.Mesh=E;E.prototype.num_faces=E.prototype.num_faces=function(){return xb(this.ptr)};E.prototype.num_attributes=E.prototype.num_attributes=
function(){return yb(this.ptr)};E.prototype.num_points=E.prototype.num_points=function(){return zb(this.ptr)};E.prototype.__destroy__=E.prototype.__destroy__=function(){Ab(this.ptr)};T.prototype=Object.create(v.prototype);T.prototype.constructor=T;T.prototype.__class__=T;T.__cache__={};a.Metadata=T;T.prototype.__destroy__=T.prototype.__destroy__=function(){Bb(this.ptr)};C.prototype=Object.create(v.prototype);C.prototype.constructor=C;C.prototype.__class__=C;C.__cache__={};a.Status=C;C.prototype.code=
C.prototype.code=function(){return Cb(this.ptr)};C.prototype.ok=C.prototype.ok=function(){return!!Db(this.ptr)};C.prototype.error_msg=C.prototype.error_msg=function(){return p(Eb(this.ptr))};C.prototype.__destroy__=C.prototype.__destroy__=function(){Fb(this.ptr)};I.prototype=Object.create(v.prototype);I.prototype.constructor=I;I.prototype.__class__=I;I.__cache__={};a.DracoFloat32Array=I;I.prototype.GetValue=I.prototype.GetValue=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return Gb(c,
b)};I.prototype.size=I.prototype.size=function(){return Hb(this.ptr)};I.prototype.__destroy__=I.prototype.__destroy__=function(){Ib(this.ptr)};J.prototype=Object.create(v.prototype);J.prototype.constructor=J;J.prototype.__class__=J;J.__cache__={};a.DracoInt8Array=J;J.prototype.GetValue=J.prototype.GetValue=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return Jb(c,b)};J.prototype.size=J.prototype.size=function(){return Kb(this.ptr)};J.prototype.__destroy__=J.prototype.__destroy__=function(){Lb(this.ptr)};
K.prototype=Object.create(v.prototype);K.prototype.constructor=K;K.prototype.__class__=K;K.__cache__={};a.DracoUInt8Array=K;K.prototype.GetValue=K.prototype.GetValue=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return Mb(c,b)};K.prototype.size=K.prototype.size=function(){return Nb(this.ptr)};K.prototype.__destroy__=K.prototype.__destroy__=function(){Ob(this.ptr)};L.prototype=Object.create(v.prototype);L.prototype.constructor=L;L.prototype.__class__=L;L.__cache__={};a.DracoInt16Array=
L;L.prototype.GetValue=L.prototype.GetValue=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return Pb(c,b)};L.prototype.size=L.prototype.size=function(){return Qb(this.ptr)};L.prototype.__destroy__=L.prototype.__destroy__=function(){Rb(this.ptr)};M.prototype=Object.create(v.prototype);M.prototype.constructor=M;M.prototype.__class__=M;M.__cache__={};a.DracoUInt16Array=M;M.prototype.GetValue=M.prototype.GetValue=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return Sb(c,b)};
M.prototype.size=M.prototype.size=function(){return Tb(this.ptr)};M.prototype.__destroy__=M.prototype.__destroy__=function(){Ub(this.ptr)};N.prototype=Object.create(v.prototype);N.prototype.constructor=N;N.prototype.__class__=N;N.__cache__={};a.DracoInt32Array=N;N.prototype.GetValue=N.prototype.GetValue=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return Vb(c,b)};N.prototype.size=N.prototype.size=function(){return Wb(this.ptr)};N.prototype.__destroy__=N.prototype.__destroy__=function(){Xb(this.ptr)};
O.prototype=Object.create(v.prototype);O.prototype.constructor=O;O.prototype.__class__=O;O.__cache__={};a.DracoUInt32Array=O;O.prototype.GetValue=O.prototype.GetValue=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return Yb(c,b)};O.prototype.size=O.prototype.size=function(){return Zb(this.ptr)};O.prototype.__destroy__=O.prototype.__destroy__=function(){$b(this.ptr)};z.prototype=Object.create(v.prototype);z.prototype.constructor=z;z.prototype.__class__=z;z.__cache__={};a.MetadataQuerier=
z;z.prototype.HasEntry=z.prototype.HasEntry=function(b,c){var d=this.ptr;r.prepare();b&&"object"===typeof b&&(b=b.ptr);c=c&&"object"===typeof c?c.ptr:R(c);return!!ac(d,b,c)};z.prototype.GetIntEntry=z.prototype.GetIntEntry=function(b,c){var d=this.ptr;r.prepare();b&&"object"===typeof b&&(b=b.ptr);c=c&&"object"===typeof c?c.ptr:R(c);return bc(d,b,c)};z.prototype.GetIntEntryArray=z.prototype.GetIntEntryArray=function(b,c,d){var g=this.ptr;r.prepare();b&&"object"===typeof b&&(b=b.ptr);c=c&&"object"===
typeof c?c.ptr:R(c);d&&"object"===typeof d&&(d=d.ptr);cc(g,b,c,d)};z.prototype.GetDoubleEntry=z.prototype.GetDoubleEntry=function(b,c){var d=this.ptr;r.prepare();b&&"object"===typeof b&&(b=b.ptr);c=c&&"object"===typeof c?c.ptr:R(c);return dc(d,b,c)};z.prototype.GetStringEntry=z.prototype.GetStringEntry=function(b,c){var d=this.ptr;r.prepare();b&&"object"===typeof b&&(b=b.ptr);c=c&&"object"===typeof c?c.ptr:R(c);return p(ec(d,b,c))};z.prototype.NumEntries=z.prototype.NumEntries=function(b){var c=this.ptr;
b&&"object"===typeof b&&(b=b.ptr);return fc(c,b)};z.prototype.GetEntryName=z.prototype.GetEntryName=function(b,c){var d=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);return p(gc(d,b,c))};z.prototype.__destroy__=z.prototype.__destroy__=function(){hc(this.ptr)};m.prototype=Object.create(v.prototype);m.prototype.constructor=m;m.prototype.__class__=m;m.__cache__={};a.Decoder=m;m.prototype.DecodeArrayToPointCloud=m.prototype.DecodeArrayToPointCloud=function(b,c,d){var g=
this.ptr;r.prepare();"object"==typeof b&&(b=Z(b));c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return B(ic(g,b,c,d),C)};m.prototype.DecodeArrayToMesh=m.prototype.DecodeArrayToMesh=function(b,c,d){var g=this.ptr;r.prepare();"object"==typeof b&&(b=Z(b));c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return B(jc(g,b,c,d),C)};m.prototype.GetAttributeId=m.prototype.GetAttributeId=function(b,c){var d=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&
(c=c.ptr);return kc(d,b,c)};m.prototype.GetAttributeIdByName=m.prototype.GetAttributeIdByName=function(b,c){var d=this.ptr;r.prepare();b&&"object"===typeof b&&(b=b.ptr);c=c&&"object"===typeof c?c.ptr:R(c);return lc(d,b,c)};m.prototype.GetAttributeIdByMetadataEntry=m.prototype.GetAttributeIdByMetadataEntry=function(b,c,d){var g=this.ptr;r.prepare();b&&"object"===typeof b&&(b=b.ptr);c=c&&"object"===typeof c?c.ptr:R(c);d=d&&"object"===typeof d?d.ptr:R(d);return mc(g,b,c,d)};m.prototype.GetAttribute=
m.prototype.GetAttribute=function(b,c){var d=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);return B(nc(d,b,c),x)};m.prototype.GetAttributeByUniqueId=m.prototype.GetAttributeByUniqueId=function(b,c){var d=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);return B(oc(d,b,c),x)};m.prototype.GetMetadata=m.prototype.GetMetadata=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return B(pc(c,b),T)};m.prototype.GetAttributeMetadata=m.prototype.GetAttributeMetadata=
function(b,c){var d=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);return B(qc(d,b,c),T)};m.prototype.GetFaceFromMesh=m.prototype.GetFaceFromMesh=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!rc(g,b,c,d)};m.prototype.GetTriangleStripsFromMesh=m.prototype.GetTriangleStripsFromMesh=function(b,c){var d=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);
return sc(d,b,c)};m.prototype.GetTrianglesUInt16Array=m.prototype.GetTrianglesUInt16Array=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!tc(g,b,c,d)};m.prototype.GetTrianglesUInt32Array=m.prototype.GetTrianglesUInt32Array=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!uc(g,b,c,d)};m.prototype.GetAttributeFloat=m.prototype.GetAttributeFloat=
function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!vc(g,b,c,d)};m.prototype.GetAttributeFloatForAllPoints=m.prototype.GetAttributeFloatForAllPoints=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!wc(g,b,c,d)};m.prototype.GetAttributeIntForAllPoints=m.prototype.GetAttributeIntForAllPoints=function(b,c,d){var g=this.ptr;
b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!xc(g,b,c,d)};m.prototype.GetAttributeInt8ForAllPoints=m.prototype.GetAttributeInt8ForAllPoints=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!yc(g,b,c,d)};m.prototype.GetAttributeUInt8ForAllPoints=m.prototype.GetAttributeUInt8ForAllPoints=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=
b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!zc(g,b,c,d)};m.prototype.GetAttributeInt16ForAllPoints=m.prototype.GetAttributeInt16ForAllPoints=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!Ac(g,b,c,d)};m.prototype.GetAttributeUInt16ForAllPoints=m.prototype.GetAttributeUInt16ForAllPoints=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&
(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!Bc(g,b,c,d)};m.prototype.GetAttributeInt32ForAllPoints=m.prototype.GetAttributeInt32ForAllPoints=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!Cc(g,b,c,d)};m.prototype.GetAttributeUInt32ForAllPoints=m.prototype.GetAttributeUInt32ForAllPoints=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===
typeof d&&(d=d.ptr);return!!Dc(g,b,c,d)};m.prototype.GetAttributeDataArrayForAllPoints=m.prototype.GetAttributeDataArrayForAllPoints=function(b,c,d,g,t){var aa=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);g&&"object"===typeof g&&(g=g.ptr);t&&"object"===typeof t&&(t=t.ptr);return!!Ec(aa,b,c,d,g,t)};m.prototype.SkipAttributeTransform=m.prototype.SkipAttributeTransform=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);Fc(c,
b)};m.prototype.GetEncodedGeometryType_Deprecated=m.prototype.GetEncodedGeometryType_Deprecated=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return Gc(c,b)};m.prototype.DecodeBufferToPointCloud=m.prototype.DecodeBufferToPointCloud=function(b,c){var d=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);return B(Hc(d,b,c),C)};m.prototype.DecodeBufferToMesh=m.prototype.DecodeBufferToMesh=function(b,c){var d=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===
typeof c&&(c=c.ptr);return B(Ic(d,b,c),C)};m.prototype.__destroy__=m.prototype.__destroy__=function(){Jc(this.ptr)};(function(){function b(){a.ATTRIBUTE_INVALID_TRANSFORM=Kc();a.ATTRIBUTE_NO_TRANSFORM=Lc();a.ATTRIBUTE_QUANTIZATION_TRANSFORM=Mc();a.ATTRIBUTE_OCTAHEDRON_TRANSFORM=Nc();a.INVALID=Oc();a.POSITION=Pc();a.NORMAL=Qc();a.COLOR=Rc();a.TEX_COORD=Sc();a.GENERIC=Tc();a.INVALID_GEOMETRY_TYPE=Uc();a.POINT_CLOUD=Vc();a.TRIANGULAR_MESH=Wc();a.DT_INVALID=Xc();a.DT_INT8=Yc();a.DT_UINT8=Zc();a.DT_INT16=
$c();a.DT_UINT16=ad();a.DT_INT32=bd();a.DT_UINT32=cd();a.DT_INT64=dd();a.DT_UINT64=ed();a.DT_FLOAT32=fd();a.DT_FLOAT64=gd();a.DT_BOOL=hd();a.DT_TYPES_COUNT=id();a.OK=jd();a.DRACO_ERROR=kd();a.IO_ERROR=ld();a.INVALID_PARAMETER=md();a.UNSUPPORTED_VERSION=nd();a.UNKNOWN_VERSION=od()}va?b():oa.unshift(b)})();if("function"===typeof a.onModuleParsed)a.onModuleParsed();a.Decoder.prototype.GetEncodedGeometryType=function(b){if(b.__class__&&b.__class__===a.DecoderBuffer)return a.Decoder.prototype.GetEncodedGeometryType_Deprecated(b);
if(8>b.byteLength)return a.INVALID_GEOMETRY_TYPE;switch(b[7]){case 0:return a.POINT_CLOUD;case 1:return a.TRIANGULAR_MESH;default:return a.INVALID_GEOMETRY_TYPE}};return n.ready}}();"object"===typeof exports&&"object"===typeof module?module.exports=DracoDecoderModule:"function"===typeof define&&define.amd?define([],function(){return DracoDecoderModule}):"object"===typeof exports&&(exports.DracoDecoderModule=DracoDecoderModule);

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

26
specification/README.md Normal file
View File

@@ -0,0 +1,26 @@
# Наброски будущей схемы конвейера
![](mvp_1.jpg)
## Список материалов:
В этот список необходимо доложить энкодер для двигателя!
| Название | Сумма, ₽ | Количество |
| :--- | :--- | :--- |
| Медная лента для удаления припоя / Оплетка для выпайки диаметр 2 мм длина 1.5 м | 164 | 1 |
| Набор проводов для пайки | 855 | 1 |
| Флюс гель универсальный безотмывочный, для пайки микросхем и компонентов Flux RMA-223-UV-10г | 162 | 1 |
| Припой для пайки с канифолью 1мм 50гр ПОС-61 на катушке (ГОСТ) | 423 | 1 |
| ALIENTEK Паяльник 140 Вт, 7 предметов | 4 985 | 1 |
| Преобразователь DC-DC понижающий с 8-60V до 1-36V 15A max | 805 | 1 |
| Беспаечная макетная плата (breadboard) MB-102, 830 точек, для Arduino и прочих устройств | 1 112 | 2 |
| 120 шт. Провода перемычки для макетных плат, соединительные провода для модулей, контроллеров arduino (10 см) 3 вида по 40 шт папа-папа, мама-мама, папа-мама | 636 | 2 |
| Homeled, Блок питания, 12V, 200W, 180-265 вольт. С клеммами. Импульсный для светодиодных лент и светильников | 599 | 1 |
| Сервопривод MG996R, 10 шт, 180 , металлические шестерни, для Arduino, роботов и RC, размеры стандарт | 2 990 | 1 |
| PWM PCA9685 драйвер на 16 сервоприводов расширитель портов с I2C интерфейсом для Led и Servo (12 bit, I2C) в IIC/I2C/TWI/SPI c тестером сервоприводов 3 режима, набор | 1 178 | 2 |
| Модуль I2C-мультиплексора CJMCU-9548 на базе TCA9548A / PCA9548A | 532 | 2 |
| Модуль VL53L0X лазерный дальномер GY-530 (до 2м, питание 3-5В, I2C) | 1064 | 4 |
| Модуль ESP32 TYPE-C CH340C + плата расширения ESP32 | 1040 | 2 |
[Ссылка на товары](https://www.ozon.ru/cart?share=wGhPkQd)

54
specification/mindmap.md Normal file
View File

@@ -0,0 +1,54 @@
# Алгоритм работы сортировщика
```mermaid
mindmap
root((Алгоритм работы))
Этап 1: Ожидание и Движение
Arduino запускает шаговый двигатель
Лента движется с постоянной скоростью
Этап 2: Детекция и Анализ
Объект проходит под датчиком и камерой
Предмет соответствует габаритам?
Да
Есть признак круга в сечении?
Да - Требуется доупаковка
Секция 2
Нет - Подходит для сортировки
Секция 3
Нет
Не подходит по габаритам
Секция 1
Этап 3: Трекинг Синхронизация
Arduino отсчитывает шаги двигателя
Вычисляется момент времени T
Этап 4: Маршрутизация
В момент T сервопривод поворачивает барьер на 45°
Объект смещается в выбранную зону
Сервопривод возвращается в исходное положение
Этап 5: Завершение
Объект попадает в накопитель
Система возвращается в состояние Ожидание
```
## Блок схема:
```mermaid
flowchart TD
Start[Этап 1: Ожидание и Движение] --> Detect[Этап 2: Детекция и Анализ]
Detect --> SizeCheck{Предмет соответствует<br/>габаритам?}
SizeCheck -->|Нет| Reject[Секция 1:<br/>Не габарит]
SizeCheck -->|Да| CircleCheck{Есть признак<br/>круга?}
CircleCheck -->|Да| Repack[Секция 2:<br/>Требуется доупаковка]
CircleCheck -->|Нет| Sort[Секция 3:<br/>Подходит для сортировки]
Reject --> Track[Этап 3: Трекинг]
Repack --> Track
Sort --> Track
Track --> Route[Этап 4: Маршрутизация]
Route --> End[Этап 5: Завершение]
End --> Start
```

277
specification/mvp_1.drawio Normal file
View File

@@ -0,0 +1,277 @@
<mxfile host="server.home">
<diagram name="Страница-1" id="mppyHKg2Vbg7W8e0-Fd9">
<mxGraphModel dx="3902" dy="2031" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1654" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="jfPUc6vKa1uVjcLF1lrC-2" edge="1" parent="1" style="edgeStyle=none;orthogonalLoop=1;jettySize=auto;html=1;rounded=0;fontFamily=Architects Daughter;fontSource=https%3A%2F%2Ffonts.googleapis.com%2Fcss%3Ffamily%3DArchitects%2BDaughter;fontSize=16;startSize=14;endArrow=none;endSize=14;sourcePerimeterSpacing=8;targetPerimeterSpacing=8;curved=1;endFill=0;" value="">
<mxGeometry relative="1" width="140" as="geometry">
<Array as="points" />
<mxPoint x="37" y="773" as="sourcePoint" />
<mxPoint x="1547" y="773" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="jfPUc6vKa1uVjcLF1lrC-6" edge="1" parent="1" style="edgeStyle=none;orthogonalLoop=1;jettySize=auto;html=1;rounded=0;fontFamily=Architects Daughter;fontSource=https://fonts.googleapis.com/css?family=Architects+Daughter;fontSize=16;startSize=14;endArrow=none;endSize=14;sourcePerimeterSpacing=8;targetPerimeterSpacing=8;curved=1;endFill=0;" value="">
<mxGeometry relative="1" width="140" as="geometry">
<Array as="points" />
<mxPoint x="257" y="720" as="sourcePoint" />
<mxPoint x="1617" y="720" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="jfPUc6vKa1uVjcLF1lrC-7" edge="1" parent="1" style="edgeStyle=none;orthogonalLoop=1;jettySize=auto;html=1;rounded=0;fontFamily=Architects Daughter;fontSource=https%3A%2F%2Ffonts.googleapis.com%2Fcss%3Ffamily%3DArchitects%2BDaughter;fontSize=16;startSize=14;endArrow=none;endSize=14;sourcePerimeterSpacing=8;targetPerimeterSpacing=8;curved=1;endFill=0;" value="">
<mxGeometry relative="1" width="140" as="geometry">
<Array as="points" />
<mxPoint x="37" y="770" as="sourcePoint" />
<mxPoint x="107" y="720" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="jfPUc6vKa1uVjcLF1lrC-8" edge="1" parent="1" style="edgeStyle=none;orthogonalLoop=1;jettySize=auto;html=1;rounded=0;fontFamily=Architects Daughter;fontSource=https://fonts.googleapis.com/css?family=Architects+Daughter;fontSize=16;startSize=14;endArrow=none;endSize=14;sourcePerimeterSpacing=8;targetPerimeterSpacing=8;curved=1;endFill=0;" value="">
<mxGeometry relative="1" width="140" as="geometry">
<Array as="points" />
<mxPoint x="1547" y="774" as="sourcePoint" />
<mxPoint x="1617" y="724" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="jfPUc6vKa1uVjcLF1lrC-9" parent="1" style="rounded=0;whiteSpace=wrap;html=1;fontFamily=Architects Daughter;fontSource=https%3A%2F%2Ffonts.googleapis.com%2Fcss%3Ffamily%3DArchitects%2BDaughter;" value="&lt;font style=&quot;font-size: 30px;&quot;&gt;1&lt;/font&gt;" vertex="1">
<mxGeometry height="80" width="160" x="847" y="830" as="geometry" />
</mxCell>
<mxCell id="jfPUc6vKa1uVjcLF1lrC-10" parent="1" style="rounded=0;whiteSpace=wrap;html=1;fontFamily=Architects Daughter;fontSource=https://fonts.googleapis.com/css?family=Architects+Daughter;" value="&lt;font style=&quot;font-size: 30px;&quot;&gt;2&lt;/font&gt;" vertex="1">
<mxGeometry height="80" width="160" x="1097" y="830" as="geometry" />
</mxCell>
<mxCell id="jfPUc6vKa1uVjcLF1lrC-11" parent="1" style="rounded=0;whiteSpace=wrap;html=1;fontFamily=Architects Daughter;fontSource=https://fonts.googleapis.com/css?family=Architects+Daughter;" value="&lt;font style=&quot;font-size: 30px;&quot;&gt;3&lt;/font&gt;" vertex="1">
<mxGeometry height="80" width="160" x="1377" y="830" as="geometry" />
</mxCell>
<mxCell id="jfPUc6vKa1uVjcLF1lrC-12" parent="1" style="rounded=0;whiteSpace=wrap;html=1;fontFamily=Architects Daughter;fontSource=https%3A%2F%2Ffonts.googleapis.com%2Fcss%3Ffamily%3DArchitects%2BDaughter;" value="" vertex="1">
<mxGeometry height="80" width="160" x="67" y="680" as="geometry" />
</mxCell>
<mxCell id="jfPUc6vKa1uVjcLF1lrC-13" edge="1" parent="1" style="edgeStyle=none;orthogonalLoop=1;jettySize=auto;html=1;rounded=0;fontFamily=Architects Daughter;fontSource=https://fonts.googleapis.com/css?family=Architects+Daughter;fontSize=16;startSize=14;endArrow=none;endSize=14;sourcePerimeterSpacing=8;targetPerimeterSpacing=8;curved=1;endFill=0;" value="">
<mxGeometry relative="1" width="140" as="geometry">
<Array as="points" />
<mxPoint x="227" y="680" as="sourcePoint" />
<mxPoint x="257" y="660" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="jfPUc6vKa1uVjcLF1lrC-14" edge="1" parent="1" style="edgeStyle=none;orthogonalLoop=1;jettySize=auto;html=1;rounded=0;fontFamily=Architects Daughter;fontSource=https://fonts.googleapis.com/css?family=Architects+Daughter;fontSize=16;startSize=14;endArrow=none;endSize=14;sourcePerimeterSpacing=8;targetPerimeterSpacing=8;curved=1;endFill=0;" value="">
<mxGeometry relative="1" width="140" as="geometry">
<Array as="points" />
<mxPoint x="227" y="760" as="sourcePoint" />
<mxPoint x="257" y="740" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="jfPUc6vKa1uVjcLF1lrC-16" edge="1" parent="1" style="edgeStyle=none;orthogonalLoop=1;jettySize=auto;html=1;rounded=0;fontFamily=Architects Daughter;fontSource=https://fonts.googleapis.com/css?family=Architects+Daughter;fontSize=16;startSize=14;endArrow=none;endSize=14;sourcePerimeterSpacing=8;targetPerimeterSpacing=8;curved=1;endFill=0;" value="">
<mxGeometry relative="1" width="140" as="geometry">
<Array as="points" />
<mxPoint x="67" y="680" as="sourcePoint" />
<mxPoint x="97" y="660" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="jfPUc6vKa1uVjcLF1lrC-17" edge="1" parent="1" style="edgeStyle=none;orthogonalLoop=1;jettySize=auto;html=1;rounded=0;fontFamily=Architects Daughter;fontSource=https%3A%2F%2Ffonts.googleapis.com%2Fcss%3Ffamily%3DArchitects%2BDaughter;fontSize=16;startSize=14;endArrow=none;endSize=14;sourcePerimeterSpacing=8;targetPerimeterSpacing=8;curved=1;endFill=0;" value="">
<mxGeometry relative="1" width="140" as="geometry">
<Array as="points" />
<mxPoint x="97" y="660" as="sourcePoint" />
<mxPoint x="257" y="660" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="jfPUc6vKa1uVjcLF1lrC-18" edge="1" parent="1" style="edgeStyle=none;orthogonalLoop=1;jettySize=auto;html=1;rounded=0;fontFamily=Architects Daughter;fontSource=https%3A%2F%2Ffonts.googleapis.com%2Fcss%3Ffamily%3DArchitects%2BDaughter;fontSize=16;startSize=14;endArrow=none;endSize=14;sourcePerimeterSpacing=8;targetPerimeterSpacing=8;curved=1;endFill=0;" value="">
<mxGeometry relative="1" width="140" as="geometry">
<Array as="points" />
<mxPoint x="257" y="740" as="sourcePoint" />
<mxPoint x="257" y="660" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="jfPUc6vKa1uVjcLF1lrC-19" parent="1" style="rounded=0;whiteSpace=wrap;html=1;fontFamily=Architects Daughter;fontSource=https%3A%2F%2Ffonts.googleapis.com%2Fcss%3Ffamily%3DArchitects%2BDaughter;" value="" vertex="1">
<mxGeometry height="80" width="50" x="477" y="390" as="geometry" />
</mxCell>
<mxCell id="jfPUc6vKa1uVjcLF1lrC-20" edge="1" parent="1" source="jfPUc6vKa1uVjcLF1lrC-19" style="edgeStyle=none;orthogonalLoop=1;jettySize=auto;html=1;rounded=0;fontFamily=Architects Daughter;fontSource=https%3A%2F%2Ffonts.googleapis.com%2Fcss%3Ffamily%3DArchitects%2BDaughter;fontSize=16;startSize=14;endArrow=none;endSize=14;sourcePerimeterSpacing=8;targetPerimeterSpacing=8;curved=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;endFill=0;" value="">
<mxGeometry relative="1" width="140" as="geometry">
<Array as="points" />
<mxPoint x="567" y="620" as="sourcePoint" />
<mxPoint x="527" y="530" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="jfPUc6vKa1uVjcLF1lrC-21" edge="1" parent="1" source="jfPUc6vKa1uVjcLF1lrC-19" style="edgeStyle=none;orthogonalLoop=1;jettySize=auto;html=1;rounded=0;fontFamily=Architects Daughter;fontSource=https%3A%2F%2Ffonts.googleapis.com%2Fcss%3Ffamily%3DArchitects%2BDaughter;fontSize=16;startSize=14;endArrow=none;endSize=14;sourcePerimeterSpacing=8;targetPerimeterSpacing=8;curved=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;endFill=0;" value="">
<mxGeometry relative="1" width="140" as="geometry">
<Array as="points" />
<mxPoint x="487" y="520" as="sourcePoint" />
<mxPoint x="477" y="530" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="jfPUc6vKa1uVjcLF1lrC-22" edge="1" parent="1" style="edgeStyle=none;orthogonalLoop=1;jettySize=auto;html=1;rounded=0;fontFamily=Architects Daughter;fontSource=https%3A%2F%2Ffonts.googleapis.com%2Fcss%3Ffamily%3DArchitects%2BDaughter;fontSize=16;startSize=14;endArrow=none;endSize=14;sourcePerimeterSpacing=8;targetPerimeterSpacing=8;curved=1;endFill=0;" value="">
<mxGeometry relative="1" width="140" as="geometry">
<Array as="points" />
<mxPoint x="477" y="530" as="sourcePoint" />
<mxPoint x="527" y="530" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="jfPUc6vKa1uVjcLF1lrC-23" edge="1" parent="1" style="edgeStyle=none;orthogonalLoop=1;jettySize=auto;html=1;rounded=0;fontFamily=Architects Daughter;fontSource=https%3A%2F%2Ffonts.googleapis.com%2Fcss%3Ffamily%3DArchitects%2BDaughter;fontSize=16;startSize=14;endArrow=none;endSize=14;sourcePerimeterSpacing=8;targetPerimeterSpacing=8;curved=1;endFill=0;dashed=1;dashPattern=12 12;" value="">
<mxGeometry relative="1" width="140" as="geometry">
<Array as="points" />
<mxPoint x="477" y="530" as="sourcePoint" />
<mxPoint x="417" y="750" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="jfPUc6vKa1uVjcLF1lrC-24" edge="1" parent="1" style="edgeStyle=none;orthogonalLoop=1;jettySize=auto;html=1;rounded=0;fontFamily=Architects Daughter;fontSource=https://fonts.googleapis.com/css?family=Architects+Daughter;fontSize=16;startSize=14;endArrow=none;endSize=14;sourcePerimeterSpacing=8;targetPerimeterSpacing=8;curved=1;endFill=0;dashed=1;dashPattern=12 12;" value="">
<mxGeometry relative="1" width="140" as="geometry">
<Array as="points" />
<mxPoint x="527" y="530" as="sourcePoint" />
<mxPoint x="587" y="750" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="jfPUc6vKa1uVjcLF1lrC-26" parent="1" style="rounded=0;whiteSpace=wrap;html=1;fontFamily=Architects Daughter;fontSource=https://fonts.googleapis.com/css?family=Architects+Daughter;" value="" vertex="1">
<mxGeometry height="80" width="50" x="307" y="390" as="geometry" />
</mxCell>
<mxCell id="jfPUc6vKa1uVjcLF1lrC-27" edge="1" parent="1" source="jfPUc6vKa1uVjcLF1lrC-26" style="edgeStyle=none;orthogonalLoop=1;jettySize=auto;html=1;rounded=0;fontFamily=Architects Daughter;fontSource=https%3A%2F%2Ffonts.googleapis.com%2Fcss%3Ffamily%3DArchitects%2BDaughter;fontSize=16;startSize=14;endArrow=none;endSize=14;sourcePerimeterSpacing=8;targetPerimeterSpacing=8;curved=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;dashed=1;dashPattern=8 8;endFill=0;" value="">
<mxGeometry relative="1" width="140" as="geometry">
<Array as="points" />
<mxPoint x="357" y="580" as="sourcePoint" />
<mxPoint x="332" y="740" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="jfPUc6vKa1uVjcLF1lrC-29" parent="1" style="shape=note;whiteSpace=wrap;html=1;backgroundOutline=1;darkOpacity=0.05;fillColor=#f0a30a;strokeColor=#BD7000;fillStyle=solid;direction=west;gradientDirection=north;shadow=1;size=20;autosizeText=1;fontFamily=Architects Daughter;fontSource=https%3A%2F%2Ffonts.googleapis.com%2Fcss%3Ffamily%3DArchitects%2BDaughter;fontColor=#000000;fontSize=17;align=left;" value="Лазерный датчик расстояния.&lt;br&gt;В нашем случае он измеряет высоту объекта.&lt;br&gt;Так как камера у нас расположена перпендикулярно движению ленты. И корректно не сможет определить высоту объектов" vertex="1">
<mxGeometry height="160" width="315" x="42" y="210" as="geometry" />
</mxCell>
<mxCell id="jfPUc6vKa1uVjcLF1lrC-30" parent="1" style="shape=note;whiteSpace=wrap;html=1;backgroundOutline=1;darkOpacity=0.05;fillColor=#f0a30a;strokeColor=#BD7000;fillStyle=solid;direction=west;gradientDirection=north;shadow=1;size=20;autosizeText=1;fontFamily=Architects Daughter;fontSource=https://fonts.googleapis.com/css?family=Architects+Daughter;fontColor=#000000;fontSize=24;align=left;" value="Тут у нас установлена камера, первично мы ее откалибруем и сможем визуально оценить размер объекта" vertex="1">
<mxGeometry height="160" width="315" x="387" y="210" as="geometry" />
</mxCell>
<mxCell id="jfPUc6vKa1uVjcLF1lrC-32" parent="1" style="ellipse;whiteSpace=wrap;html=1;fontFamily=Architects Daughter;fontSource=https%3A%2F%2Ffonts.googleapis.com%2Fcss%3Ffamily%3DArchitects%2BDaughter;" value="" vertex="1">
<mxGeometry height="40" width="40" x="804" y="640" as="geometry" />
</mxCell>
<mxCell id="jfPUc6vKa1uVjcLF1lrC-37" edge="1" parent="1" source="jfPUc6vKa1uVjcLF1lrC-38" style="edgeStyle=none;orthogonalLoop=1;jettySize=auto;html=1;rounded=0;fontFamily=Architects Daughter;fontSource=https%3A%2F%2Ffonts.googleapis.com%2Fcss%3Ffamily%3DArchitects%2BDaughter;fontSize=16;startSize=14;endArrow=open;endSize=14;sourcePerimeterSpacing=8;targetPerimeterSpacing=8;curved=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;exitX=0.5;exitY=0;exitDx=0;exitDy=0;exitPerimeter=0;dashed=1;dashPattern=12 12;" target="jfPUc6vKa1uVjcLF1lrC-32" value="">
<mxGeometry relative="1" width="140" as="geometry">
<Array as="points" />
<mxPoint x="847" y="370" as="sourcePoint" />
<mxPoint x="967" y="710" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="jfPUc6vKa1uVjcLF1lrC-38" parent="1" style="shape=note;whiteSpace=wrap;html=1;backgroundOutline=1;darkOpacity=0.05;fillColor=#f0a30a;strokeColor=#BD7000;fillStyle=solid;direction=west;gradientDirection=north;shadow=1;size=20;autosizeText=1;fontFamily=Architects Daughter;fontSource=https://fonts.googleapis.com/css?family=Architects+Daughter;fontColor=#000000;fontSize=26;align=left;" value="Датчик ультразвуковой, определят что объект приехал на сортировку" vertex="1">
<mxGeometry height="160" width="315" x="737" y="190" as="geometry" />
</mxCell>
<mxCell id="jfPUc6vKa1uVjcLF1lrC-39" edge="1" parent="1" source="jfPUc6vKa1uVjcLF1lrC-40" style="edgeStyle=none;orthogonalLoop=1;jettySize=auto;html=1;rounded=0;fontFamily=Architects Daughter;fontSource=https://fonts.googleapis.com/css?family=Architects+Daughter;fontSize=16;startSize=14;endArrow=open;endSize=14;sourcePerimeterSpacing=8;targetPerimeterSpacing=8;curved=1;exitX=0.542;exitY=0.018;exitDx=0;exitDy=0;exitPerimeter=0;dashed=1;dashPattern=12 12;" value="">
<mxGeometry relative="1" width="140" as="geometry">
<Array as="points" />
<mxPoint x="1197" y="360" as="sourcePoint" />
<mxPoint x="937" y="610" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="jfPUc6vKa1uVjcLF1lrC-40" parent="1" style="shape=note;whiteSpace=wrap;html=1;backgroundOutline=1;darkOpacity=0.05;fillColor=#f0a30a;strokeColor=#BD7000;fillStyle=solid;direction=west;gradientDirection=north;shadow=1;size=20;autosizeText=1;fontFamily=Architects Daughter;fontSource=https://fonts.googleapis.com/css?family=Architects+Daughter;fontColor=#000000;fontSize=25;align=left;" value="Сервопривод с ограничителем движения который не дает объекту двигаться дальше." vertex="1">
<mxGeometry height="160" width="315" x="1062" y="190" as="geometry" />
</mxCell>
<mxCell id="jfPUc6vKa1uVjcLF1lrC-42" connectable="0" parent="1" style="group" value="" vertex="1">
<mxGeometry height="80" width="90" x="882" y="620" as="geometry" />
</mxCell>
<mxCell id="jfPUc6vKa1uVjcLF1lrC-31" parent="jfPUc6vKa1uVjcLF1lrC-42" style="rounded=0;whiteSpace=wrap;html=1;fontFamily=Architects Daughter;fontSource=https%3A%2F%2Ffonts.googleapis.com%2Fcss%3Ffamily%3DArchitects%2BDaughter;" value="" vertex="1">
<mxGeometry height="80" width="90" as="geometry" />
</mxCell>
<mxCell id="jfPUc6vKa1uVjcLF1lrC-34" edge="1" parent="jfPUc6vKa1uVjcLF1lrC-42" style="edgeStyle=none;orthogonalLoop=1;jettySize=auto;html=1;rounded=0;fontFamily=Architects Daughter;fontSource=https://fonts.googleapis.com/css?family=Architects+Daughter;fontSize=16;startSize=14;endArrow=none;endSize=14;sourcePerimeterSpacing=8;targetPerimeterSpacing=8;curved=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;endFill=0;" value="">
<mxGeometry relative="1" width="140" as="geometry">
<Array as="points" />
<mxPoint x="45" y="80" as="sourcePoint" />
<mxPoint x="95" y="150" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="jfPUc6vKa1uVjcLF1lrC-35" edge="1" parent="jfPUc6vKa1uVjcLF1lrC-42" style="edgeStyle=none;orthogonalLoop=1;jettySize=auto;html=1;rounded=0;fontFamily=Architects Daughter;fontSource=https://fonts.googleapis.com/css?family=Architects+Daughter;fontSize=16;startSize=14;endArrow=none;endSize=14;sourcePerimeterSpacing=8;targetPerimeterSpacing=8;curved=1;endFill=0;" value="">
<mxGeometry relative="1" width="140" as="geometry">
<Array as="points" />
<mxPoint x="95" y="150" as="sourcePoint" />
<mxPoint x="95" y="40" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="jfPUc6vKa1uVjcLF1lrC-36" edge="1" parent="jfPUc6vKa1uVjcLF1lrC-42" source="jfPUc6vKa1uVjcLF1lrC-31" style="edgeStyle=none;orthogonalLoop=1;jettySize=auto;html=1;rounded=0;fontFamily=Architects Daughter;fontSource=https://fonts.googleapis.com/css?family=Architects+Daughter;fontSize=16;startSize=14;endArrow=none;endSize=14;sourcePerimeterSpacing=8;targetPerimeterSpacing=8;curved=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;endFill=0;" value="">
<mxGeometry relative="1" width="140" as="geometry">
<Array as="points" />
<mxPoint x="45" y="-20" as="sourcePoint" />
<mxPoint x="95" y="40" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="jfPUc6vKa1uVjcLF1lrC-48" parent="1" style="ellipse;whiteSpace=wrap;html=1;fontFamily=Architects Daughter;fontSource=https://fonts.googleapis.com/css?family=Architects+Daughter;" value="" vertex="1">
<mxGeometry height="40" width="40" x="1062" y="640" as="geometry" />
</mxCell>
<mxCell id="jfPUc6vKa1uVjcLF1lrC-49" edge="1" parent="1" source="jfPUc6vKa1uVjcLF1lrC-38" style="edgeStyle=none;orthogonalLoop=1;jettySize=auto;html=1;rounded=0;fontFamily=Architects Daughter;fontSource=https://fonts.googleapis.com/css?family=Architects+Daughter;fontSize=16;startSize=14;endArrow=open;endSize=14;sourcePerimeterSpacing=8;targetPerimeterSpacing=8;curved=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;exitX=0.5;exitY=0;exitDx=0;exitDy=0;exitPerimeter=0;dashed=1;dashPattern=12 12;" target="jfPUc6vKa1uVjcLF1lrC-48" value="">
<mxGeometry relative="1" width="140" as="geometry">
<Array as="points" />
<mxPoint x="948" y="430" as="sourcePoint" />
<mxPoint x="877" y="720" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="jfPUc6vKa1uVjcLF1lrC-50" edge="1" parent="1" source="jfPUc6vKa1uVjcLF1lrC-40" style="edgeStyle=none;orthogonalLoop=1;jettySize=auto;html=1;rounded=0;fontFamily=Architects Daughter;fontSource=https://fonts.googleapis.com/css?family=Architects+Daughter;fontSize=16;startSize=14;endArrow=open;endSize=14;sourcePerimeterSpacing=8;targetPerimeterSpacing=8;curved=1;exitX=0.542;exitY=-0.004;exitDx=0;exitDy=0;exitPerimeter=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;dashed=1;dashPattern=12 12;" target="jfPUc6vKa1uVjcLF1lrC-44" value="">
<mxGeometry relative="1" width="140" as="geometry">
<Array as="points" />
<mxPoint x="1436" y="410" as="sourcePoint" />
<mxPoint x="1167" y="673" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="jfPUc6vKa1uVjcLF1lrC-51" connectable="0" parent="1" style="group" value="" vertex="1">
<mxGeometry height="80" width="90" x="1137" y="620" as="geometry" />
</mxCell>
<mxCell id="jfPUc6vKa1uVjcLF1lrC-44" parent="jfPUc6vKa1uVjcLF1lrC-51" style="rounded=0;whiteSpace=wrap;html=1;fontFamily=Architects Daughter;fontSource=https://fonts.googleapis.com/css?family=Architects+Daughter;" value="" vertex="1">
<mxGeometry height="80" width="90" as="geometry" />
</mxCell>
<mxCell id="jfPUc6vKa1uVjcLF1lrC-45" edge="1" parent="jfPUc6vKa1uVjcLF1lrC-51" style="edgeStyle=none;orthogonalLoop=1;jettySize=auto;html=1;rounded=0;fontFamily=Architects Daughter;fontSource=https://fonts.googleapis.com/css?family=Architects+Daughter;fontSize=16;startSize=14;endArrow=none;endSize=14;sourcePerimeterSpacing=8;targetPerimeterSpacing=8;curved=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;endFill=0;" value="">
<mxGeometry relative="1" width="140" as="geometry">
<Array as="points" />
<mxPoint x="45" y="80" as="sourcePoint" />
<mxPoint x="140" y="90" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="jfPUc6vKa1uVjcLF1lrC-46" edge="1" parent="jfPUc6vKa1uVjcLF1lrC-51" style="edgeStyle=none;orthogonalLoop=1;jettySize=auto;html=1;rounded=0;fontFamily=Architects Daughter;fontSource=https://fonts.googleapis.com/css?family=Architects+Daughter;fontSize=16;startSize=14;endArrow=none;endSize=14;sourcePerimeterSpacing=8;targetPerimeterSpacing=8;curved=1;endFill=0;" value="">
<mxGeometry relative="1" width="140" as="geometry">
<Array as="points" />
<mxPoint x="140" y="90" as="sourcePoint" />
<mxPoint x="140" y="-10" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="jfPUc6vKa1uVjcLF1lrC-47" edge="1" parent="jfPUc6vKa1uVjcLF1lrC-51" source="jfPUc6vKa1uVjcLF1lrC-44" style="edgeStyle=none;orthogonalLoop=1;jettySize=auto;html=1;rounded=0;fontFamily=Architects Daughter;fontSource=https://fonts.googleapis.com/css?family=Architects+Daughter;fontSize=16;startSize=14;endArrow=none;endSize=14;sourcePerimeterSpacing=8;targetPerimeterSpacing=8;curved=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;endFill=0;" value="">
<mxGeometry relative="1" width="140" as="geometry">
<Array as="points" />
<mxPoint x="45" y="-20" as="sourcePoint" />
<mxPoint x="140" y="-10" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="jfPUc6vKa1uVjcLF1lrC-52" connectable="0" parent="1" style="group" value="" vertex="1">
<mxGeometry height="80" width="90" x="1407" y="620" as="geometry" />
</mxCell>
<mxCell id="jfPUc6vKa1uVjcLF1lrC-53" parent="jfPUc6vKa1uVjcLF1lrC-52" style="rounded=0;whiteSpace=wrap;html=1;fontFamily=Architects Daughter;fontSource=https://fonts.googleapis.com/css?family=Architects+Daughter;" value="" vertex="1">
<mxGeometry height="80" width="90" as="geometry" />
</mxCell>
<mxCell id="jfPUc6vKa1uVjcLF1lrC-54" edge="1" parent="jfPUc6vKa1uVjcLF1lrC-52" style="edgeStyle=none;orthogonalLoop=1;jettySize=auto;html=1;rounded=0;fontFamily=Architects Daughter;fontSource=https://fonts.googleapis.com/css?family=Architects+Daughter;fontSize=16;startSize=14;endArrow=none;endSize=14;sourcePerimeterSpacing=8;targetPerimeterSpacing=8;curved=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;endFill=0;" value="">
<mxGeometry relative="1" width="140" as="geometry">
<Array as="points" />
<mxPoint x="45" y="80" as="sourcePoint" />
<mxPoint x="140" y="90" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="jfPUc6vKa1uVjcLF1lrC-55" edge="1" parent="jfPUc6vKa1uVjcLF1lrC-52" style="edgeStyle=none;orthogonalLoop=1;jettySize=auto;html=1;rounded=0;fontFamily=Architects Daughter;fontSource=https://fonts.googleapis.com/css?family=Architects+Daughter;fontSize=16;startSize=14;endArrow=none;endSize=14;sourcePerimeterSpacing=8;targetPerimeterSpacing=8;curved=1;endFill=0;" value="">
<mxGeometry relative="1" width="140" as="geometry">
<Array as="points" />
<mxPoint x="140" y="90" as="sourcePoint" />
<mxPoint x="140" y="-10" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="jfPUc6vKa1uVjcLF1lrC-56" edge="1" parent="jfPUc6vKa1uVjcLF1lrC-52" source="jfPUc6vKa1uVjcLF1lrC-53" style="edgeStyle=none;orthogonalLoop=1;jettySize=auto;html=1;rounded=0;fontFamily=Architects Daughter;fontSource=https://fonts.googleapis.com/css?family=Architects+Daughter;fontSize=16;startSize=14;endArrow=none;endSize=14;sourcePerimeterSpacing=8;targetPerimeterSpacing=8;curved=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;endFill=0;" value="">
<mxGeometry relative="1" width="140" as="geometry">
<Array as="points" />
<mxPoint x="45" y="-20" as="sourcePoint" />
<mxPoint x="140" y="-10" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="jfPUc6vKa1uVjcLF1lrC-57" parent="1" style="ellipse;whiteSpace=wrap;html=1;fontFamily=Architects Daughter;fontSource=https://fonts.googleapis.com/css?family=Architects+Daughter;" value="" vertex="1">
<mxGeometry height="40" width="40" x="1337" y="640" as="geometry" />
</mxCell>
<mxCell id="jfPUc6vKa1uVjcLF1lrC-58" edge="1" parent="1" source="jfPUc6vKa1uVjcLF1lrC-38" style="edgeStyle=none;orthogonalLoop=1;jettySize=auto;html=1;rounded=0;fontFamily=Architects Daughter;fontSource=https://fonts.googleapis.com/css?family=Architects+Daughter;fontSize=16;startSize=14;endArrow=open;endSize=14;sourcePerimeterSpacing=8;targetPerimeterSpacing=8;curved=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;exitX=0.5;exitY=0;exitDx=0;exitDy=0;exitPerimeter=0;dashed=1;dashPattern=12 12;" target="jfPUc6vKa1uVjcLF1lrC-57" value="">
<mxGeometry relative="1" width="140" as="geometry">
<Array as="points" />
<mxPoint x="957" y="530" as="sourcePoint" />
<mxPoint x="1144" y="820" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="jfPUc6vKa1uVjcLF1lrC-59" edge="1" parent="1" style="edgeStyle=none;orthogonalLoop=1;jettySize=auto;html=1;rounded=0;fontFamily=Architects Daughter;fontSource=https://fonts.googleapis.com/css?family=Architects+Daughter;fontSize=16;startSize=14;endArrow=open;endSize=14;sourcePerimeterSpacing=8;targetPerimeterSpacing=8;curved=1;dashed=1;dashPattern=12 12;" value="">
<mxGeometry relative="1" width="140" as="geometry">
<Array as="points" />
<mxPoint x="1207" y="350" as="sourcePoint" />
<mxPoint x="1447" y="620" as="targetPoint" />
</mxGeometry>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

BIN
specification/mvp_1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

View File

@@ -0,0 +1,97 @@
Привет! Как инженер-конструктор и эксперт по электронике, я с удовольствием помогу тебе спроектировать этот макет. Задача ясна: создаем систему автоматизированной сортировки с точным управлением скоростью и маршрутизацией по высоте.
Давай разберем твои вопросы по порядку.
### 1. Выбор двигателя: Шаговый vs Коллекторный
Для твоей задачи — **максимально точного выдерживания скорости** и управления с микроконтроллера — выбор очевиден.
| Характеристика | Коллекторный двигатель (DC) | Шаговый двигатель (Stepper) |
| :--- | :--- | :--- |
| **Точность скорости** | Низкая. Скорость зависит от напряжения и нагрузки. Для точности нужен энкодер (обратная связь). | **Высокая**. Скорость задается частотой импульсов. Постоянна при заданной нагрузке. |
| **Управление** | Простое (PWM/ШИМ). | Требует специальный драйвер (например, A4988 или TMC2208). |
| **Пуск/Стоп** | Плавный разгон, сложно точно позиционировать. | **Идеальный**. Можно точно задать количество шагов и мгновенно остановиться. |
| **Крутящий момент** | Высокий на высоких оборотах. | Высокий на низких скоростях (идеально для ленты). |
| **Сложность реализации** | Очень низкая. | Средняя (нужен драйвер и настройка тока). |
**Мой вердикт:** Используй **шаговый двигатель (например, NEMA 17)**.
Если ты хочешь «выдерживать скорость максимально точно», обычный DC-мотор тебя разочарует: при увеличении веса объекта на ленте скорость будет падать. Шаговый же двигатель будет поддерживать заданный темп, пока не будет достигнут предел его крутящего момента.
---
### 2. Селекторы (Сервоприводы)
Использование сервоприводов с барьером под 45° — это классическое и эффективное решение для макета.
**Советы по реализации:**
* **Механика:** Чтобы объект не застревал и не «зажевывался», сделай барьер обтекаемым или используй гладкий пластик/алюминий.
* **Угол:** 45 градусов — оптимально, но убедись, что при срабатывании сервопривод не толкает объект слишком резко, чтобы тот не перевернулся.
* **Логика:** В промышленном подходе, как отмечено в контексте, важно отслеживать «корректность маршрутизации» [1]. Рекомендую добавить в код проверку: действительно ли объект ушел в нужную зону, или произошла ошибка.
---
### 3. Датчик определения высоты
Для измерения высоты объекта сверху, когда лента движется, тебе подойдут три варианта в зависимости от требуемой точности:
1. **Ультразвуковой датчик (HC-SR04):** Самый дешевый и популярный.
* *Минус:* Широкий луч (может «цеплять» борта конвейера) и низкая скорость обновления.
2. **ИК-датчик расстояния (Sharp GP2Y0A21YK0F):** Более точный, узкий луч.
* *Минус:* Аналоговый сигнал, чувствителен к цвету объекта (черный поглощает ИК).
3. **Лазерный ToF-датчик (VL53L0X / VL53L1X):** **Мой выбор для этого проекта.**
* *Плюсы:* Измеряет время полета фотона (Time-of-Flight), работает независимо от цвета объекта, очень высокая точность (до миллиметров) и очень узкий луч.
---
### Итоговая архитектура (предложение):
* **Контроллер:** Arduino (Uno или Mega, если планируешь расширять количество датчиков).
* **Привод ленты:** Шаговый двигатель NEMA 17 $\rightarrow$ Драйвер A4988/TMC2208 $\rightarrow$ Arduino.
* **Сортировка:** 3 $\times$ Сервоприводы SG90 или MG90S (с металлическим редуктором надежнее).
* **Датчик высоты:** VL53L0X (I2C интерфейс).
* **Питание:** Отдельный блок питания 12В для двигателя и 5В для Arduino/сервоприводов (общий минус/земля).
**Важное замечание из спецификации:**
Не забудь предусмотреть в логике категорию «Не подходит для сортировки» [1]. Если датчик зафиксирует объект, который либо слишком высокий, либо слишком низкий (выходит за допустимые габариты), система должна отправить его в отдельный поток или оставить на ленте до конца [1].
Поскольку мы проектируем макет, важно учитывать не только функциональность, но и ограничения, упомянутые в техническом контексте: доступную площадь участка [1], разнообразие форм товаров (включая круглые в сечении) и необходимость бережного обращения с хрупкими или ценными объектами [1].
# Сервоприводы с барьером — это надежный вариант, но в зависимости от того, какие объекты вы будете сортировать, рассмотрите следующие альтернативы:
### 1. Электромагнитные толкатели (Solenoids)
Вместо плавного поворота барьера, соленоид дает резкий линейный толчок.
* **Плюсы:** Очень высокая скорость срабатывания, компактность [1].
* **Минусы:** Резкий удар. Если ваши объекты хрупкие, этот вариант **не рекомендуется**, так как он противоречит требованию к «мягкости обращения» [1].
* **Когда использовать:** Для прочных, тяжелых или однотипных деталей.
### 2. Откидная платформа (Tipping Platform)
Это участок конвейера, который может наклоняться в сторону или под ним открывается люк.
* **Плюсы:** Позволяет перемещать объекты за счет гравитации. Это один из самых бережных способов перемещения, что важно для ценных или хрупких товаров [1].
* **Минусы:** Требует более сложной механической интеграции в саму ленту.
* **Когда использовать:** Если приоритетом является сохранность объекта и минимизация механических ударов.
### 3. Поворотный диск-распределитель (Rotating Disc)
В конце основной ленты устанавливается диск, который поворачивается на определенный угол, направляя объект в один из трех лотков.
* **Плюсы:** Занимает минимум места (оптимально для ограниченной площади участка [1]). Отлично работает с объектами, имеющими круг в сечении [1].
* **Минусы:** Требует точной синхронизации остановки ленты и поворота диска.
* **Когда использовать:** Если макет должен быть максимально компактным, а объекты имеют схожую форму (например, цилиндры или сферы).
### 4. Боковой конвейер (Cross-Belt / Diverter)
Это короткий перпендикулярный отрезок ленты, который включается только тогда, когда объект должен быть сброшен в зону.
* **Плюсы:** Самый профессиональный и «мягкий» метод. Идеален для хрупких товаров [1].
* **Минусы:** Высокая стоимость и сложность (нужно еще 3 маленьких мотора и дополнительные ленты).
* **Когда использовать:** Если вы хотите создать максимально приближенный к реальному промышленному производству макет.
### Сводная таблица для выбора:
| Вариант | Бережность (для хрупких [1]) | Компактность (площадь [1]) | Сложность реализации | Работа с круглыми объектами [1] |
| :--- | :--- | :--- | :--- | :--- |
| **Серво-барьер** | Средняя | Высокая | Низкая | Хорошо |
| **Соленоид** | Низкая | Очень высокая | Низкая | Средне |
| **Платформа** | Высокая | Средняя | Средняя | Хорошо |
| **Диск** | Средняя | Очень высокая | Средняя | Идеально |
| **Боковой конвейер**| Очень высокая | Низкая | Высокая | Хорошо |
**Моя рекомендация:**
Если вы хотите сохранить простоту управления через Arduino, но при этом учесть требования к бережности и компактности [1], я рекомендую остаться на **сервоприводах**, но изменить конструкцию барьера: вместо «толкателя» сделать **«поворотный желоб»** или **«шлюз»**. Это позволит объекту плавно скатываться в нужную зону, не подвергаясь резким ударам.

View File

@@ -1,131 +0,0 @@
import { useEffect, useRef, useState, useCallback } from 'react';
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import MainPage from './pages/MainPage';
import DocumentationPage from './pages/DocumentationPage';
import {
createPlaybackState,
startPlayback,
pausePlayback,
resumePlayback,
stopPlayback,
updatePlayback,
seekToCase,
seekNextCase,
seekPrevCase,
setPlaybackSpeed,
type ContinuousPlaybackState,
type PlaybackSpeed,
} from './domain/continuousPlayback';
function AppContent() {
const [playback, setPlayback] = useState<ContinuousPlaybackState>(() => createPlaybackState());
const playbackRafRef = useRef<number | null>(null);
const playbackLastRef = useRef(performance.now());
const PLAYBACK_INTERVAL = 50;
// Public page autostarts the sorter loop; ?playback=paused keeps it idle
// (used by screenshot/debug tooling). First item is gated in the 3D scene
// until PRODUCT_ASSETS_READY (see SorterDigitalTwinContinuous).
useEffect(() => {
const params = new URLSearchParams(window.location.search);
if (params.get('playback') === 'paused') return;
setPlayback((prev) => (prev.status === 'idle' ? startPlayback(prev) : prev));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (playback.status !== 'running') {
if (playbackRafRef.current) {
cancelAnimationFrame(playbackRafRef.current);
playbackRafRef.current = null;
}
return;
}
const tick = () => {
const now = performance.now();
const deltaMs = now - playbackLastRef.current;
if (deltaMs >= PLAYBACK_INTERVAL) {
playbackLastRef.current = now;
setPlayback((prev) => updatePlayback(prev, deltaMs));
}
playbackRafRef.current = requestAnimationFrame(tick);
};
playbackLastRef.current = performance.now();
playbackRafRef.current = requestAnimationFrame(tick);
return () => {
if (playbackRafRef.current) {
cancelAnimationFrame(playbackRafRef.current);
playbackRafRef.current = null;
}
};
}, [playback.status]);
const handleMainPlay = useCallback(() => {
setPlayback((prev) => {
if (prev.status === 'paused') {
return resumePlayback(prev);
}
return startPlayback(prev);
});
}, []);
const handleMainPause = useCallback(() => {
setPlayback((prev) => pausePlayback(prev));
}, []);
const handleMainStop = useCallback(() => {
setPlayback(stopPlayback);
}, []);
const handleSeekCase = useCallback((index: number) => {
setPlayback((prev) => seekToCase(prev, index));
}, []);
const handleSeekNext = useCallback(() => {
setPlayback((prev) => seekNextCase(prev));
}, []);
const handleSeekPrev = useCallback(() => {
setPlayback((prev) => seekPrevCase(prev));
}, []);
const handleSetSpeed = useCallback((speed: PlaybackSpeed) => {
setPlayback((prev) => setPlaybackSpeed(prev, speed));
}, []);
return (
<Routes>
<Route
path="/"
element={
<MainPage
playback={playback}
onPlay={handleMainPlay}
onPause={handleMainPause}
onStop={handleMainStop}
onSeekCase={handleSeekCase}
onSeekNext={handleSeekNext}
onSeekPrev={handleSeekPrev}
onSetSpeed={handleSetSpeed}
/>
}
/>
<Route path="/documentation" element={<DocumentationPage />} />
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
);
}
export default function App() {
return (
<BrowserRouter>
<AppContent />
</BrowserRouter>
);
}

View File

@@ -1,35 +0,0 @@
import { NavLink } from 'react-router-dom';
interface AppNavProps {
/** Overlay (desktop sim), bar (mobile full-width), solid (docs header). */
variant?: 'overlay' | 'bar' | 'solid';
}
/**
* Product navigation — only Simulation and Documentation.
*/
export default function AppNav({ variant = 'solid' }: AppNavProps) {
return (
<nav
className={`app-nav app-nav-${variant}`}
aria-label="Основная навигация"
data-testid="app-nav"
>
<NavLink
to="/"
end
className={({ isActive }) => `app-nav-link${isActive ? ' active' : ''}`}
data-testid="nav-simulation"
>
Симуляция
</NavLink>
<NavLink
to="/documentation"
className={({ isActive }) => `app-nav-link${isActive ? ' active' : ''}`}
data-testid="nav-documentation"
>
Документация
</NavLink>
</nav>
);
}

View File

@@ -1,51 +0,0 @@
import { useEffect, useState } from 'react';
export interface VersionInfo {
commit: string;
branch: string;
builtAt: string;
release: string;
}
/**
* Engineering-only build chip. Hidden in presentation mode.
* Visible with ?perf=1 or ?build=1.
*/
export default function BuildIdentityBadge() {
const [info, setInfo] = useState<VersionInfo | null>(null);
const [show, setShow] = useState(false);
const [inPresentation, setInPresentation] = useState(false);
useEffect(() => {
const params = new URLSearchParams(window.location.search);
setShow(params.get('perf') === '1' || params.get('build') === '1');
}, []);
useEffect(() => {
if (!show) return;
let cancelled = false;
fetch('/version.json', { cache: 'no-store' })
.then((r) => (r.ok ? r.json() : null))
.then((j) => {
if (!cancelled && j?.commit) setInfo(j as VersionInfo);
})
.catch(() => undefined);
const id = window.setInterval(() => {
setInPresentation(!!document.querySelector('.presentation-mode'));
}, 500);
return () => {
cancelled = true;
window.clearInterval(id);
};
}, [show]);
if (!show || !info || inPresentation) return null;
return (
<div className="build-identity" data-testid="build-identity" title={`${info.branch} · ${info.builtAt}`}>
<span className="build-identity-label">build</span>
<span className="build-identity-commit">{info.commit}</span>
<span className="build-identity-release">{info.release}</span>
</div>
);
}

View File

@@ -1,227 +0,0 @@
/**
* CV Inspection Overlay — industrial measurement system monitor.
* Shows stepper, laser, stereo camera data and classification results.
*/
import type { MeasurementData } from '../domain/measurementSystem';
import { getStageLabel } from '../domain/measurementSystem';
import { DIMENSION_LIMITS, isCircularCrossSection } from '../domain/classifier';
interface CVInspectionOverlayProps {
data: MeasurementData;
visible: boolean;
}
const SHAPE_ICONS: Record<string, string> = {
box: '▭',
round: '◯',
irregular: '◇',
};
export default function CVInspectionOverlay({ data, visible }: CVInspectionOverlayProps) {
if (!visible) return null;
const {
stage,
stepCount,
mmPerStep,
measuredLengthMm,
pulseActive,
laserDistanceMm,
laserMountHeightMm,
measuredHeightMm,
laserBeamActive,
measuredWidthMm,
roundnessK,
stereoActive,
confidence,
dimensionsPass,
shapeResult,
finalCategory,
command,
cPriorityApplied,
isLowConfidence,
classificationReason,
classificationLabel,
itemTitle,
itemDimensions,
} = data;
const confidencePercent = Math.round(confidence * 100);
const roundnessPercent = Math.round(roundnessK * 100);
const stageLabel = getStageLabel(stage);
const categoryColors: Record<string, string> = {
B: '#22c55e',
C: '#f97316',
D: '#8b5cf6',
};
const isActive = stage !== 'idle';
return (
<div className="cv-overlay">
<div className="cv-header">
<span className="cv-icon"></span>
<span className="cv-title">MEASUREMENT</span>
<span className={`cv-status ${isActive ? 'active' : 'idle'}`}>{stageLabel}</span>
</div>
<div className="cv-body">
{/* Item info */}
<div className="cv-row cv-item-row">
<span className="cv-label">ITEM</span>
<span className="cv-value">{itemTitle}</span>
</div>
<div className="cv-divider" />
{/* Stepper motor section */}
<div className="cv-section-header">
<span className={`cv-indicator ${pulseActive ? 'pulse' : ''}`}></span>
STEPPER LENGTH
</div>
<div className="cv-row cv-compact">
<span className="cv-label">Pulses</span>
<span className="cv-value cv-mono">
{stepCount.toLocaleString()}
{pulseActive && <span className="cv-blink"> </span>}
</span>
</div>
<div className="cv-row cv-compact">
<span className="cv-label">mm/step</span>
<span className="cv-value cv-mono">{mmPerStep.toFixed(3)}</span>
</div>
<div className="cv-row">
<span className="cv-label">Length</span>
<span className="cv-value cv-result">{measuredLengthMm} mm</span>
</div>
<div className="cv-divider" />
{/* Laser rangefinder section */}
<div className="cv-section-header">
<span className={`cv-indicator ${laserBeamActive ? 'active' : ''}`}></span>
LASER HEIGHT
</div>
<div className="cv-row cv-compact">
<span className="cv-label">Mount</span>
<span className="cv-value cv-mono">{laserMountHeightMm} mm</span>
</div>
<div className="cv-row cv-compact">
<span className="cv-label">Distance</span>
<span className="cv-value cv-mono">{laserDistanceMm} mm</span>
</div>
<div className="cv-row">
<span className="cv-label">Height</span>
<span className="cv-value cv-result">{measuredHeightMm} mm</span>
</div>
<div className="cv-divider" />
{/* Stereo camera section */}
<div className="cv-section-header">
<span className={`cv-indicator ${stereoActive ? 'active' : ''}`}></span>
STEREO WIDTH/SHAPE
</div>
<div className="cv-row">
<span className="cv-label">Width</span>
<span className="cv-value cv-result">{measuredWidthMm} mm</span>
</div>
<div className="cv-row">
<span className="cv-label">Shape</span>
<span className="cv-value cv-shape">
<span className="shape-icon">{SHAPE_ICONS[shapeResult]}</span>
{shapeResult}
</span>
</div>
<div className="cv-row">
<span className="cv-label">Roundness</span>
<span className={`cv-value ${isCircularCrossSection(roundnessK) ? 'warning' : ''}`}>
K = {roundnessK.toFixed(2)} ({roundnessPercent}%)
{isCircularCrossSection(roundnessK) && (
<span className="cv-flag"> &gt;{DIMENSION_LIMITS.roundnessThreshold}</span>
)}
</span>
</div>
<div className="cv-divider" />
{/* Decision section */}
<div className="cv-section-header">
<span className="cv-indicator"></span>
PLC DECISION
</div>
<div className="cv-row">
<span className="cv-label">Dims</span>
<span className={`cv-value ${dimensionsPass ? 'pass' : 'fail'}`}>
{dimensionsPass ? 'PASS' : 'FAIL'}
<span className="cv-dims-detail">
{' '}({itemDimensions.width}×{itemDimensions.depth}×{itemDimensions.height})
</span>
</span>
</div>
<div className="cv-row">
<span className="cv-label">Confidence</span>
<span className={`cv-value ${isLowConfidence ? 'warning' : ''}`}>
{confidencePercent}%
{isLowConfidence && <span className="cv-flag"> LOW</span>}
</span>
</div>
{finalCategory && (
<div className="cv-row cv-result-row">
<span className="cv-label">CLASS</span>
<span
className="cv-value cv-category"
style={{ color: categoryColors[finalCategory] }}
>
{finalCategory}
{cPriorityApplied && <span className="cv-priority"> (C priority)</span>}
</span>
</div>
)}
{classificationReason && (
<div className="cv-row cv-reason-row">
<span className="cv-label">RULE</span>
<span className="cv-value cv-reason">
{classificationLabel ? `${classificationLabel}` : ''}
{classificationReason}
</span>
</div>
)}
<div className="cv-row cv-command-row">
<span className="cv-label">CMD</span>
<span
className="cv-value cv-command"
style={{ color: finalCategory ? categoryColors[finalCategory] : '#64748b' }}
>
{command}
</span>
</div>
{/* Warnings */}
{cPriorityApplied && (
<div className="cv-warning cv-cpriority">
<span className="warning-icon"></span>
<span className="warning-text">Dims fail overrides roundness C</span>
</div>
)}
{isLowConfidence && (
<div className="cv-warning">
<span className="warning-icon"></span>
<span className="warning-text">Low confidence, rule-based fallback</span>
</div>
)}
</div>
<div className="cv-footer">
<span className="cv-live"> LIVE</span>
<span className="cv-fps">PLC</span>
</div>
</div>
);
}

View File

@@ -1,302 +0,0 @@
import type { Category, MachineState, SimulationState } from '../domain/types';
const routeColors: Record<Category, string> = {
B: '#4ade80',
C: '#f59e0b',
D: '#c084fc',
};
function progressForState(state: MachineState, elapsedMs: number): number {
const ratios: Partial<Record<MachineState, [number, number, number]>> = {
MOVING_TO_CAMERA: [0.06, 0.34, 1200],
DETECTING: [0.34, 0.36, 900],
MOVING_TO_GATE: [0.36, 0.64, 1300],
WAITING_AT_GATE: [0.64, 0.65, 800],
CLASSIFYING: [0.65, 0.66, 700],
ROUTE_TO_B: [0.66, 0.93, 1200],
ROUTE_TO_C: [0.66, 0.78, 1200],
ROUTE_TO_D: [0.66, 0.78, 1200],
RETURN_HOME: [0.93, 0.94, 700],
};
const segment = ratios[state];
if (!segment) {
return state === 'IDLE' ? 0 : 0.65;
}
const [from, to, duration] = segment;
return from + (to - from) * Math.min(elapsedMs / duration, 1);
}
function itemPosition(simulation: SimulationState): { x: number; y: number } {
const progress = progressForState(simulation.machineState, simulation.elapsedInStateMs);
const baseX = 126 + progress * 790;
const beltY = 360;
if (simulation.machineState === 'ROUTE_TO_C') {
return { x: baseX, y: beltY + Math.min(simulation.elapsedInStateMs / 1200, 1) * 172 };
}
if (simulation.machineState === 'ROUTE_TO_D') {
return { x: baseX, y: beltY - Math.min(simulation.elapsedInStateMs / 1200, 1) * 172 };
}
return { x: baseX, y: beltY };
}
function DimensionLine({ x1, y1, x2, y2, label }: { x1: number; y1: number; x2: number; y2: number; label: string }) {
const labelX = (x1 + x2) / 2;
const labelY = (y1 + y2) / 2;
return (
<g className="dimension-line">
<line x1={x1} y1={y1} x2={x2} y2={y2} />
<circle cx={x1} cy={y1} r="3" />
<circle cx={x2} cy={y2} r="3" />
<text x={labelX} y={labelY - 8}>{label}</text>
</g>
);
}
function LegendItem({ color, label }: { color: string; label: string }) {
return (
<g className="legend-item">
<rect width="10" height="10" fill={color} rx="2" />
<text x="16" y="10">{label}</text>
</g>
);
}
interface SorterSceneProps {
simulation: SimulationState;
variant?: 'full' | 'simple';
}
export default function SorterScene({ simulation, variant = 'full' }: SorterSceneProps) {
const current = simulation.currentItem;
const category = current?.classification.category;
const color = category ? routeColors[category] : '#38bdf8';
const position = itemPosition(simulation);
const detecting = simulation.machineState === 'DETECTING';
const classifying = simulation.machineState === 'CLASSIFYING' || simulation.machineState === 'WAITING_AT_GATE';
const routeVisible = simulation.machineState.startsWith('ROUTE_TO_');
const stopped = simulation.machineState === 'FAULT' || simulation.machineState === 'EMERGENCY_STOP';
const itemWidth = current ? Math.max(24, Math.min(74, current.item.dimensionsMm.width / 6)) : 56;
const itemHeight = current ? Math.max(18, Math.min(58, current.item.dimensionsMm.depth / 5.5)) : 48;
const isSimple = variant === 'simple';
return (
<div className={`scene-wrap scene-${variant}`}>
{!isSimple ? (
<div className="scene-title-row">
<div>
<p className="eyebrow">Work zone 6000 x 10000 mm / conveyor 500 mm</p>
<h2>Engineering layout, sensors and routing commands</h2>
</div>
<div className={`machine-state-chip ${stopped ? 'fault-chip' : ''}`}>{simulation.machineState}</div>
</div>
) : (
<div className="scene-simple-header">
<p className="eyebrow">Конвейер · камера · classifier · gate · зоны B/C/D</p>
<div className={`machine-state-chip ${stopped ? 'fault-chip' : ''}`}>{simulation.machineState}</div>
</div>
)}
<svg viewBox="0 0 1120 720" role="img" aria-label="Sorter simulation scene" className="sorter-svg">
<defs>
<pattern id={`gridMinor-${variant}`} width="24" height="24" patternUnits="userSpaceOnUse">
<path d="M 24 0 L 0 0 0 24" fill="none" stroke="#12283b" strokeWidth="1" />
</pattern>
<pattern id={`gridMajor-${variant}`} width="120" height="120" patternUnits="userSpaceOnUse">
<rect width="120" height="120" fill={`url(#gridMinor-${variant})`} />
<path d="M 120 0 L 0 0 0 120" fill="none" stroke="#244863" strokeWidth="1.4" />
</pattern>
{(['B', 'C', 'D'] as Category[]).map((route) => (
<marker key={route} id={`arrow${route}-${variant}`} markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto">
<path d="M0,0 L0,6 L9,3 z" fill={routeColors[route]} />
</marker>
))}
</defs>
<rect x="24" y="26" width="1072" height="632" rx="12" fill="#07111d" stroke="#244863" />
{!isSimple ? (
<rect x="64" y="82" width="928" height="520" fill={`url(#gridMajor-${variant})`} opacity="0.9" />
) : null}
{!isSimple ? (
<>
<text x="78" y="74" className="scale-label left-label">Scaled plan: 6000 mm x 10000 mm work cell</text>
<DimensionLine x1={64} y1={626} x2={992} y2={626} label="6000 mm work zone width" />
<DimensionLine x1={1024} y1={82} x2={1024} y2={602} label="10000 mm work zone length" />
<DimensionLine x1={92} y1={314} x2={92} y2={406} label="500 mm conveyor" />
</>
) : null}
<g className="zone zone-a">
<rect x="88" y="292" width="128" height="136" rx="8" />
<text x="152" y={isSimple ? 370 : 282}>{isSimple ? 'A' : 'A feed zone'}</text>
</g>
<g className={`zone zone-b ${category === 'B' ? 'zone-active' : ''}`}>
<rect x="842" y="292" width="136" height="136" rx="8" />
<text x="910" y={isSimple ? 370 : 282} className={isSimple ? 'zone-label-large' : undefined}>
{isSimple ? 'B' : 'B main sorter'}
</text>
</g>
<g className={`zone zone-d ${category === 'D' ? 'zone-active' : ''}`}>
<rect x="642" y="112" width="210" height="124" rx="8" />
<text x="747" y={isSimple ? 185 : 102} className={isSimple ? 'zone-label-large' : undefined}>
{isSimple ? 'D' : 'D roll-cage 1200 x 800 x 800 mm'}
</text>
</g>
<g className={`zone zone-c ${category === 'C' ? 'zone-active' : ''}`}>
<rect x="642" y="486" width="210" height="124" rx="8" />
<text x="747" y={isSimple ? 560 : 632} className={isSimple ? 'zone-label-large' : undefined}>
{isSimple ? 'C' : 'C roll-cage 1200 x 800 x 800 mm'}
</text>
</g>
<rect className={stopped ? 'conveyor stopped' : 'conveyor'} x="106" y="314" width="850" height="92" rx="6" />
<line x1="126" y1="360" x2="936" y2="360" className="belt-center" />
{!isSimple
? Array.from({ length: 18 }).map((_, index) => (
<line key={index} x1={132 + index * 44} y1="324" x2={158 + index * 44} y2="396" className="roller-line" />
))
: null}
<g className={simulation.sensors.camera.active ? 'device active' : 'device'}>
<rect x="350" y="218" width="76" height="50" rx="6" />
<line x1="388" y1="268" x2="388" y2="314" />
<text x="388" y="208">{isSimple ? 'Camera' : 'Camera / bbox'}</text>
</g>
{!isSimple ? (
<>
<g className={simulation.sensors.laser.active ? 'device active' : 'device'}>
<rect x="474" y="218" width="76" height="50" rx="6" />
<line x1="512" y1="268" x2="512" y2="314" />
<text x="512" y="208">Laser height</text>
</g>
<g className={simulation.sensors.ultrasound.active ? 'device active' : 'device'}>
<circle cx="646" cy="243" r="28" />
<line x1="646" y1="271" x2="646" y2="314" />
<text x="646" y="208">Ultrasonic gate</text>
</g>
</>
) : (
<g className={classifying || simulation.sensors.ultrasound.active ? 'device active' : 'device'}>
<rect x="520" y="218" width="110" height="50" rx="6" />
<line x1="575" y1="268" x2="575" y2="314" />
<text x="575" y="208">Classifier</text>
</g>
)}
<g className={simulation.gate.open ? 'gate open' : 'gate closed'}>
<line x1="708" y1="296" x2="708" y2="424" />
<text x="746" y="300">{isSimple ? 'Gate' : `Stop-gate ${simulation.gate.open ? 'open' : 'closed'}`}</text>
</g>
{!isSimple ? (
<>
<g className={`pusher ${simulation.actuators.pusherC}`}>
<rect x="625" y="424" width="174" height="34" rx="6" />
<text x="712" y="476">Pusher C command</text>
</g>
<g className={`pusher ${simulation.actuators.pusherD}`}>
<rect x="625" y="262" width="174" height="34" rx="6" />
<text x="712" y="256">Pusher D command</text>
</g>
</>
) : null}
<line
x1="706"
y1="360"
x2="928"
y2="360"
className={`route-guide route-b ${category === 'B' && routeVisible ? 'route-active' : ''}`}
markerEnd={`url(#arrowB-${variant})`}
/>
<line
x1="706"
y1="376"
x2="748"
y2="548"
className={`route-guide route-c ${category === 'C' && routeVisible ? 'route-active' : ''}`}
markerEnd={`url(#arrowC-${variant})`}
/>
<line
x1="706"
y1="344"
x2="748"
y2="174"
className={`route-guide route-d ${category === 'D' && routeVisible ? 'route-active' : ''}`}
markerEnd={`url(#arrowD-${variant})`}
/>
{routeVisible && category ? (
<g className="route-command">
<rect x="788" y="326" width="156" height="34" rx="8" fill={routeColors[category]} />
<text x="866" y="348">{simulation.machineState}</text>
</g>
) : null}
{current ? (
<g>
<rect
x={position.x - itemWidth / 2}
y={position.y - itemHeight / 2}
width={itemWidth}
height={itemHeight}
rx={current.item.shape.includes('round') || current.item.shape.includes('cylinder') ? Math.min(itemWidth, itemHeight) / 2 : 5}
fill={color}
opacity="0.92"
stroke="#ffffff"
strokeWidth="1.4"
/>
<text x={position.x} y={position.y + itemHeight / 2 + 18} className="item-label">
{isSimple ? current.item.name : current.item.id}
</text>
{detecting ? (
<g className="bbox">
<rect
x={position.x - itemWidth / 2 - 10}
y={position.y - itemHeight / 2 - 10}
width={itemWidth + 20}
height={itemHeight + 20}
/>
{!isSimple ? (
<text x={position.x} y={position.y - itemHeight / 2 - 18}>
bbox {current.item.dimensionsMm.width} x {current.item.dimensionsMm.depth} mm
</text>
) : null}
</g>
) : null}
</g>
) : null}
{stopped ? (
<g className="fault-overlay">
<rect x="610" y="286" width="210" height="148" rx="10" />
<text x="715" y="350">{simulation.machineState}</text>
<text x="715" y="376">Conveyor stopped, reset required</text>
</g>
) : null}
{!isSimple ? (
<g className="scene-legend" transform="translate(78 664)">
<LegendItem color="#4ade80" label="B main sorter" />
<g transform="translate(140 0)"><LegendItem color="#f59e0b" label="C oversize" /></g>
<g transform="translate(270 0)"><LegendItem color="#c084fc" label="D shape / repack" /></g>
<g transform="translate(430 0)"><LegendItem color="#38bdf8" label="camera / laser / ultrasonic active" /></g>
<g transform="translate(700 0)"><LegendItem color="#fb3d4e" label="stop-gate / fault" /></g>
</g>
) : (
<g className="scene-legend" transform="translate(78 664)">
<LegendItem color="#4ade80" label="B" />
<g transform="translate(70 0)"><LegendItem color="#f59e0b" label="C" /></g>
<g transform="translate(140 0)"><LegendItem color="#c084fc" label="D" /></g>
<g transform="translate(210 0)"><LegendItem color="#38bdf8" label="active" /></g>
<g transform="translate(320 0)"><LegendItem color="#fb3d4e" label="fault" /></g>
</g>
)}
</svg>
</div>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,143 +0,0 @@
/**
* R3F performance sampler. Mount only when ?perf=1 or enabled prop.
* Exposes window.__PERF_SNAPSHOT__ (getter) and window.__PERF_RESET__.
*/
import { useFrame, useThree } from '@react-three/fiber';
import { useEffect, useRef } from 'react';
import type { WebGLRenderer } from 'three';
import {
computeFrameTimeStats,
emptyPerfSnapshot,
isHardwareAccelerated,
isPerfQueryEnabled,
roundPerfSnapshot,
type PerfSnapshot,
} from '../../domain/perfMetrics';
const MAX_SAMPLES = 600;
export interface PerfCollectorProps {
/** Force-enable even without ?perf=1 */
enabled?: boolean;
mode?: string;
shadows?: boolean;
antialias?: boolean;
}
function readRendererString(gl: WebGLRenderer): string {
try {
const ctx = gl.getContext() as WebGLRenderingContext;
const dbg = ctx.getExtension('WEBGL_debug_renderer_info');
if (dbg) {
return String(ctx.getParameter(dbg.UNMASKED_RENDERER_WEBGL) ?? 'unknown');
}
return String(ctx.getParameter(ctx.RENDERER) ?? 'unknown');
} catch {
return 'unknown';
}
}
function readHeapMb(): number {
const mem = (performance as Performance & { memory?: { usedJSHeapSize: number } }).memory;
if (!mem) return 0;
return mem.usedJSHeapSize / (1024 * 1024);
}
function PerfCollectorInner({
mode = 'demo',
shadows = false,
antialias = false,
}: Omit<PerfCollectorProps, 'enabled'>) {
const { gl } = useThree();
const frameTimes = useRef<number[]>([]);
const lastTs = useRef(0);
const meta = useRef({ mode, shadows, antialias, renderer: 'unknown' });
meta.current = { ...meta.current, mode, shadows, antialias };
const buildSnapshot = (): PerfSnapshot => {
const stats = computeFrameTimeStats(frameTimes.current);
const info = gl.info;
const renderer = meta.current.renderer || readRendererString(gl);
return roundPerfSnapshot(
emptyPerfSnapshot({
mode: meta.current.mode,
renderer,
...stats,
drawCalls: info.render.calls,
triangles: info.render.triangles,
geometries: info.memory.geometries,
textures: info.memory.textures,
programs: info.programs?.length ?? 0,
heapMb: readHeapMb(),
dpr: gl.getPixelRatio(),
shadows: meta.current.shadows,
antialias: meta.current.antialias,
hardwareAccelerated: isHardwareAccelerated(renderer),
}),
);
};
useEffect(() => {
meta.current.renderer = readRendererString(gl);
const reset = () => {
frameTimes.current = [];
lastTs.current = 0;
gl.info.reset();
};
Object.defineProperty(window, '__PERF_SNAPSHOT__', {
configurable: true,
enumerable: true,
get: () => buildSnapshot(),
});
window.__PERF_RESET__ = reset;
return () => {
reset();
try {
delete window.__PERF_SNAPSHOT__;
} catch {
/* ignore */
}
delete window.__PERF_RESET__;
};
}, [gl]);
useFrame((_state, delta) => {
// Prefer measured rAF delta; fall back to clock delta (seconds → ms)
const now = performance.now();
let dtMs: number;
if (lastTs.current > 0) {
dtMs = now - lastTs.current;
} else {
dtMs = delta * 1000;
}
lastTs.current = now;
// Ignore absurd spikes from tab backgrounding
if (dtMs <= 0 || dtMs > 500) return;
const buf = frameTimes.current;
buf.push(dtMs);
if (buf.length > MAX_SAMPLES) buf.shift();
});
return null;
}
/**
* Safe wrapper: returns null unless enabled or ?perf=1.
* Keeps demos free of sampling overhead by default.
*/
export default function PerfCollector({
enabled,
mode,
shadows,
antialias,
}: PerfCollectorProps) {
const active = enabled === true || (enabled !== false && isPerfQueryEnabled());
if (!active) return null;
return <PerfCollectorInner mode={mode} shadows={shadows} antialias={antialias} />;
}

View File

@@ -1,128 +0,0 @@
/**
* Compact FPS / GPU overlay. Visible only with ?perf=1 and outside presentation mode.
*/
import { useEffect, useState } from 'react';
import {
isPerfQueryEnabled,
type PerfSnapshot,
emptyPerfSnapshot,
} from '../../domain/perfMetrics';
import type { PhysicsPerfSnapshot } from '../../domain/physicsPerf';
const POLL_MS = 500;
export interface PerfOverlayProps {
/** Force show (still hidden in presentation mode via CSS / class check) */
enabled?: boolean;
}
export default function PerfOverlay({ enabled }: PerfOverlayProps) {
const [visible, setVisible] = useState(
() => enabled === true || (enabled !== false && isPerfQueryEnabled()),
);
const [snap, setSnap] = useState<PerfSnapshot>(() => emptyPerfSnapshot());
const [phys, setPhys] = useState<PhysicsPerfSnapshot | null>(null);
const [inPresentation, setInPresentation] = useState(false);
useEffect(() => {
if (enabled === false) {
setVisible(false);
return;
}
setVisible(enabled === true || isPerfQueryEnabled());
}, [enabled]);
useEffect(() => {
if (!visible) return;
const poll = () => {
const s = window.__PERF_SNAPSHOT__;
if (s) setSnap(s);
const p = window.__PHYSICS_PERF__;
if (p) setPhys(p);
setInPresentation(!!document.querySelector('.presentation-mode'));
};
poll();
const id = window.setInterval(poll, POLL_MS);
return () => window.clearInterval(id);
}, [visible]);
if (!visible || inPresentation) return null;
const soft = !snap.hardwareAccelerated;
const exportBenchmark = () => {
const payload = {
exportedAt: new Date().toISOString(),
url: window.location.href,
userAgent: navigator.userAgent,
viewport: { width: window.innerWidth, height: window.innerHeight },
snapshot: snap,
};
const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = `sorter-benchmark-${Date.now()}.json`;
a.click();
URL.revokeObjectURL(a.href);
};
return (
<div
className="perf-overlay"
data-testid="perf-overlay"
aria-label="Performance metrics"
>
<div className="perf-overlay-title">PERF</div>
<div>
FPS {snap.averageFps.toFixed(0)}
<span className="perf-muted"> (min {snap.minimumFps.toFixed(0)})</span>
</div>
<div>
p95 {snap.p95FrameTimeMs.toFixed(1)}ms
<span className="perf-muted"> / p99 {snap.p99FrameTimeMs.toFixed(1)}ms</span>
</div>
{phys && phys.count > 0 ? (
<div data-testid="physics-perf-line">
phys p95 {phys.p95Ms.toFixed(2)}ms
<span className="perf-muted">
{' '}
· avg {phys.avgMs.toFixed(2)} · n={phys.count}
</span>
</div>
) : null}
<div>
draws {snap.drawCalls}
<span className="perf-muted"> · tris {snap.triangles}</span>
</div>
<div>
geo {snap.geometries}
<span className="perf-muted">
{' '}
· tex {snap.textures} · prog {snap.programs}
</span>
</div>
<div>
heap {snap.heapMb.toFixed(1)}MB
<span className="perf-muted">
{' '}
· dpr {snap.dpr} · {snap.mode}
</span>
</div>
<div className={soft ? 'perf-soft' : 'perf-hw'} title={snap.renderer}>
{soft ? 'SW' : 'GPU'} {snap.renderer.slice(0, 42)}
{snap.renderer.length > 42 ? '…' : ''}
</div>
<button
type="button"
className="perf-export-btn"
data-testid="perf-export"
onClick={exportBenchmark}
>
Export benchmark
</button>
</div>
);
}

View File

@@ -1,229 +0,0 @@
import { memo, useEffect, useMemo } from 'react';
import { getPhysicalItemPose } from '../../domain/physicalItemMotion';
import { getModelAsset } from '../../data/modelAssets';
import { resolveItem } from '../../data/resolveItem';
import type { PlaylistCase } from '../../domain/demoPlaylist';
import { classifyItem } from '../../domain/classifier';
import { getRenderedItemDimensions } from '../../domain/physicalLayout';
import * as THREE from 'three';
import RealItemModel, { isProductAssetReady } from './RealItemModel';
import { ItemVerificationOverlay } from './RealModelVerification';
const COLORS = {
B: '#16a34a',
C: '#ea580c',
D: '#7c3aed',
sensorAccent: '#3b82f6',
};
/** Base real-model materials per SKU (Stage 1 §20 — basic, form-revealing). */
const ITEM_MATERIALS: Record<string, { color: string; roughness: number; metalness?: number }> = {
'SKU-001': { color: '#b68b58', roughness: 0.82 }, // cardboard
'SKU-002': { color: '#e8eef6', roughness: 0.5 }, // lunchbox plastic
'SKU-003': { color: '#93c5fd', roughness: 0.38 }, // detergent jug plastic
'SKU-004': { color: '#c49a6c', roughness: 0.82 }, // cardboard
'SKU-005': { color: '#a78bfa', roughness: 0.92 }, // soft pouf fabric
'SKU-006': { color: '#f8fafc', roughness: 0.42 }, // plate ceramic
'SKU-007': { color: '#7dd3fc', roughness: 0.28 }, // bottle plastic
'SKU-008': { color: '#cbd5e1', roughness: 0.35, metalness: 0.15 },
'SKU-009': { color: '#475569', roughness: 0.5 }, // pen body
};
interface RenderProps {
color: string;
accentColor: string;
emissiveIntensity: number;
roughness: number;
metalness: number;
}
function FallbackPrimitive({ type, color, accentColor, emissiveIntensity, roughness, metalness, w, h, d, castShadow }: RenderProps & {
type: 'box' | 'cylinder' | 'sphere';
w: number; h: number; d: number;
castShadow?: boolean;
}) {
const geometry = useMemo<THREE.BufferGeometry>(() => {
if (type === 'cylinder' || type === 'sphere') {
const r = Math.max(w, d) / 2;
return new THREE.CylinderGeometry(r, r, h, 16);
}
return new THREE.BoxGeometry(w, h, d);
}, [type, w, h, d]);
useEffect(() => () => geometry.dispose(), [geometry]);
return (
<mesh geometry={geometry} castShadow={castShadow}>
<meshStandardMaterial color={color} emissive={accentColor} emissiveIntensity={emissiveIntensity} roughness={roughness} metalness={metalness} />
</mesh>
);
}
/** Inner visual content of an item (shared by kinematic and physics drivers). */
export function ItemVisualContent({
caseData,
phase,
surface,
isSettled,
castShadow = false,
verifySku = null,
onVisualReady,
}: {
caseData: PlaylistCase;
phase: string;
surface: string;
isSettled: boolean;
castShadow?: boolean;
verifySku?: string | null;
/** Fires once the visible mesh (real or procedural) is ready to show. */
onVisualReady?: () => void;
}) {
const itemData = useMemo(() => resolveItem(caseData.itemId), [caseData.itemId]);
const classification = useMemo(() => classifyItem(itemData), [itemData]);
const itemId = itemData.id.replace('-LC', '');
const asset = getModelAsset(itemId);
const dims = getRenderedItemDimensions(itemData.dimensionsMm);
const isRouting = phase === 'routing';
const onTransport = surface === 'main_belt'
|| surface === 'inspection_station'
|| surface === 'routing_junction'
|| surface === 'b_transfer';
const routeAccent = COLORS[classification.category] ?? COLORS.sensorAccent;
const material = ITEM_MATERIALS[itemId] ?? { color: '#d8c3a5', roughness: 0.75 };
const bodyColor = phase === 'fault' ? '#ef4444' : material.color;
const accentColor = isSettled ? '#94a3b8' : routeAccent;
const emissiveIntensity = phase === 'fault' ? 0.25 : isRouting ? 0.12 : isSettled ? 0.01 : 0.03;
const metalness = material.metalness ?? 0.05;
// Real official model is the default when the manifest provides one;
// procedural fallback only for missing assets or load failure (Stage 1 §15.1).
const useReal = Boolean(asset?.defaultRealAsset && asset?.runtimePath);
const fallbackType = asset?.fallbackPrimitive ?? 'box';
// Pose position is the EXPECTED bbox center (surfaceY + h/2). Real models use
// a bottom-center pivot, so the mesh is offset down by half the model height.
// Contact epsilon vs the surface is therefore exactly 0 mm by construction.
const modelHeightM = asset?.worldExpectedMm
? asset.worldExpectedMm.y / 1000
: dims.height;
const pivotOffsetY = -modelHeightM / 2;
// Procedural / already-cached assets are ready immediately.
useEffect(() => {
if (!useReal || !asset?.runtimePath || isProductAssetReady(asset.runtimePath)) {
onVisualReady?.();
}
}, [useReal, asset?.runtimePath, caseData.id, onVisualReady]);
const fallback = (
<FallbackPrimitive
type={fallbackType}
color={bodyColor}
accentColor={accentColor}
emissiveIntensity={emissiveIntensity}
roughness={material.roughness}
metalness={metalness}
w={dims.width}
h={dims.height}
d={dims.depth}
castShadow={castShadow}
/>
);
const verifying = verifySku != null && verifySku === itemId && asset != null;
return (
<>
{useReal && asset ? (
<group position={[0, pivotOffsetY, 0]}>
<RealItemModel
asset={asset}
material={{
color: bodyColor,
emissive: accentColor,
emissiveIntensity,
roughness: material.roughness,
metalness,
}}
castShadow={castShadow}
fallback={fallback}
onReady={onVisualReady}
/>
</group>
) : (
fallback
)}
{verifying && asset && (
<ItemVerificationOverlay
asset={asset}
pivotOffsetY={pivotOffsetY}
cardY={modelHeightM + 0.3}
fallbackSizeM={{ x: dims.width, y: dims.height, z: dims.depth }}
/>
)}
{isSettled && (
<mesh position={[0, -dims.height / 2 + 0.003, 0]} rotation={[-Math.PI / 2, 0, 0]}>
<ringGeometry args={[Math.max(dims.width, dims.depth) * 0.35, Math.max(dims.width, dims.depth) * 0.42, 20]} />
<meshBasicMaterial color={routeAccent} transparent opacity={0.5} />
</mesh>
)}
{onTransport && (
<mesh position={[0, -dims.height / 2 + 0.001, 0]} rotation={[-Math.PI / 2, 0, 0]}>
<circleGeometry args={[Math.max(dims.width, dims.depth) / 2 + 0.01, 16]} />
<meshStandardMaterial color="#475569" transparent opacity={0.15} />
</mesh>
)}
</>
);
}
export const PhysicalPlaybackItem = memo(function PhysicalPlaybackItem({
caseData,
elapsedMs,
slotIndex = 0,
jitter,
castShadow = false,
verifySku = null,
}: {
caseData: PlaylistCase;
elapsedMs: number;
slotIndex?: number;
jitter?: { x: number; z: number; yaw: number };
castShadow?: boolean;
/** Stage 1 verification: SKU to overlay (null = off, 'follow' handled by caller passing current SKU). */
verifySku?: string | null;
}) {
const itemData = useMemo(() => resolveItem(caseData.itemId), [caseData.itemId]);
const classification = useMemo(() => classifyItem(itemData), [itemData]);
const pose = getPhysicalItemPose({
caseId: caseData.id,
slotIndex,
dimensionsMm: itemData.dimensionsMm,
targetCategory: classification.category,
elapsedMs,
faultType: caseData.faultType,
jitter,
});
const { position, rotation, phase, surface, isSettled } = pose;
if (elapsedMs < 0) return null;
return (
<group position={position} rotation={rotation}>
<ItemVisualContent
caseData={caseData}
phase={phase}
surface={surface}
isSettled={isSettled}
castShadow={castShadow}
verifySku={verifySku}
/>
</group>
);
});

View File

@@ -1,296 +0,0 @@
/**
* Stage 2 — item with hybrid kinematic/dynamic authority (Rapier).
*
* Authority flow (see docs/stage2_real_sorter/physics-architecture.md):
* 1. kinematicPosition — follows getPhysicalItemPose exactly (domain truth);
* 2. at getDropHandoffTimeMs → dynamic with deterministic initial velocity
* (B: belt edge carry-over; C/D: pusher impulse, scaled per SKU profile);
* 3. gravity/collision/friction/restitution/angular velocity govern the drop;
* 4. on sleep (or controlled 4.5 s timeout) the final position is verified
* against the DOMAIN-decided receiver volume and the body is frozen
* (kinematic) — no drift, clean replay, no teleportation at any point.
*/
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import * as THREE from 'three';
import { useFrame } from '@react-three/fiber';
import {
RigidBody,
CuboidCollider,
CapsuleCollider,
CylinderCollider,
type RapierRigidBody,
} from '@react-three/rapier';
import { RigidBodyType } from '@dimforge/rapier3d-compat';
import { getPhysicalItemPose, getDropHandoffTimeMs } from '../../domain/physicalItemMotion';
import { getVisualPhysicsProfile } from '../../domain/visualPhysicsProfiles';
import { resolveItem } from '../../data/resolveItem';
import { classifyItem } from '../../domain/classifier';
import { receiverContains } from '../../domain/receiverVolumes';
import type { PlaylistCase } from '../../domain/demoPlaylist';
import { ItemVisualContent } from './PhysicalPlaybackItem';
import { recordDropResult, physicsSimClock } from './SorterPhysics';
import { getModelAsset } from '../../data/modelAssets';
import { isProductAssetReady } from './RealItemModel';
type Authority = 'kinematic' | 'dynamic' | 'frozen';
/** Controlled settle budget after handoff — in PHYSICS-simulated seconds,
* not domain ms: under render lag domain time races ahead of the stepper,
* and a domain-ms budget would freeze items mid-flight (§14.2). */
const SETTLE_BUDGET_SEC = 4.5;
function colliderDensity(profile: ReturnType<typeof getVisualPhysicsProfile>): number {
if (profile.collider === 'cuboid' && profile.cuboidHalfExtents) {
const [hx, hy, hz] = profile.cuboidHalfExtents;
return profile.approximateMassKg / (8 * hx * hy * hz);
}
const [r, hh] = profile.capsule ?? [0.05, 0.1];
const volume = profile.collider === 'capsule'
? Math.PI * r * r * (2 * hh + (4 / 3) * r)
: Math.PI * r * r * 2 * hh;
return profile.approximateMassKg / volume;
}
export const PhysicalPlaybackItemPhysics = memo(function PhysicalPlaybackItemPhysics({
caseData,
elapsedMs,
slotIndex = 0,
jitter,
castShadow = false,
verifySku = null,
}: {
caseData: PlaylistCase;
elapsedMs: number;
slotIndex?: number;
jitter?: { x: number; z: number; yaw: number };
castShadow?: boolean;
verifySku?: string | null;
}) {
const itemData = useMemo(() => resolveItem(caseData.itemId), [caseData.itemId]);
const classification = useMemo(() => classifyItem(itemData), [itemData]);
const category = classification.category as 'B' | 'C' | 'D';
const itemId = itemData.id.replace('-LC', '');
const profile = getVisualPhysicsProfile(itemId);
const handoffMs = getDropHandoffTimeMs(classification.category, caseData.faultType);
const bodyRef = useRef<RapierRigidBody>(null);
const traceEnabled = useRef(
typeof window !== 'undefined'
&& new URLSearchParams(window.location.search).get('trace') === '1',
);
const authority = useRef<Authority>('kinematic');
const frozenPose = useRef<{ p: [number, number, number]; q: THREE.Quaternion } | null>(null);
const handedOffAtSimSec = useRef<number | null>(null);
const verified = useRef(false);
const asset = getModelAsset(itemId);
const needsRealAsset = Boolean(asset?.defaultRealAsset && asset?.runtimePath);
const [spawned, setSpawned] = useState(
() => !needsRealAsset || isProductAssetReady(asset?.runtimePath),
);
const onVisualReady = useCallback(() => {
setSpawned(true);
}, []);
const pose = getPhysicalItemPose({
caseId: caseData.id,
slotIndex,
dimensionsMm: itemData.dimensionsMm,
targetCategory: classification.category,
elapsedMs,
faultType: caseData.faultType,
jitter,
});
const handoffPose = useMemo(() => {
if (handoffMs == null) return null;
return getPhysicalItemPose({
caseId: caseData.id,
slotIndex,
dimensionsMm: itemData.dimensionsMm,
targetCategory: classification.category,
elapsedMs: handoffMs,
faultType: caseData.faultType,
jitter,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [handoffMs, caseData.id]);
// Reset authority whenever a new case mounts this body. The Rapier body is
// reused across cases, so a case that ended while still DYNAMIC (settle
// budget cut short under render lag) must be forced back to kinematic —
// otherwise setNextKinematicTranslation is a no-op and the next case's item
// is stuck invisibly mid-scene.
useEffect(() => {
authority.current = 'kinematic';
frozenPose.current = null;
handedOffAtSimSec.current = null;
verified.current = false;
const ready = !needsRealAsset || isProductAssetReady(asset?.runtimePath);
setSpawned(ready);
const body = bodyRef.current;
if (body) {
body.setBodyType(RigidBodyType.KinematicPositionBased, false);
body.setLinvel({ x: 0, y: 0, z: 0 }, true);
body.setAngvel({ x: 0, y: 0, z: 0 }, true);
const p = pose.position;
body.setTranslation({ x: p[0], y: p[1], z: p[2] }, true);
const e = new THREE.Euler(pose.rotation[0], pose.rotation[1], pose.rotation[2]);
const q = new THREE.Quaternion().setFromEuler(e);
body.setRotation({ x: q.x, y: q.y, z: q.z, w: q.w }, true);
}
// eslint-disable-next-line react-hooks/exhaustive-deps -- reset on case id only
}, [caseData.id]);
useFrame(() => {
const body = bodyRef.current;
if (!body) return;
// PREPARING: hold at spawn pose, zero velocity, keep invisible until visual ready.
if (!spawned) {
const p = pose.position;
body.setNextKinematicTranslation({ x: p[0], y: p[1], z: p[2] });
const e = new THREE.Euler(pose.rotation[0], pose.rotation[1], pose.rotation[2]);
const q = new THREE.Quaternion().setFromEuler(e);
body.setNextKinematicRotation({ x: q.x, y: q.y, z: q.z, w: q.w });
body.setLinvel({ x: 0, y: 0, z: 0 }, true);
body.setAngvel({ x: 0, y: 0, z: 0 }, true);
return;
}
if (authority.current === 'kinematic') {
// Physics handoff at pusher contact / belt edge — never for fault cases.
// MUST be checked BEFORE the kinematic drive: under render lag a single
// frame can jump several seconds past handoffMs, and pose(elapsedMs) is
// then already deep inside the receiver. Applying setNextKinematic*
// from that pose in the same frame as the dynamic switch teleports the
// body (forbidden) — the next-step kinematic target still applies.
if (handoffMs != null && handoffPose && elapsedMs >= handoffMs) {
const hp = handoffPose.position;
body.setTranslation({ x: hp[0], y: hp[1], z: hp[2] }, true);
const he = new THREE.Euler(handoffPose.rotation[0], handoffPose.rotation[1], handoffPose.rotation[2]);
const hq = new THREE.Quaternion().setFromEuler(he);
body.setRotation({ x: hq.x, y: hq.y, z: hq.z, w: hq.w }, true);
body.setBodyType(RigidBodyType.Dynamic, true);
// Deterministic initial velocity: belt carry-over only — for C/D the
// Z motion comes from the kinematic paddle CONTACT (Stage 2B §13).
body.setLinvel({ x: 1.0, y: 0, z: 0 }, true);
if (profile.canRoll && category === 'B') {
body.setAngvel({ x: 2.0, y: 0.4, z: 0 }, true);
} else {
body.setAngvel({ x: 0, y: 0, z: 0 }, true);
}
authority.current = 'dynamic';
handedOffAtSimSec.current = physicsSimClock.simSec;
return;
}
// Kinematic drive: domain pose is truth (belt travel, inspection dwell).
const p = pose.position;
const e = new THREE.Euler(pose.rotation[0], pose.rotation[1], pose.rotation[2]);
const q = new THREE.Quaternion().setFromEuler(e);
body.setNextKinematicTranslation({ x: p[0], y: p[1], z: p[2] });
body.setNextKinematicRotation({ x: q.x, y: q.y, z: q.z, w: q.w });
return;
}
if (authority.current === 'dynamic') {
const slept = body.isSleeping();
const lv = body.linvel();
const av = body.angvel();
const slow = Math.hypot(lv.x, lv.y, lv.z) < 0.2 && Math.hypot(av.x, av.y, av.z) < 1.0;
if (traceEnabled.current) {
const t = body.translation();
const w = window as unknown as { __ITEM_TRACE?: unknown[] };
w.__ITEM_TRACE = w.__ITEM_TRACE ?? [];
const arr = w.__ITEM_TRACE as { e: number; x: number; y: number; z: number; lv: number; slept: boolean }[];
if (arr.length === 0 || arr[arr.length - 1].e < elapsedMs - 200) {
arr.push({ e: Math.round(elapsedMs), x: +t.x.toFixed(3), y: +t.y.toFixed(3), z: +t.z.toFixed(3), lv: +Math.hypot(lv.x, lv.y, lv.z).toFixed(2), slept });
if (arr.length > 120) arr.shift();
}
}
const timedOut = handedOffAtSimSec.current != null
&& physicsSimClock.simSec - handedOffAtSimSec.current > SETTLE_BUDGET_SEC;
// §14.2: freeze only after actual rest (sleep) or a timeout WITH low
// velocities — never freeze a body that is still moving/flying.
if ((slept || (timedOut && slow)) && !verified.current) {
verified.current = true;
const t = body.translation();
const p: [number, number, number] = [t.x, t.y, t.z];
recordDropResult({
caseId: caseData.id,
itemId,
expectedZone: category,
finalPosition: p,
insideExpectedReceiver: receiverContains(category, p),
settledByTimeout: !slept,
timestampMs: Date.now(),
});
const r = body.rotation();
frozenPose.current = { p, q: new THREE.Quaternion(r.x, r.y, r.z, r.w) };
body.setBodyType(RigidBodyType.KinematicPositionBased, false);
body.setLinvel({ x: 0, y: 0, z: 0 }, false);
body.setAngvel({ x: 0, y: 0, z: 0 }, false);
authority.current = 'frozen';
}
return;
}
// frozen: hold the verified rest pose (no drift across replays).
if (frozenPose.current) {
const { p, q } = frozenPose.current;
body.setNextKinematicTranslation({ x: p[0], y: p[1], z: p[2] });
body.setNextKinematicRotation({ x: q.x, y: q.y, z: q.z, w: q.w });
}
});
if (elapsedMs < 0) return null;
const density = colliderDensity(profile);
// CCD for small/fast items (pen) and thin items (plate) — mirrors the sim.
const ccd = profile.approximateMassKg < 0.05 || itemData.dimensionsMm.height < 50;
return (
<RigidBody
ref={bodyRef}
type="kinematicPosition"
colliders={false}
friction={profile.friction}
restitution={profile.restitution}
linearDamping={profile.linearDamping}
angularDamping={profile.angularDamping}
ccd={ccd}
enabledRotations={[true, true, true]}
position={pose.position}
>
{/* Colliders only after visual ready — avoids stale/orphan contact. */}
{spawned && profile.collider === 'cuboid' && profile.cuboidHalfExtents && (
<CuboidCollider args={profile.cuboidHalfExtents} density={density} />
)}
{spawned && profile.collider === 'capsule' && profile.capsule && (
<CapsuleCollider
args={[profile.capsule[1], profile.capsule[0]]}
density={density}
rotation={profile.colliderAxis === 'x' ? [0, 0, Math.PI / 2] : undefined}
/>
)}
{spawned && profile.collider === 'cylinder' && profile.capsule && (
<CylinderCollider
args={[profile.capsule[1], profile.capsule[0]]}
density={density}
rotation={profile.colliderAxis === 'x' ? [0, 0, Math.PI / 2] : undefined}
/>
)}
<group visible={spawned}>
<ItemVisualContent
caseData={caseData}
phase={pose.phase}
surface={pose.surface}
isSettled={authority.current === 'frozen' ? true : pose.isSettled}
castShadow={castShadow && spawned}
verifySku={verifySku}
onVisualReady={onVisualReady}
/>
</group>
</RigidBody>
);
});

View File

@@ -1,22 +0,0 @@
/**
* PostProcessingSpike — Stage 0 cost-measurement only.
*
* Loaded lazily (separate chunk) and mounted ONLY in stage0 prototype mode
* with post=1. Default route never downloads @react-three/postprocessing.
* Deliberately cheap set: Bloom + Vignette + Noise + SMAA. No DoF, no SSR,
* no motion blur, no TAA, no volumetrics.
*/
import { EffectComposer, Bloom, Vignette, Noise, SMAA } from '@react-three/postprocessing';
import { BlendFunction } from 'postprocessing';
export default function PostProcessingSpike() {
return (
<EffectComposer multisampling={0}>
<SMAA />
<Bloom intensity={0.35} luminanceThreshold={0.75} luminanceSmoothing={0.2} mipmapBlur />
<Noise premultiply blendFunction={BlendFunction.SCREEN} opacity={0.25} />
<Vignette offset={0.25} darkness={0.55} eskil={false} />
</EffectComposer>
);
}

View File

@@ -1,193 +0,0 @@
/**
* RealItemModel — единый loader для официальных real-model ассетов.
*
* Контракт (одинаковый для STL сейчас и GLB позже):
* manifest (modelAssets.ts) → нормализованная геометрия → mesh.
*
* Нормализация запекается в клон геометрии один раз при загрузке:
* 1. rotation из manifest (в мм-пространстве источника);
* 2. uniform scale 0.001 (mm → meters);
* 3. pivot → bottom-center (центр footprint по X/Z, низ по Y);
* 4. computeVertexNormals.
*
* Shared loader cache никогда не мутируется (clone перед transforms),
* dispose вызывается только для локального клона.
*/
import { Component, Suspense, useEffect, useMemo, type ReactNode } from 'react';
import * as THREE from 'three';
import { useLoader } from '@react-three/fiber';
import { STLLoader } from 'three/examples/jsm/loaders/STLLoader.js';
import type { ModelAsset } from '../../data/modelAssets';
export interface RealItemMaterial {
color: string;
emissive?: string;
emissiveIntensity?: number;
roughness?: number;
metalness?: number;
}
interface InnerProps {
asset: ModelAsset;
material: RealItemMaterial;
castShadow?: boolean;
receiveShadow?: boolean;
onReady?: () => void;
}
const readyUrls = new Set<string>();
const inflight = new Map<string, Promise<void>>();
export function isProductAssetReady(runtimePath: string | null | undefined): boolean {
if (!runtimePath) return true;
return readyUrls.has(runtimePath);
}
export function markProductAssetReady(runtimePath: string): void {
readyUrls.add(runtimePath);
}
/**
* Preload a runtime STL into the shared loader cache and resolve when ready.
* Deduped by URL — concurrent callers share one Promise.
*/
export function preloadRealItemModelAsync(runtimePath: string): Promise<void> {
if (readyUrls.has(runtimePath)) return Promise.resolve();
const existing = inflight.get(runtimePath);
if (existing) return existing;
// Warm R3F useLoader cache (deduped).
useLoader.preload(STLLoader, runtimePath);
const promise = new Promise<void>((resolve, reject) => {
const loader = new STLLoader();
loader.load(
runtimePath,
() => {
readyUrls.add(runtimePath);
inflight.delete(runtimePath);
resolve();
},
undefined,
(err) => {
inflight.delete(runtimePath);
reject(err);
},
);
});
inflight.set(runtimePath, promise);
return promise;
}
/** Preload a runtime asset into the shared loader cache (deduped by URL). */
export function preloadRealItemModel(runtimePath: string): void {
void preloadRealItemModelAsync(runtimePath);
}
/**
* Normalize a freshly cloned geometry per manifest rules.
* Returns the clone with transforms BAKED IN (pivot = bottom-center, meters).
*/
export function normalizeGeometryClone(source: THREE.BufferGeometry, asset: ModelAsset): THREE.BufferGeometry {
const g = source.clone();
const [rx, ry, rz] = asset.rotation;
if (rx) g.rotateX(rx);
if (ry) g.rotateY(ry);
if (rz) g.rotateZ(rz);
g.scale(0.001, 0.001, 0.001); // mm → m, uniform (scaleMode: 'uniform-mm-to-m')
g.computeBoundingBox();
const bb = g.boundingBox!;
const cx = (bb.min.x + bb.max.x) / 2;
const cz = (bb.min.z + bb.max.z) / 2;
g.translate(-cx, -bb.min.y, -cz); // pivotMode: 'bottom-center'
g.computeVertexNormals();
g.computeBoundingBox();
return g;
}
function RealItemModelInner({ asset, material, castShadow, receiveShadow, onReady }: InnerProps) {
const shared = useLoader(STLLoader, asset.runtimePath!) as THREE.BufferGeometry;
const geometry = useMemo(() => normalizeGeometryClone(shared, asset), [shared, asset]);
useEffect(() => () => geometry.dispose(), [geometry]);
useEffect(() => {
if (asset.runtimePath) markProductAssetReady(asset.runtimePath);
onReady?.();
}, [asset.runtimePath, geometry, onReady]);
return (
<mesh geometry={geometry} castShadow={castShadow} receiveShadow={receiveShadow}>
<meshStandardMaterial
color={material.color}
emissive={material.emissive ?? material.color}
emissiveIntensity={material.emissiveIntensity ?? 0.05}
roughness={material.roughness ?? 0.6}
metalness={material.metalness ?? 0.05}
/>
</mesh>
);
}
interface BoundaryProps {
fallback: ReactNode;
children: ReactNode;
onError?: (error: Error) => void;
}
interface BoundaryState {
failed: boolean;
}
/** Per-item error boundary: load failure → procedural fallback, no scene crash. */
class ItemModelErrorBoundary extends Component<BoundaryProps, BoundaryState> {
state: BoundaryState = { failed: false };
static getDerivedStateFromError(): BoundaryState {
return { failed: true };
}
componentDidCatch(error: Error): void {
// eslint-disable-next-line no-console
console.warn('[RealItemModel] asset load failed, procedural fallback engaged:', error.message);
this.props.onError?.(error);
}
render() {
return this.state.failed ? this.props.fallback : this.props.children;
}
}
export interface RealItemModelProps extends InnerProps {
/**
* Procedural fallback for load failure only.
* Suspense placeholder stays invisible so spawn is atomic (no flash-then-swap).
*/
fallback: ReactNode;
onError?: (error: Error) => void;
/** When true, Suspense shows fallback (legacy). Default: invisible placeholder. */
showSuspenseFallback?: boolean;
}
export default function RealItemModel({
fallback,
showSuspenseFallback = false,
onReady,
onError,
...inner
}: RealItemModelProps) {
if (!inner.asset.runtimePath) {
return <>{fallback}</>;
}
return (
<ItemModelErrorBoundary
fallback={fallback}
onError={(err) => {
onReady?.();
onError?.(err);
}}
>
<Suspense fallback={showSuspenseFallback ? fallback : null}>
<RealItemModelInner {...inner} onReady={onReady} />
</Suspense>
</ItemModelErrorBoundary>
);
}

View File

@@ -1,168 +0,0 @@
/**
* RealModelVerification — Stage 1 debug overlay (?stage1=1&verify=real-models).
*
* Read-only instrumentation: bounding box, axes, pivot marker, bottom contact
* plane и информационная панель (источник, формат, размеры, статус валидации).
* Рендерится только в verification mode; business state не изменяется.
*
* Использование: <ItemVerificationOverlay> внутри pose-группы товара
* (PhysicalPlaybackItem) — следует за товаром по маршруту B/C/D.
*/
import { useEffect, useMemo } from 'react';
import * as THREE from 'three';
import { Html } from '@react-three/drei';
import { useLoader } from '@react-three/fiber';
import { STLLoader } from 'three/examples/jsm/loaders/STLLoader.js';
import type { ModelAsset } from '../../data/modelAssets';
import { normalizeGeometryClone } from './RealItemModel';
export interface SizeM {
x: number;
y: number;
z: number;
}
const AXIS_COLORS = { x: '#ef4444', y: '#22c55e', z: '#3b82f6' } as const;
/** Wire bbox + axis tripod + pivot marker + bottom contact plane (bottom-center space). */
function VerificationGizmos({ sizeM }: { sizeM: SizeM }) {
const boxEdges = useMemo(() => {
const box = new THREE.BoxGeometry(sizeM.x, sizeM.y, sizeM.z);
const edges = new THREE.EdgesGeometry(box);
box.dispose();
return edges;
}, [sizeM.x, sizeM.y, sizeM.z]);
const axes = useMemo(() => ({
x: new THREE.BufferGeometry().setFromPoints([new THREE.Vector3(0, 0, 0), new THREE.Vector3(0.15, 0, 0)]),
y: new THREE.BufferGeometry().setFromPoints([new THREE.Vector3(0, 0, 0), new THREE.Vector3(0, 0.15, 0)]),
z: new THREE.BufferGeometry().setFromPoints([new THREE.Vector3(0, 0, 0), new THREE.Vector3(0, 0, 0.15)]),
}), []);
useEffect(() => () => {
boxEdges.dispose();
axes.x.dispose();
axes.y.dispose();
axes.z.dispose();
}, [boxEdges, axes]);
return (
<group>
{/* bounding box of the normalized model (bottom-center pivot) */}
<lineSegments geometry={boxEdges} position={[0, sizeM.y / 2, 0]}>
<lineBasicMaterial color="#facc15" />
</lineSegments>
{/* axis tripod at pivot (footprint center, bottom point) */}
<lineSegments geometry={axes.x}><lineBasicMaterial color={AXIS_COLORS.x} /></lineSegments>
<lineSegments geometry={axes.y}><lineBasicMaterial color={AXIS_COLORS.y} /></lineSegments>
<lineSegments geometry={axes.z}><lineBasicMaterial color={AXIS_COLORS.z} /></lineSegments>
{/* pivot marker */}
<mesh position={[0, 0.004, 0]}>
<sphereGeometry args={[0.008, 12, 8]} />
<meshBasicMaterial color="#facc15" depthTest={false} />
</mesh>
{/* bottom contact plane (item footprint on the surface) */}
<mesh position={[0, 0.0005, 0]} rotation={[-Math.PI / 2, 0, 0]}>
<planeGeometry args={[sizeM.x, sizeM.z]} />
<meshBasicMaterial color="#22c55e" transparent opacity={0.25} depthWrite={false} />
</mesh>
</group>
);
}
function InfoCard({ asset, measuredM, y }: {
asset: ModelAsset;
measuredM: SizeM | null;
y: number;
}) {
const mm = (m: number) => (m * 1000).toFixed(1);
const expected = asset.worldExpectedMm;
const rows: Array<[string, string]> = [
['Model', asset.displayName],
['Badge', asset.defaultRealAsset ? 'REAL (official)' : 'FALLBACK · NO_EXACT_OFFICIAL_MODEL'],
['Source', asset.sourceFile ?? 'n/a'],
['SHA-256', asset.sourceSha256 ? `${asset.sourceSha256.slice(0, 12)}` : 'n/a'],
['Format', asset.runtimeFormat ?? 'procedural'],
['File size', asset.fileSizeBytes != null ? `${(asset.fileSizeBytes / 1024).toFixed(0)} KB` : 'n/a'],
['Triangles', asset.triangleCount != null ? String(asset.triangleCount) : 'n/a'],
['Conversion', asset.conversionStatus],
['Pivot', asset.pivotMode],
];
if (expected) rows.push(['Expected x/y/z mm', `${expected.x} / ${expected.y} / ${expected.z}`]);
if (measuredM) rows.push(['Measured x/y/z mm', `${mm(measuredM.x)} / ${mm(measuredM.y)} / ${mm(measuredM.z)}`]);
rows.push(['Validation', asset.validationStatus]);
return (
<Html position={[0, y, 0]} center style={{ pointerEvents: 'none' }}>
<div style={{
fontFamily: 'ui-monospace, monospace',
fontSize: '10px',
lineHeight: 1.45,
color: '#e2e8f0',
background: 'rgba(2, 6, 23, 0.88)',
border: '1px solid #334155',
borderRadius: '6px',
padding: '8px 10px',
whiteSpace: 'nowrap',
transform: 'translateY(-100%)',
}}>
<div style={{ fontWeight: 700, color: '#facc15', marginBottom: 4 }}>
STAGE1 VERIFY · {asset.itemId}
</div>
{rows.map(([k, v]) => (
<div key={k}>
<span style={{ color: '#64748b' }}>{k}: </span>
<span>{v}</span>
</div>
))}
</div>
</Html>
);
}
/** Real-asset branch: measures the actual runtime file via the shared loader cache. */
function RealAssetVerification({ asset, cardY }: { asset: ModelAsset; cardY: number }) {
const shared = useLoader(STLLoader, asset.runtimePath!) as THREE.BufferGeometry;
const measuredM = useMemo<SizeM>(() => {
const g = normalizeGeometryClone(shared, asset);
g.computeBoundingBox();
const bb = g.boundingBox!;
const size = { x: bb.max.x - bb.min.x, y: bb.max.y - bb.min.y, z: bb.max.z - bb.min.z };
g.dispose();
return size;
}, [shared, asset]);
return (
<group>
<VerificationGizmos sizeM={measuredM} />
<InfoCard asset={asset} measuredM={measuredM} y={cardY} />
</group>
);
}
/**
* Overlay for one item in its pose group. `pivotOffsetY` is the same local Y
* offset used by the rendered mesh (bottom-center compensation), `cardY` the
* height for the info card (item top + margin, in the same local space).
*/
export function ItemVerificationOverlay({ asset, pivotOffsetY, cardY, fallbackSizeM }: {
asset: ModelAsset;
pivotOffsetY: number;
cardY: number;
fallbackSizeM: SizeM;
}) {
return (
<group position={[0, pivotOffsetY, 0]}>
{asset.runtimePath ? (
<RealAssetVerification asset={asset} cardY={cardY} />
) : (
<group>
<VerificationGizmos sizeM={fallbackSizeM} />
<InfoCard asset={asset} measuredM={null} y={cardY} />
</group>
)}
</group>
);
}
export default function RealModelVerification() {
return null;
}

View File

@@ -1,154 +0,0 @@
/**
* Stage 2B §9 — Intel RealSense Depth Camera D435i (SPEC_DERIVED).
*
* Built from the official Intel datasheet dimensions (90 × 25 × 25 mm,
* 50 mm stereo baseline, depth FOV 87°×58°) and the reference photo:
* horizontal anodized-aluminium bar, full-width front glass, left/right IR
* imagers, center RGB module, IR texture projector, rear USB-C, tripod boss.
* SPEC_DERIVED — not an official Intel CAD file; provenance documented in
* docs/stage2_real_sorter/camera-realsense-spec.md.
*
* Mount: overhead bar across the belt (Z), front glass facing DOWN (-Y),
* optical center 1.35 m (0.65 m above belt top 0.7 m) — clears the 500 mm
* oversized item by 112 mm. Laser triangulation module separately at 1.15 m
* (project doc height), so camera and laser heights are NOT conflated.
*/
import { memo } from 'react';
import { SCAN_START_X, SCAN_END_X } from '../../domain/measurementZone';
import { ZONES, CONVEYOR_WIDTH_M, BELT_TOP_Y } from '../../domain/physicalLayout';
/** Official datasheet dimensions (m). */
export const D435I = {
width: 0.09, // 90 mm along the bar (Z when mounted across the belt)
height: 0.025, // 25 mm
depth: 0.025, // 25 mm
baseline: 0.05,
fovH: 87, // deg, along the baseline
fovV: 58, // deg
opticalCenterY: 1.35,
} as const;
const HOUSING = '#222528';
const HOUSING_EDGE = '#31363b';
const GLASS = '#0c1116';
const LENS_RIM = '#3c4249';
const LENS_INNER = '#05070a';
/** One sensor window on the glass face (rim + recessed lens), facing DOWN (-Y). */
function SensorWindow({ z, radius }: { z: number; radius: number }) {
const y = -D435I.height / 2 - 0.0004;
return (
<group position={[0, 0, z]}>
<mesh position={[0, y, 0]} rotation={[-Math.PI / 2, 0, 0]}>
<torusGeometry args={[radius, 0.0016, 10, 24]} />
<meshStandardMaterial color={LENS_RIM} metalness={0.8} roughness={0.35} />
</mesh>
<mesh position={[0, y + 0.0008, 0]}>
<cylinderGeometry args={[radius * 0.72, radius * 0.72, 0.0016, 20]} />
<meshStandardMaterial color={LENS_INNER} metalness={0.4} roughness={0.15} />
</mesh>
</group>
);
}
export const RealSenseD435i = memo(function RealSenseD435i({
castShadow = false,
}: {
castShadow?: boolean;
}) {
const w = D435I.width;
return (
// Bar runs across the belt (Z); front glass face looks DOWN (-Y) at the belt.
<group>
{/* Main housing bar with chamfered edge rails */}
<mesh castShadow={castShadow}>
<boxGeometry args={[D435I.depth - 0.004, D435I.height - 0.004, w]} />
<meshStandardMaterial color={HOUSING} metalness={0.7} roughness={0.42} />
</mesh>
{/* side edge rails (rounded anodized look) */}
{[-1, 1].map((s) => (
<mesh key={s} position={[s * (D435I.depth / 2 - 0.002), 0, 0]}>
<boxGeometry args={[0.004, D435I.height, w - 0.006]} />
<meshStandardMaterial color={HOUSING_EDGE} metalness={0.75} roughness={0.35} />
</mesh>
))}
{/* Full-width front glass on the downward face */}
<mesh position={[0, -D435I.height / 2 - 0.0006, 0]}>
<boxGeometry args={[D435I.depth - 0.007, 0.0012, w - 0.008]} />
<meshPhysicalMaterial
color={GLASS}
metalness={0.1}
roughness={0.08}
transparent
opacity={0.82}
/>
</mesh>
{/* Sensors along the bar: left imager / RGB / IR projector / right imager */}
<SensorWindow z={D435I.baseline / 2} radius={0.0065} />
<SensorWindow z={-D435I.baseline / 2} radius={0.0065} />
<SensorWindow z={0.012} radius={0.0042} />
<SensorWindow z={-0.011} radius={0.0052} />
{/* USB-C port on the right end (rear) */}
<mesh position={[D435I.depth / 2 - 0.001, 0.002, w / 2 - 0.008]}>
<boxGeometry args={[0.004, 0.006, 0.009]} />
<meshStandardMaterial color="#0b0d0f" metalness={0.3} roughness={0.6} />
</mesh>
{/* Tripod boss on top (mount point) */}
<mesh position={[0, D435I.height / 2 + 0.003, 0]}>
<boxGeometry args={[0.012, 0.006, 0.02]} />
<meshStandardMaterial color={HOUSING_EDGE} metalness={0.8} roughness={0.4} />
</mesh>
</group>
);
});
/**
* Debug-only measurement frustum (§9.4): optical axis, FOV pyramid,
* scan-zone rectangle on the belt, entry/exit markers.
*/
export const RealSenseFrustumDebug = memo(function RealSenseFrustumDebug() {
const h = D435I.opticalCenterY - BELT_TOP_Y;
const halfAlong = Math.tan((D435I.fovV / 2) * (Math.PI / 180)) * h; // along belt X
const halfAcross = Math.tan((D435I.fovH / 2) * (Math.PI / 180)) * h; // across belt Z
const cx = ZONES.CAMERA.x;
const top: [number, number, number] = [cx, D435I.opticalCenterY - D435I.height / 2, 0];
const y = BELT_TOP_Y;
const corners: [number, number, number][] = [
[cx - halfAlong, y, -halfAcross],
[cx + halfAlong, y, -halfAcross],
[cx + halfAlong, y, halfAcross],
[cx - halfAlong, y, halfAcross],
];
return (
<group>
{/* optical axis */}
<lineSegments>
<bufferGeometry>
<bufferAttribute
attach="attributes-position"
args={[new Float32Array([
...top, cx, y, 0,
// FOV edges
...corners.flatMap((c) => [...top, ...c]),
// FOV footprint rectangle
...corners.flatMap((c, i) => [...c, ...corners[(i + 1) % 4]]),
]), 3]}
/>
</bufferGeometry>
<lineBasicMaterial color="#38bdf8" transparent opacity={0.55} />
</lineSegments>
{/* scan zone rectangle on the belt */}
<mesh position={[(SCAN_START_X + SCAN_END_X) / 2, y + 0.003, 0]} rotation={[-Math.PI / 2, 0, 0]}>
<planeGeometry args={[SCAN_END_X - SCAN_START_X, CONVEYOR_WIDTH_M]} />
<meshBasicMaterial color="#38bdf8" transparent opacity={0.12} />
</mesh>
{/* entry / exit markers */}
{[SCAN_START_X, SCAN_END_X].map((x) => (
<mesh key={x} position={[x, y + 0.004, 0]} rotation={[-Math.PI / 2, 0, 0]}>
<planeGeometry args={[0.015, CONVEYOR_WIDTH_M]} />
<meshBasicMaterial color={x === SCAN_START_X ? '#22c55e' : '#ef4444'} transparent opacity={0.6} />
</mesh>
))}
</group>
);
});

View File

@@ -1,185 +0,0 @@
/**
* RollCageMesh — честная сетчатая модель роллтейнера C/D по ground truth.
*
* Exterior bounding box: 1200 × 800 × 800 мм (включая колёса) — ROLL_CAGE.
* Открытый верх, читаемая сетка стен (~100мм), пол-панель на CAGE_FLOOR_Y.
*
* Вся геометрия — 3 instanced draw call (трубы+прутья, колёса) + 1 mesh (пол).
* Shared roll-cage mesh for C/D receivers.
*
* Классификация узла (Stage 1 §13.4): PROCEDURAL_FALLBACK — официальной
* CAD-модели роллтейнера в архивах нет; размеры соответствуют спецификации.
*/
import { useEffect, useMemo, useRef } from 'react';
import * as THREE from 'three';
import { ROLL_CAGE, CAGE_FLOOR_Y } from '../../domain/physicalLayout';
const { width: W, depth: D, height: H, wheelRadius: WR, frameThickness: FT } = ROLL_CAGE;
const WHEEL_D = WR * 2; // 0.08m — cage floor height (CAGE_FLOOR_Y)
const BODY_H = H - WHEEL_D; // frame body above wheels; total exterior = H exactly
const ROD = 0.008; // grid rod thickness (8mm wire)
const GRID_STEP = 0.1; // ~100mm grid pitch
interface CageInstances {
boxes: THREE.Matrix4[];
wheels: THREE.Matrix4[];
}
/**
* Stage 2: roll cages are 3-sided with an OPEN FRONT on the conveyor-facing
* side (real roll-container design) plus a 40mm sill — items enter through
* the opening from the gravity chute. Matches physicsWorldLayout colliders.
*/
export type CageOpenSide = 'z-' | 'z+' | 'none';
function boxInstance(x: number, y: number, z: number, sx: number, sy: number, sz: number): THREE.Matrix4 {
return new THREE.Matrix4().compose(
new THREE.Vector3(x, y, z),
new THREE.Quaternion(),
new THREE.Vector3(sx, sy, sz),
);
}
/** Deterministic instance layout for the cage (built once per open side). */
function buildInstances(openSide: CageOpenSide): CageInstances {
const boxes: THREE.Matrix4[] = [];
const yBot = WHEEL_D; // bottom of frame body
const yTop = H; // top of frame body (exterior top)
const openSign = openSide === 'z-' ? -1 : openSide === 'z+' ? 1 : 0;
// 4 corner posts
for (const sx of [-1, 1]) {
for (const sz of [-1, 1]) {
boxes.push(boxInstance(sx * (W / 2 - FT / 2), yBot + BODY_H / 2, sz * (D / 2 - FT / 2), FT, BODY_H, FT));
}
}
// bottom + top frame rectangles (skip the open side's tubes; sill added below)
for (const y of [yBot + FT / 2, yTop - FT / 2]) {
for (const sz of [-1, 1]) {
if (sz === openSign && y === yTop - FT / 2) continue; // open front: no top tube
boxes.push(boxInstance(0, y, sz * (D / 2 - FT / 2), W, FT, FT));
}
boxes.push(boxInstance(W / 2 - FT / 2, y, 0, FT, FT, D - FT * 2));
boxes.push(boxInstance(-(W / 2 - FT / 2), y, 0, FT, FT, D - FT * 2));
}
// 40mm sill across the open front (matches entry-sill collider)
if (openSign !== 0) {
boxes.push(boxInstance(0, yBot + 0.02, openSign * (D / 2 - FT / 2), W, 0.04, FT));
}
// grid walls between frames (interior span)
const yGridBot = yBot + FT;
const yGridTop = yTop - FT;
const gridH = yGridTop - yGridBot;
const yMid = yGridBot + gridH / 2;
const xInner = W / 2 - FT; // inner half-width
const zInner = D / 2 - FT;
// front/back walls (z = ±(D/2 ROD/2)): vertical + horizontal rods
const vCols = Math.floor((xInner * 2) / GRID_STEP) - 1; // exclude corners (posts)
const hRows = Math.max(1, Math.round(gridH / GRID_STEP) - 1);
for (const sz of [-1, 1]) {
if (sz === openSign) continue; // open front: no grid wall
const z = sz * (D / 2 - ROD / 2);
for (let i = 1; i <= vCols; i++) {
const x = -xInner + (i * (xInner * 2)) / (vCols + 1);
boxes.push(boxInstance(x, yMid, z, ROD, gridH, ROD));
}
for (let r = 1; r <= hRows; r++) {
const y = yGridBot + (r * gridH) / (hRows + 1);
boxes.push(boxInstance(0, y, z, W - FT * 2, ROD, ROD));
}
}
// side walls (x = ±(W/2 ROD/2))
const sCols = Math.floor((zInner * 2) / GRID_STEP) - 1;
for (const sx of [-1, 1]) {
const x = sx * (W / 2 - ROD / 2);
for (let i = 1; i <= sCols; i++) {
const z = -zInner + (i * (zInner * 2)) / (sCols + 1);
boxes.push(boxInstance(x, yMid, z, ROD, gridH, ROD));
}
for (let r = 1; r <= hRows; r++) {
const y = yGridBot + (r * gridH) / (hRows + 1);
boxes.push(boxInstance(x, y, 0, ROD, ROD, D - FT * 2));
}
}
// caster wheels (lying cylinders)
const wheels: THREE.Matrix4[] = [];
const wheelQuat = new THREE.Quaternion().setFromEuler(new THREE.Euler(0, 0, Math.PI / 2));
for (const sx of [-1, 1]) {
for (const sz of [-1, 1]) {
wheels.push(new THREE.Matrix4().compose(
new THREE.Vector3(sx * (W / 2 - 0.08), WR, sz * (D / 2 - 0.08)),
wheelQuat,
new THREE.Vector3(1, 1, 1),
));
}
}
return { boxes, wheels };
}
export default function RollCageMesh({ color, active = false, shadows = false, openSide = 'none' }: {
color: string;
active?: boolean;
shadows?: boolean;
openSide?: CageOpenSide;
}) {
const instances = useMemo(() => buildInstances(openSide), [openSide]);
const boxGeo = useMemo(() => new THREE.BoxGeometry(1, 1, 1), []);
const wheelGeo = useMemo(() => new THREE.CylinderGeometry(WR, WR, 0.03, 12), []);
const boxesRef = useRef<THREE.InstancedMesh>(null);
const wheelsRef = useRef<THREE.InstancedMesh>(null);
useEffect(() => {
const boxes = boxesRef.current;
if (boxes) {
instances.boxes.forEach((m, i) => boxes.setMatrixAt(i, m));
boxes.instanceMatrix.needsUpdate = true;
}
const wheels = wheelsRef.current;
if (wheels) {
instances.wheels.forEach((m, i) => wheels.setMatrixAt(i, m));
wheels.instanceMatrix.needsUpdate = true;
}
}, [instances]);
useEffect(() => () => {
boxGeo.dispose();
wheelGeo.dispose();
}, [boxGeo, wheelGeo]);
const emissiveIntensity = active ? 0.35 : 0;
return (
<group>
{/* frame + grid walls: single instanced draw call */}
<instancedMesh
ref={boxesRef}
args={[boxGeo, undefined, instances.boxes.length]}
castShadow={shadows}
>
<meshStandardMaterial
color={color}
metalness={0.6}
roughness={0.35}
emissive={color}
emissiveIntensity={emissiveIntensity}
/>
</instancedMesh>
{/* caster wheels: single instanced draw call */}
<instancedMesh ref={wheelsRef} args={[wheelGeo, undefined, instances.wheels.length]}>
<meshStandardMaterial color="#475569" metalness={0.7} roughness={0.3} />
</instancedMesh>
{/* interior floor pan where items rest (top at CAGE_FLOOR_Y) */}
<mesh position={[0, CAGE_FLOOR_Y - 0.005, 0]} receiveShadow={shadows}>
<boxGeometry args={[W - FT, 0.01, D - FT]} />
<meshStandardMaterial color="#1e293b" metalness={0.3} roughness={0.7} />
</mesh>
</group>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,181 +0,0 @@
/**
* Stage 2 — physics world for the sorter drop segment.
*
* Hybrid authority (docs/stage2_real_sorter/physics-architecture.md):
* - items on the belt are KINEMATIC (domain pose is truth);
* - at the drop handoff (pusher contact / belt edge) the body switches to
* DYNAMIC with deterministic initial velocity;
* - static colliders mirror the visible chute / receiver geometry
* (documented hidden colliders, same dimensions as the visuals).
*
* Determinism: fixed dt = 1/60, max 4 substeps/frame, no unseeded randomness.
* Physics freezes when the domain clock is paused (documented simulation
* assumption — belt, gate and items halt together; EMERGENCY_STOP creates no
* new impulses).
*
* Stage 2E: Rapier step timing via PhysicsPerfSampler (?perf=1 | ?physicsPerf=1).
* Render time is NOT included in physics p95.
*/
import { useRef, type ReactNode } from 'react';
import { useFrame } from '@react-three/fiber';
import { Physics, RigidBody, CuboidCollider, useRapier } from '@react-three/rapier';
import { getStaticColliders } from '../../domain/physicsWorldLayout';
import { PHYSICS_TIMESTEP_SEC } from '../../domain/physicsTimestep';
import {
PhysicsPerfSampler,
isPhysicsPerfQueryEnabled,
type PhysicsPerfSnapshot,
} from '../../domain/physicsPerf';
export const PHYSICS_DT = PHYSICS_TIMESTEP_SEC;
export const PHYSICS_MAX_SUBSTEPS = 4;
const MAX_SUBSTEPS = PHYSICS_MAX_SUBSTEPS;
/**
* Physics-time clock (seconds actually simulated by THIS world instance).
* Kinematic mechanisms must be driven by this clock — never by the domain
* wall clock — because under render lag the stepper burns at most
* MAX_SUBSTEPS per frame and physics time falls behind domain time.
*/
export const physicsSimClock = { simSec: 0 };
export function resetPhysicsSimClock() {
physicsSimClock.simSec = 0;
}
/** Drop verification record (debug/e2e introspection, no secrets). */
export interface DropResult {
caseId: string;
itemId: string;
expectedZone: 'B' | 'C' | 'D';
finalPosition: [number, number, number];
insideExpectedReceiver: boolean;
settledByTimeout: boolean;
timestampMs: number;
}
declare global {
interface Window {
__DROP_RESULTS?: DropResult[];
__PHYSICS_PERF__?: PhysicsPerfSnapshot;
__PHYSICS_PERF_RESET__?: () => void;
}
}
export function recordDropResult(result: DropResult) {
if (typeof window !== 'undefined') {
window.__DROP_RESULTS = [...(window.__DROP_RESULTS ?? []).slice(-49), result];
}
}
function readWorldMeta(world: {
bodies?: { len: () => number };
colliders?: { len: () => number };
}): { activeBodies: number; sleepingBodies: number; colliders: number; contactPairs: number } {
try {
// @react-three/rapier wraps Rapier world; body counts via forEach when available
const w = world as unknown as {
forEachRigidBody?: (cb: (b: { isSleeping: () => boolean; numColliders: () => number }) => void) => void;
bodies?: { len: () => number };
colliders?: { len: () => number };
};
let active = 0;
let sleeping = 0;
let colliders = 0;
if (typeof w.forEachRigidBody === 'function') {
w.forEachRigidBody((b) => {
if (b.isSleeping()) sleeping += 1;
else active += 1;
try {
colliders += b.numColliders();
} catch {
/* ignore */
}
});
} else {
active = w.bodies?.len?.() ?? 0;
colliders = w.colliders?.len?.() ?? 0;
}
return { activeBodies: active, sleepingBodies: sleeping, colliders, contactPairs: 0 };
} catch {
return { activeBodies: 0, sleepingBodies: 0, colliders: 0, contactPairs: 0 };
}
}
/** Steps the Rapier world with a fixed dt, scaled by domain playback speed. */
function RapierStepper({ running, speed }: { running: boolean; speed: number }) {
const { world } = useRapier();
const accumulator = useRef(0);
const sampler = useRef(new PhysicsPerfSampler(PHYSICS_DT, MAX_SUBSTEPS));
const perfOn = useRef(false);
// Latch query once (and expose reset) — no React state.
if (typeof window !== 'undefined' && !perfOn.current) {
perfOn.current = isPhysicsPerfQueryEnabled();
if (perfOn.current) {
window.__PHYSICS_PERF_RESET__ = () => sampler.current.reset();
}
}
useFrame((_, delta) => {
if (!running) return;
accumulator.current += Math.min(delta, 0.1) * speed;
let steps = 0;
let framePhysicsMs = 0;
while (accumulator.current >= PHYSICS_DT && steps < MAX_SUBSTEPS) {
if (perfOn.current) {
const t0 = performance.now();
world.step();
framePhysicsMs += performance.now() - t0;
} else {
world.step();
}
physicsSimClock.simSec += PHYSICS_DT;
accumulator.current -= PHYSICS_DT;
steps += 1;
}
if (perfOn.current && steps > 0) {
// Record per-frame physics cost (sum of substeps this frame), not render.
sampler.current.pushStepMs(framePhysicsMs, steps);
window.__PHYSICS_PERF__ = sampler.current.snapshot(readWorldMeta(world));
}
if (steps === MAX_SUBSTEPS) accumulator.current = 0;
});
return null;
}
/** Static colliders for the whole working area (fixed bodies, cheap cuboids).
* Layout data lives in domain/physicsWorldLayout — shared with headless tests. */
export function SorterStaticColliders() {
return (
<RigidBody type="fixed" colliders={false}>
{getStaticColliders().map((c) => (
<CuboidCollider
key={c.id}
args={c.halfExtents}
position={c.position}
rotation={c.rotation}
friction={c.friction}
/>
))}
</RigidBody>
);
}
export function SorterPhysicsWorld({
running,
speed,
children,
}: {
running: boolean;
speed: number;
children: ReactNode;
}) {
return (
<Physics updateLoop="independent" paused timeStep={PHYSICS_DT} gravity={[0, -9.81, 0]}>
<RapierStepper running={running} speed={speed} />
<SorterStaticColliders />
{children}
</Physics>
);
}

View File

@@ -1,143 +0,0 @@
import { Component, ReactNode } from 'react';
interface Props {
children: ReactNode;
onError?: (error: Error) => void;
/** Explicit switch-to-2D action (replaces the old dead `use-2d-fallback` event). */
onUse2D?: () => void;
}
interface State {
hasError: boolean;
error: Error | null;
}
/**
* ErrorBoundary для 3D Canvas.
* Ловит ошибки Three.js/WebGL и показывает fallback вместо чёрного экрана.
*/
export default class ThreeErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: unknown) {
console.error('3D Canvas error caught by ErrorBoundary:', error, errorInfo);
this.props.onError?.(error);
}
handleReload = () => {
this.setState({ hasError: false, error: null });
window.location.reload();
};
handleUse2D = () => {
this.setState({ hasError: false, error: null });
this.props.onUse2D?.();
};
render() {
if (this.state.hasError) {
return (
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
minHeight: '400px',
padding: '24px',
border: '1px solid rgba(251, 61, 78, 0.3)',
borderRadius: '12px',
background: 'rgba(251, 61, 78, 0.05)',
color: '#e5f2ff',
}}
>
<svg
width="48"
height="48"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
style={{ marginBottom: '16px', color: '#fb3d4e' }}
>
<circle cx="12" cy="12" r="10" />
<line x1="12" y1="8" x2="12" y2="12" />
<line x1="12" y1="16" x2="12.01" y2="16" />
</svg>
<h3 style={{ margin: '0 0 8px 0', fontSize: '18px', fontWeight: 600 }}>
3D Scene Failed
</h3>
<p style={{ margin: '0 0 20px 0', color: 'rgba(229, 242, 255, 0.7)', fontSize: '14px', textAlign: 'center', maxWidth: '400px' }}>
3D rendering encountered an error. You can reload or switch to stable 2D fallback.
</p>
{import.meta.env.DEV && this.state.error && (
<pre
style={{
fontSize: '12px',
color: '#fb3d4e',
background: 'rgba(0, 0, 0, 0.3)',
padding: '12px',
borderRadius: '6px',
maxWidth: '100%',
overflow: 'auto',
marginBottom: '20px',
}}
>
{this.state.error.message}
</pre>
)}
<div style={{ display: 'flex', gap: '12px' }}>
<button
type="button"
onClick={this.handleReload}
style={{
padding: '10px 20px',
background: 'rgba(56, 189, 248, 0.15)',
border: '1px solid rgba(56, 189, 248, 0.3)',
borderRadius: '8px',
color: '#38bdf8',
cursor: 'pointer',
fontSize: '14px',
fontWeight: 600,
}}
>
Reload 3D
</button>
{this.props.onUse2D && (
<button
type="button"
onClick={this.handleUse2D}
style={{
padding: '10px 20px',
background: 'rgba(148, 163, 184, 0.15)',
border: '1px solid rgba(148, 163, 184, 0.3)',
borderRadius: '8px',
color: '#94a3b8',
cursor: 'pointer',
fontSize: '14px',
fontWeight: 600,
}}
>
Use 2D Fallback
</button>
)}
</div>
</div>
);
}
return this.props.children;
}
}

View File

@@ -1,28 +0,0 @@
import { useEffect, useState } from 'react';
export function detectWebGL(): boolean {
try {
const canvas = document.createElement('canvas');
return Boolean(
canvas.getContext('webgl2') ||
canvas.getContext('webgl') ||
canvas.getContext('experimental-webgl'),
);
} catch {
return false;
}
}
export function useWebGLSupport(): boolean {
const [supported, setSupported] = useState(true);
useEffect(() => {
setSupported(detectWebGL());
}, []);
return supported;
}
export function prefer3DByDefault(width: number, webgl: boolean): boolean {
return webgl && width >= 640;
}

View File

@@ -1,111 +0,0 @@
import type { Item } from '../domain/types';
export const ITEMS: Item[] = [
{
id: 'SKU-001',
name: 'Box 300x200x200',
dimensionsMm: { width: 300, depth: 200, height: 200 },
roundness: 0.12,
confidence: 0.94,
shape: 'box',
expectedCategory: 'B',
},
{
id: 'SKU-002',
name: 'Lunchbox 201x152x62',
dimensionsMm: { width: 201, depth: 152, height: 62 },
roundness: 0.22,
confidence: 0.91,
shape: 'rectangular prism',
expectedCategory: 'B',
},
{
id: 'SKU-003',
name: 'Detergent 259x179x278',
dimensionsMm: { width: 259, depth: 179, height: 278 },
roundness: 0.38,
confidence: 0.88,
shape: 'bottle box',
expectedCategory: 'B',
},
{
id: 'SKU-004',
name: 'Oversized box 401x300x400',
dimensionsMm: { width: 401, depth: 300, height: 400 },
roundness: 0.18,
confidence: 0.9,
shape: 'oversized box',
expectedCategory: 'C',
},
{
id: 'SKU-005',
name: 'Pouf 489x264x489',
dimensionsMm: { width: 489, depth: 264, height: 489 },
roundness: 0.74,
confidence: 0.86,
shape: 'soft bulky item',
expectedCategory: 'C',
},
{
id: 'SKU-006',
name: 'Plate 210x209x27',
dimensionsMm: { width: 210, depth: 209, height: 27 },
roundness: 0.95,
confidence: 0.89,
shape: 'round plate',
expectedCategory: 'D',
},
{
id: 'SKU-007',
name: 'Bottle 91x91x305',
dimensionsMm: { width: 91, depth: 91, height: 305 },
roundness: 0.92,
confidence: 0.93,
shape: 'cylinder bottle',
expectedCategory: 'D',
},
{
id: 'SKU-008',
name: 'Cylinder 435x50x43',
dimensionsMm: { width: 435, depth: 50, height: 43 },
roundness: 0.88,
confidence: 0.87,
shape: 'long cylinder',
expectedCategory: 'D',
},
{
id: 'SKU-009',
name: 'Pen 9x13x148',
dimensionsMm: { width: 9, depth: 13, height: 148 },
roundness: 0.66,
confidence: 0.84,
shape: 'thin item',
expectedCategory: 'C',
},
{
id: 'SKU-010',
name: 'Near-max box 449x319x319',
dimensionsMm: { width: 449, depth: 319, height: 319 },
roundness: 0.2,
confidence: 0.9,
shape: 'boundary box',
expectedCategory: 'B',
},
{
id: 'SKU-011',
name: 'Oversized round 500x300x300',
dimensionsMm: { width: 500, depth: 300, height: 300 },
roundness: 0.93,
confidence: 0.88,
shape: 'oversized round cylinder',
expectedCategory: 'C',
},
];
export function getItem(id: string): Item {
const item = ITEMS.find((candidate) => candidate.id === id);
if (!item) {
throw new Error(`Unknown item id: ${id}`);
}
return item;
}

View File

@@ -1,149 +0,0 @@
import { describe, it, expect } from 'vitest';
import {
MODEL_ASSETS,
ARCHIVE_ONLY_MODELS,
getModelAsset,
getRealAssets,
getProceduralAssets,
getManifestStats,
} from './modelAssets';
// Runtime files under public/ (project convention: import.meta.glob instead of node:fs)
const RUNTIME_MODEL_FILES = Object.keys(
import.meta.glob('../../public/models/*.stl', { eager: true, query: '?url', import: 'default' }),
).map((p) => p.replace(/^.*\/public/, ''));
describe('modelAssets (Stage 1 real-model manifest)', () => {
describe('MODEL_ASSETS', () => {
it('should contain assets for all scenario items', () => {
const expectedIds = [
'SKU-001', 'SKU-002', 'SKU-003', 'SKU-004', 'SKU-005', 'SKU-006',
'SKU-007', 'SKU-008', 'SKU-009', 'SKU-010', 'SKU-011',
];
const actualIds = MODEL_ASSETS.map((asset) => asset.itemId);
expectedIds.forEach((id) => {
expect(actualIds).toContain(id);
});
});
it('should have a unique SKU per entry', () => {
const ids = MODEL_ASSETS.map((asset) => asset.itemId);
expect(new Set(ids).size).toBe(ids.length);
});
it('should define a fallback primitive for every asset', () => {
MODEL_ASSETS.forEach((asset) => {
expect(asset.fallbackPrimitive).toMatch(/^(box|cylinder|sphere)$/);
});
});
it('should have valid expected dimensions for every asset', () => {
MODEL_ASSETS.forEach((asset) => {
expect(asset.dimensions.width).toBeGreaterThan(0);
expect(asset.dimensions.depth).toBeGreaterThan(0);
expect(asset.dimensions.height).toBeGreaterThan(0);
});
});
it('runtime paths must exist on disk for every real asset', () => {
for (const asset of MODEL_ASSETS) {
if (asset.defaultRealAsset) {
expect(asset.runtimePath, `${asset.itemId} runtimePath`).toBeTruthy();
expect(
RUNTIME_MODEL_FILES,
`${asset.itemId}${asset.runtimePath} must exist in public/models`,
).toContain(asset.runtimePath);
} else {
expect(asset.runtimePath).toBeNull();
}
}
});
it('real assets must declare provenance (archive, file, sha256)', () => {
for (const asset of getRealAssets()) {
expect(asset.sourceArchive).toBe('input_info/doc-1782987733.zip');
expect(asset.sourceFile).toBeTruthy();
expect(asset.sourceSha256).toMatch(/^[0-9a-f]{64}$/);
expect(asset.runtimeSha256).toMatch(/^[0-9a-f]{64}$/);
expect(asset.sourceBoundingBoxMm).not.toBeNull();
expect(asset.worldExpectedMm).not.toBeNull();
expect(asset.triangleCount).toBeGreaterThan(0);
expect(asset.fileSizeBytes).toBeGreaterThan(0);
}
});
it('real assets use bottom-center pivot and uniform mm→m scale only', () => {
for (const asset of getRealAssets()) {
expect(asset.pivotMode).toBe('bottom-center');
expect(asset.scaleMode).toBe('uniform-mm-to-m');
}
});
it('must NOT silently substitute another model for a SKU', () => {
// SKU-011 previously reused cylinder.stl — forbidden now.
const sku011 = getModelAsset('SKU-011');
expect(sku011?.defaultRealAsset).toBe(false);
expect(sku011?.runtimePath).toBeNull();
expect(sku011?.notes).toContain('NO_EXACT_OFFICIAL_MODEL');
});
});
describe('official test-set coverage', () => {
it('integrates the 9 official models that have matching SKUs', () => {
const realIds = getRealAssets().map((a) => a.itemId).sort();
expect(realIds).toEqual([
'SKU-001', 'SKU-002', 'SKU-003', 'SKU-004', 'SKU-005',
'SKU-006', 'SKU-007', 'SKU-008', 'SKU-009',
]);
});
it('marks SKUs without an exact official model honestly', () => {
for (const id of ['SKU-010', 'SKU-011']) {
const asset = getModelAsset(id);
expect(asset?.defaultRealAsset).toBe(false);
expect(asset?.notes).toContain('NO_EXACT_OFFICIAL_MODEL');
}
});
it('documents archive-only official models (bag, helmet)', () => {
const names = ARCHIVE_ONLY_MODELS.map((m) => m.displayName);
expect(names).toContain('Мешок');
expect(names).toContain('Шлем');
ARCHIVE_ONLY_MODELS.forEach((m) => {
expect(m.sourceSha256).toMatch(/^[0-9a-f]{64}$/);
});
});
it('respects runtime file budgets (<= 1.5MB hard, <= 100k tris acceptable)', () => {
for (const asset of getRealAssets()) {
expect(asset.fileSizeBytes!, `${asset.itemId} file size`).toBeLessThanOrEqual(1.5 * 1024 * 1024);
expect(asset.triangleCount!, `${asset.itemId} triangles`).toBeLessThanOrEqual(100_000);
}
});
});
describe('getModelAsset', () => {
it('should return asset for valid item ID', () => {
const asset = getModelAsset('SKU-006');
expect(asset).toBeDefined();
expect(asset?.displayName).toBe('Тарелка');
expect(asset?.categoryScenario).toBe('D');
});
it('should return undefined for invalid item ID', () => {
const asset = getModelAsset('SKU-999');
expect(asset).toBeUndefined();
});
});
describe('getManifestStats', () => {
it('should return correct totals', () => {
const stats = getManifestStats();
expect(stats.total).toBe(MODEL_ASSETS.length);
expect(stats.real).toBe(getRealAssets().length);
expect(stats.procedural).toBe(getProceduralAssets().length);
expect(stats.real + stats.procedural).toBe(stats.total);
expect(stats.realPercentage).toBe(Math.round((stats.real / stats.total) * 100));
});
});
});

Some files were not shown because too many files have changed in this diff Show More