From 4f253b860f141e704ae1c36500a5f46aea0380ee Mon Sep 17 00:00:00 2001 From: gitrusprus Date: Fri, 17 Jul 2026 12:25:00 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A0=D0=B5=D1=81=D1=82=D1=80=D1=83=D0=BA?= =?UTF-8?q?=D1=82=D1=83=D1=80=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20=D0=BF?= =?UTF-8?q?=D1=80=D0=BE=D0=B5=D0=BA=D1=82=D0=B0=20=D0=B8=20=D0=B3=D0=B5?= =?UTF-8?q?=D0=BD=D0=B5=D1=80=D0=B0=D1=82=D0=BE=D1=80=20=D1=81=D0=B8=D0=BD?= =?UTF-8?q?=D1=82=D0=B5=D1=82=D0=B8=D1=87=D0=B5=D1=81=D0=BA=D0=B8=D1=85=20?= =?UTF-8?q?=D0=B4=D0=B0=D1=82=D0=B0=D1=81=D0=B5=D1=82=D0=BE=D0=B2=20=D1=8D?= =?UTF-8?q?=D1=85=D0=BE=D0=BB=D0=BE=D1=82=D0=B0.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Перенесены backend/frontend/desktop/engine, добавлены вкладки конструктора сцен и генератора датасета с параметрами лучей и длины сетки рельефа, обновлены API и Docker-сборка. Co-authored-by: Cursor --- .gitignore | 8 + ARCHITECTURE.md | 115 +++ DotsToSirface.pro | 104 --- DotsToSirfaceTests.pro | 92 -- DotsToSurface.pro | 104 +++ DotsToSurfaceTests.pro | 92 ++ README.md | 62 +- assets/airplane_reference.png | Bin 0 -> 105091 bytes backend/README.md | 36 + {api => backend}/builtin_presets.py | 0 backend/dataset_generator.py | 785 ++++++++++++++++++ {api => backend}/demo_generator.py | 0 {api => backend}/main.py | 291 ++++++- {api => backend}/pipeline_insights.py | 2 +- {api => backend}/requirements.txt | 0 backend/scene_generator.py | 674 +++++++++++++++ {api => backend}/stage_meta.py | 0 docker/Dockerfile | 17 +- docker/README.md | 44 + docker/docker-compose.yml | 8 +- {web-vue => frontend/web}/.gitignore | 0 frontend/web/README.md | 15 + {web-vue => frontend/web}/index.html | 2 +- {web-vue => frontend/web}/package-lock.json | 4 +- {web-vue => frontend/web}/package.json | 2 +- frontend/web/public/airplane_reference.png | Bin 0 -> 105091 bytes frontend/web/src/App.vue | 111 +++ frontend/web/src/api/client.js | 325 ++++++++ .../web}/src/catalog/pipelineUiCatalog.js | 0 .../web}/src/catalog/theme.js | 0 .../web}/src/components/PipelineDashboard.vue | 17 +- .../web}/src/components/WizardPanel.vue | 0 .../components/generator/GeneratorSidebar.vue | 461 ++++++++++ .../components/generator/GeneratorViewer.vue | 202 +++++ .../generator/LayerSettingsForm.vue | 66 ++ .../components/generator/TransformToolbar.vue | 109 +++ .../src/components/pipeline/MetricsStrip.vue | 0 .../components/pipeline/PipelineCanvas.vue | 0 .../components/pipeline/StageChainView.vue | 0 .../src/components/pipeline/StagePalette.vue | 0 .../pipeline/StageSettingsDialog.vue | 0 .../src/components/viewer/ViewerModebar.vue | 0 .../src/components/viewer/ViewerPanel.vue | 12 +- .../src/composables/usePointCloudViewer.js | 104 ++- .../src/composables/useSceneCloudViewer.js | 293 +++++++ .../web}/src/composables/useTheme.js | 2 +- {web-vue => frontend/web}/src/main.js | 2 + frontend/web/src/router/index.js | 13 + frontend/web/src/stores/dataset.js | 215 +++++ frontend/web/src/stores/generator.js | 517 ++++++++++++ .../web}/src/stores/pipeline.js | 29 +- {web-vue => frontend/web}/src/styles/main.css | 0 frontend/web/src/views/DatasetView.vue | 475 +++++++++++ frontend/web/src/views/GeneratorView.vue | 37 + frontend/web/src/views/PipelineView.vue | 33 + {web-vue => frontend/web}/vite.config.js | 0 scripts/run-docker.sh | 5 + src/README.md | 39 + src/{ui => desktop}/glview.cpp | 0 src/{ui => desktop}/glview.h | 2 +- src/{ui => desktop}/mainwindow.cpp | 10 +- src/{ui => desktop}/mainwindow.h | 6 +- .../qml/components/StageInspector.qml | 0 .../qml/components/WizardPanel.qml | 0 .../qml/components/pipeline/MetricsStrip.qml | 0 .../components/pipeline/PipelineCanvas.qml | 0 .../components/pipeline/StageChainView.qml | 0 .../qml/components/pipeline/StagePalette.qml | 0 .../qml/dialogs/StageSettingsDialog.qml | 0 .../qml/pages/PipelineDashboard.qml | 0 .../qml/shared/PipelineUiCatalog.js | 0 src/{ui => desktop}/qml/shared/Theme.js | 0 src/{ui => desktop}/qml/ui_qml.qrc | 0 .../adapters/pcl/pcl_point_cloud_adapter.cpp | 0 .../adapters/pcl/pcl_point_cloud_adapter.h | 0 .../reconstruction/reconstruction_adapter.cpp | 0 .../reconstruction/reconstruction_adapter.h | 0 .../registration/registration_adapter.cpp | 0 .../registration/registration_adapter.h | 0 .../adapters/ros2/pointcloud2_adapter.cpp | 0 .../adapters/ros2/pointcloud2_adapter.h | 0 .../adapters/ros2/tf2_adapter.cpp | 0 src/{ => engine}/adapters/ros2/tf2_adapter.h | 0 .../sources/file_point_cloud_source.cpp | 0 .../sources/file_point_cloud_source.h | 0 .../sources/ply_point_cloud_loader.cpp | 0 .../adapters/sources/ply_point_cloud_loader.h | 0 .../sources/ros2_point_cloud_source.cpp | 0 .../sources/ros2_point_cloud_source.h | 0 .../preprocess/preprocess_algorithms.cpp | 0 .../preprocess/preprocess_algorithms.h | 0 .../reconstruction/surface_reconstruction.cpp | 0 .../reconstruction/surface_reconstruction.h | 0 src/{ => engine}/cli/pipeline_cli_runner.cpp | 4 +- src/{ => engine}/cli/pipeline_cli_runner.h | 0 src/{ => engine}/core/data_source.h | 0 src/{ => engine}/core/hole_aware_score.cpp | 0 src/{ => engine}/core/hole_aware_score.h | 0 src/{ => engine}/core/pipeline_config.cpp | 0 src/{ => engine}/core/pipeline_config.h | 0 .../core/pipeline_config_validation.cpp | 0 .../core/pipeline_config_validation.h | 0 src/{ => engine}/core/pipeline_executor.cpp | 0 src/{ => engine}/core/pipeline_executor.h | 0 .../core/pipeline_plugin_registry.cpp | 0 .../core/pipeline_plugin_registry.h | 0 src/{ => engine}/core/pipeline_stage.h | 0 .../core/pipeline_stage_defaults.cpp | 0 .../core/pipeline_stage_defaults.h | 0 src/{ => engine}/core/point_cloud_types.h | 0 src/{ => engine}/core/tuning_service.cpp | 0 src/{ => engine}/core/tuning_service.h | 0 .../pipeline/desktop_pipeline_factory.cpp | 0 .../pipeline/desktop_pipeline_factory.h | 0 .../strategies/preprocess_basic_stages.cpp | 0 .../strategies/preprocess_basic_stages.h | 0 .../strategies/preprocess_pcl_stages.cpp | 0 .../strategies/preprocess_pcl_stages.h | 0 .../reconstruction_pcl_greedy_stage.h | 0 .../reconstruction_pcl_poisson_stage.h | 0 .../strategies/reconstruction_surface_stage.h | 0 .../strategies/registration_icp_stage.cpp | 0 .../strategies/registration_icp_stage.h | 0 .../strategies/transform_tf_stage.cpp | 0 .../strategies/transform_tf_stage.h | 0 .../tests/pipeline_smoke_tests.cpp | 0 src/{ => engine}/tests/pipeline_smoke_tests.h | 0 src/{ => engine}/tests/test_runner_main.cpp | 0 src/main.cpp | 4 +- web-vue/src/App.vue | 71 -- web-vue/src/api/client.js | 54 -- web/app.js | 236 ------ web/index.html | 61 -- web/style.css | 154 ---- 134 files changed, 5263 insertions(+), 863 deletions(-) create mode 100644 ARCHITECTURE.md delete mode 100644 DotsToSirface.pro delete mode 100644 DotsToSirfaceTests.pro create mode 100644 DotsToSurface.pro create mode 100644 DotsToSurfaceTests.pro create mode 100644 assets/airplane_reference.png create mode 100644 backend/README.md rename {api => backend}/builtin_presets.py (100%) create mode 100644 backend/dataset_generator.py rename {api => backend}/demo_generator.py (100%) rename {api => backend}/main.py (61%) rename {api => backend}/pipeline_insights.py (94%) rename {api => backend}/requirements.txt (100%) create mode 100644 backend/scene_generator.py rename {api => backend}/stage_meta.py (100%) create mode 100644 docker/README.md rename {web-vue => frontend/web}/.gitignore (100%) create mode 100644 frontend/web/README.md rename {web-vue => frontend/web}/index.html (88%) rename {web-vue => frontend/web}/package-lock.json (99%) rename {web-vue => frontend/web}/package.json (93%) create mode 100644 frontend/web/public/airplane_reference.png create mode 100644 frontend/web/src/App.vue create mode 100644 frontend/web/src/api/client.js rename {web-vue => frontend/web}/src/catalog/pipelineUiCatalog.js (100%) rename {web-vue => frontend/web}/src/catalog/theme.js (100%) rename {web-vue => frontend/web}/src/components/PipelineDashboard.vue (85%) rename {web-vue => frontend/web}/src/components/WizardPanel.vue (100%) create mode 100644 frontend/web/src/components/generator/GeneratorSidebar.vue create mode 100644 frontend/web/src/components/generator/GeneratorViewer.vue create mode 100644 frontend/web/src/components/generator/LayerSettingsForm.vue create mode 100644 frontend/web/src/components/generator/TransformToolbar.vue rename {web-vue => frontend/web}/src/components/pipeline/MetricsStrip.vue (100%) rename {web-vue => frontend/web}/src/components/pipeline/PipelineCanvas.vue (100%) rename {web-vue => frontend/web}/src/components/pipeline/StageChainView.vue (100%) rename {web-vue => frontend/web}/src/components/pipeline/StagePalette.vue (100%) rename {web-vue => frontend/web}/src/components/pipeline/StageSettingsDialog.vue (100%) rename {web-vue => frontend/web}/src/components/viewer/ViewerModebar.vue (100%) rename {web-vue => frontend/web}/src/components/viewer/ViewerPanel.vue (96%) rename {web-vue => frontend/web}/src/composables/usePointCloudViewer.js (63%) create mode 100644 frontend/web/src/composables/useSceneCloudViewer.js rename {web-vue => frontend/web}/src/composables/useTheme.js (93%) rename {web-vue => frontend/web}/src/main.js (84%) create mode 100644 frontend/web/src/router/index.js create mode 100644 frontend/web/src/stores/dataset.js create mode 100644 frontend/web/src/stores/generator.js rename {web-vue => frontend/web}/src/stores/pipeline.js (91%) rename {web-vue => frontend/web}/src/styles/main.css (100%) create mode 100644 frontend/web/src/views/DatasetView.vue create mode 100644 frontend/web/src/views/GeneratorView.vue create mode 100644 frontend/web/src/views/PipelineView.vue rename {web-vue => frontend/web}/vite.config.js (100%) create mode 100755 scripts/run-docker.sh create mode 100644 src/README.md rename src/{ui => desktop}/glview.cpp (100%) rename src/{ui => desktop}/glview.h (92%) rename src/{ui => desktop}/mainwindow.cpp (96%) rename src/{ui => desktop}/mainwindow.h (95%) rename src/{ui => desktop}/qml/components/StageInspector.qml (100%) rename src/{ui => desktop}/qml/components/WizardPanel.qml (100%) rename src/{ui => desktop}/qml/components/pipeline/MetricsStrip.qml (100%) rename src/{ui => desktop}/qml/components/pipeline/PipelineCanvas.qml (100%) rename src/{ui => desktop}/qml/components/pipeline/StageChainView.qml (100%) rename src/{ui => desktop}/qml/components/pipeline/StagePalette.qml (100%) rename src/{ui => desktop}/qml/dialogs/StageSettingsDialog.qml (100%) rename src/{ui => desktop}/qml/pages/PipelineDashboard.qml (100%) rename src/{ui => desktop}/qml/shared/PipelineUiCatalog.js (100%) rename src/{ui => desktop}/qml/shared/Theme.js (100%) rename src/{ui => desktop}/qml/ui_qml.qrc (100%) rename src/{ => engine}/adapters/pcl/pcl_point_cloud_adapter.cpp (100%) rename src/{ => engine}/adapters/pcl/pcl_point_cloud_adapter.h (100%) rename src/{ => engine}/adapters/reconstruction/reconstruction_adapter.cpp (100%) rename src/{ => engine}/adapters/reconstruction/reconstruction_adapter.h (100%) rename src/{ => engine}/adapters/registration/registration_adapter.cpp (100%) rename src/{ => engine}/adapters/registration/registration_adapter.h (100%) rename src/{ => engine}/adapters/ros2/pointcloud2_adapter.cpp (100%) rename src/{ => engine}/adapters/ros2/pointcloud2_adapter.h (100%) rename src/{ => engine}/adapters/ros2/tf2_adapter.cpp (100%) rename src/{ => engine}/adapters/ros2/tf2_adapter.h (100%) rename src/{ => engine}/adapters/sources/file_point_cloud_source.cpp (100%) rename src/{ => engine}/adapters/sources/file_point_cloud_source.h (100%) rename src/{ => engine}/adapters/sources/ply_point_cloud_loader.cpp (100%) rename src/{ => engine}/adapters/sources/ply_point_cloud_loader.h (100%) rename src/{ => engine}/adapters/sources/ros2_point_cloud_source.cpp (100%) rename src/{ => engine}/adapters/sources/ros2_point_cloud_source.h (100%) rename src/{ => engine}/algorithms/preprocess/preprocess_algorithms.cpp (100%) rename src/{ => engine}/algorithms/preprocess/preprocess_algorithms.h (100%) rename src/{ => engine}/algorithms/reconstruction/surface_reconstruction.cpp (100%) rename src/{ => engine}/algorithms/reconstruction/surface_reconstruction.h (100%) rename src/{ => engine}/cli/pipeline_cli_runner.cpp (99%) rename src/{ => engine}/cli/pipeline_cli_runner.h (100%) rename src/{ => engine}/core/data_source.h (100%) rename src/{ => engine}/core/hole_aware_score.cpp (100%) rename src/{ => engine}/core/hole_aware_score.h (100%) rename src/{ => engine}/core/pipeline_config.cpp (100%) rename src/{ => engine}/core/pipeline_config.h (100%) rename src/{ => engine}/core/pipeline_config_validation.cpp (100%) rename src/{ => engine}/core/pipeline_config_validation.h (100%) rename src/{ => engine}/core/pipeline_executor.cpp (100%) rename src/{ => engine}/core/pipeline_executor.h (100%) rename src/{ => engine}/core/pipeline_plugin_registry.cpp (100%) rename src/{ => engine}/core/pipeline_plugin_registry.h (100%) rename src/{ => engine}/core/pipeline_stage.h (100%) rename src/{ => engine}/core/pipeline_stage_defaults.cpp (100%) rename src/{ => engine}/core/pipeline_stage_defaults.h (100%) rename src/{ => engine}/core/point_cloud_types.h (100%) rename src/{ => engine}/core/tuning_service.cpp (100%) rename src/{ => engine}/core/tuning_service.h (100%) rename src/{ => engine}/factories/pipeline/desktop_pipeline_factory.cpp (100%) rename src/{ => engine}/factories/pipeline/desktop_pipeline_factory.h (100%) rename src/{ => engine}/strategies/preprocess_basic_stages.cpp (100%) rename src/{ => engine}/strategies/preprocess_basic_stages.h (100%) rename src/{ => engine}/strategies/preprocess_pcl_stages.cpp (100%) rename src/{ => engine}/strategies/preprocess_pcl_stages.h (100%) rename src/{ => engine}/strategies/reconstruction_pcl_greedy_stage.h (100%) rename src/{ => engine}/strategies/reconstruction_pcl_poisson_stage.h (100%) rename src/{ => engine}/strategies/reconstruction_surface_stage.h (100%) rename src/{ => engine}/strategies/registration_icp_stage.cpp (100%) rename src/{ => engine}/strategies/registration_icp_stage.h (100%) rename src/{ => engine}/strategies/transform_tf_stage.cpp (100%) rename src/{ => engine}/strategies/transform_tf_stage.h (100%) rename src/{ => engine}/tests/pipeline_smoke_tests.cpp (100%) rename src/{ => engine}/tests/pipeline_smoke_tests.h (100%) rename src/{ => engine}/tests/test_runner_main.cpp (100%) delete mode 100644 web-vue/src/App.vue delete mode 100644 web-vue/src/api/client.js delete mode 100644 web/app.js delete mode 100644 web/index.html delete mode 100644 web/style.css diff --git a/.gitignore b/.gitignore index 2552316..13ca68d 100644 --- a/.gitignore +++ b/.gitignore @@ -41,6 +41,14 @@ bearer/ *.temp __pycache__/ *.pyc +backend/.venv/ +backend/wheels/ +data/ +frontend/web/node_modules/ +frontend/web/dist/ + +# Generated PointNet sonar dataset +sonar_dataset/ # OS/editor files .DS_Store diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..2393805 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,115 @@ +# Архитектура DotsToSurface + +Этот документ описывает **логические слои** проекта и соответствие **физических папок**. + +## Три слоя + +| Слой | Папки | Ответственность | +|------|-------|-----------------| +| **Engine** (C++) | `src/engine/` | Обработка облака точек: preprocess, PCL, реконструкция, метрики, CLI | +| **Backend** (HTTP) | `backend/` | REST API: пресеты, validate, wizard, вызов CLI, отдача статики Vue | +| **Frontend (Web)** | `frontend/web/` | Dashboard в браузере, Pinia, Three.js viewer | +| **Frontend (Desktop)** | `src/desktop/` | Qt Widgets + QML dashboard, OpenGL viewer | + +**Важно:** Python и Vue **не выполняют** пайплайн сами. Backend вызывает бинарник `DotsToSurface --cli` через `subprocess` ([`backend/main.py`](backend/main.py)). + +## Карта репозитория + +``` +DotsToSurface/ +├── src/ +│ ├── main.cpp # Entry: --cli или Qt GUI +│ ├── engine/ # Engine — C++ пайплайн +│ │ ├── core/ +│ │ ├── strategies/ +│ │ ├── adapters/ +│ │ ├── algorithms/ +│ │ ├── cli/ +│ │ ├── factories/ +│ │ └── tests/ +│ └── desktop/ # Frontend (Desktop) — Qt/QML +│ ├── mainwindow.cpp +│ ├── glview.cpp +│ └── qml/ +├── backend/ # Backend — FastAPI +├── frontend/ +│ └── web/ # Frontend (Web) — Vue 3 +├── docker/ # Infra — сборка и runtime-образ +├── presets/ # Shared — JSON-пресеты +└── DotsToSurface.pro +``` + +Артефакты сборки **не в git**: `build/`, `frontend/web/dist/`, `frontend/web/node_modules/`. + +## Потоки данных + +### Web (Docker или локально) + +```mermaid +flowchart LR + Browser[Browser Vue3] -->|HTTP /api/*| FastAPI[backend/main.py] + FastAPI -->|subprocess| CLI[DotsToSurface --cli] + CLI --> Engine[PipelineExecutor] + Engine --> JSON[result.json] + JSON --> FastAPI + FastAPI --> Browser +``` + +### Desktop (Qt) + +```mermaid +flowchart LR + QML[Qt QML Dashboard] --> MainWindow[MainWindow C++] + MainWindow --> Engine[PipelineExecutor in-process] + Engine --> GLView[OpenGL viewer] +``` + +## Что работает без C++ бинарника + +| Endpoint / функция | Без `DotsToSurface` | +|--------------------|---------------------| +| `GET /api/catalog`, `/api/presets`, `/api/builtin-presets` | Да | +| `POST /api/validate-config`, `/api/wizard` | Да | +| `GET /api/demo` | Да (точки генерирует Python) | +| `POST /api/run` | **Нет** — нужен CLI | + +## Сценарии разработки + +### Web-версия целиком (рекомендуется) + +```bash +cd docker +docker compose up --build +# → http://localhost:8080 +``` + +### Frontend отдельно + +```bash +cd frontend/web +npm install +npm run dev +# → http://localhost:5173, /api проксируется на :8080 +``` + +### Engine + Desktop локально + +```bash +qmake DotsToSurface.pro 'DEFINES+=PCL_ENABLED' +make -j$(nproc) +./DotsToSurface +``` + +## Shared и известный техдолг + +- **`presets/`** — JSON-шаблоны; читаются Qt и Backend. +- **`pipelineUiCatalog`** — дублирован в Qt и Vue: + - [`src/desktop/qml/shared/PipelineUiCatalog.js`](src/desktop/qml/shared/PipelineUiCatalog.js) + - [`frontend/web/src/catalog/pipelineUiCatalog.js`](frontend/web/src/catalog/pipelineUiCatalog.js) + +## Указатели по папкам + +- [backend/README.md](backend/README.md) +- [frontend/web/README.md](frontend/web/README.md) +- [src/README.md](src/README.md) +- [docker/README.md](docker/README.md) diff --git a/DotsToSirface.pro b/DotsToSirface.pro deleted file mode 100644 index 7973427..0000000 --- a/DotsToSirface.pro +++ /dev/null @@ -1,104 +0,0 @@ -QT += core gui widgets opengl quick quickwidgets qml - -CONFIG += c++14 -TEMPLATE = app -TARGET = DotsToSirface - -win32:LIBS += -lopengl32 -unix:LIBS += -lGL - -contains(DEFINES, PCL_ENABLED) { - message(PCL support enabled) - win32 { - isEmpty(PCL_ROOT) { - PCL_ROOT = C:/PCL - } - INCLUDEPATH += $$PCL_ROOT/include $$PCL_ROOT/include/pcl-1.12 - LIBS += -L$$PCL_ROOT/lib \ - -lpcl_common \ - -lpcl_io \ - -lpcl_filters \ - -lpcl_features \ - -lpcl_kdtree \ - -lpcl_search \ - -lpcl_surface - } - - unix { - CONFIG += link_pkgconfig - PKGCONFIG += \ - pcl_common \ - pcl_io \ - pcl_filters \ - pcl_features \ - pcl_kdtree \ - pcl_search \ - pcl_surface - } -} - -SOURCES += \ - src/main.cpp \ - src/cli/pipeline_cli_runner.cpp \ - src/algorithms/preprocess/preprocess_algorithms.cpp \ - src/algorithms/reconstruction/surface_reconstruction.cpp \ - src/adapters/reconstruction/reconstruction_adapter.cpp \ - src/adapters/pcl/pcl_point_cloud_adapter.cpp \ - src/adapters/registration/registration_adapter.cpp \ - src/adapters/ros2/pointcloud2_adapter.cpp \ - src/adapters/ros2/tf2_adapter.cpp \ - src/adapters/sources/file_point_cloud_source.cpp \ - src/adapters/sources/ply_point_cloud_loader.cpp \ - src/adapters/sources/ros2_point_cloud_source.cpp \ - src/core/pipeline_config.cpp \ - src/core/pipeline_config_validation.cpp \ - src/core/hole_aware_score.cpp \ - src/core/pipeline_stage_defaults.cpp \ - src/core/tuning_service.cpp \ - src/core/pipeline_executor.cpp \ - src/core/pipeline_plugin_registry.cpp \ - src/factories/pipeline/desktop_pipeline_factory.cpp \ - src/strategies/registration_icp_stage.cpp \ - src/strategies/preprocess_basic_stages.cpp \ - src/strategies/preprocess_pcl_stages.cpp \ - src/strategies/transform_tf_stage.cpp \ - src/tests/pipeline_smoke_tests.cpp \ - src/ui/mainwindow.cpp \ - src/ui/glview.cpp - -HEADERS += \ - src/cli/pipeline_cli_runner.h \ - src/algorithms/preprocess/preprocess_algorithms.h \ - src/algorithms/reconstruction/surface_reconstruction.h \ - src/adapters/reconstruction/reconstruction_adapter.h \ - src/adapters/pcl/pcl_point_cloud_adapter.h \ - src/adapters/registration/registration_adapter.h \ - src/adapters/ros2/pointcloud2_adapter.h \ - src/adapters/ros2/tf2_adapter.h \ - src/adapters/sources/file_point_cloud_source.h \ - src/adapters/sources/ply_point_cloud_loader.h \ - src/adapters/sources/ros2_point_cloud_source.h \ - src/core/data_source.h \ - src/core/point_cloud_types.h \ - src/core/pipeline_config.h \ - src/core/pipeline_config_validation.h \ - src/core/hole_aware_score.h \ - src/core/pipeline_stage_defaults.h \ - src/core/tuning_service.h \ - src/core/pipeline_stage.h \ - src/core/pipeline_executor.h \ - src/core/pipeline_plugin_registry.h \ - src/factories/pipeline/desktop_pipeline_factory.h \ - src/strategies/preprocess_basic_stages.h \ - src/strategies/preprocess_pcl_stages.h \ - src/strategies/registration_icp_stage.h \ - src/strategies/reconstruction_pcl_greedy_stage.h \ - src/strategies/reconstruction_pcl_poisson_stage.h \ - src/strategies/reconstruction_surface_stage.h \ - src/strategies/transform_tf_stage.h \ - src/tests/pipeline_smoke_tests.h \ - src/ui/mainwindow.h \ - src/ui/glview.h - -RESOURCES += \ - src/ui/qml/ui_qml.qrc diff --git a/DotsToSirfaceTests.pro b/DotsToSirfaceTests.pro deleted file mode 100644 index b9de0f5..0000000 --- a/DotsToSirfaceTests.pro +++ /dev/null @@ -1,92 +0,0 @@ -QT += core - -CONFIG += console c++14 -CONFIG -= app_bundle -TEMPLATE = app -TARGET = DotsToSirfaceTests - -contains(DEFINES, PCL_ENABLED) { - message(PCL support enabled) - win32 { - isEmpty(PCL_ROOT) { - PCL_ROOT = C:/PCL - } - INCLUDEPATH += $$PCL_ROOT/include $$PCL_ROOT/include/pcl-1.12 - LIBS += -L$$PCL_ROOT/lib \ - -lpcl_common \ - -lpcl_io \ - -lpcl_filters \ - -lpcl_features \ - -lpcl_kdtree \ - -lpcl_search \ - -lpcl_surface - } - - unix { - CONFIG += link_pkgconfig - PKGCONFIG += \ - pcl_common \ - pcl_io \ - pcl_filters \ - pcl_features \ - pcl_kdtree \ - pcl_search \ - pcl_surface - } -} - -SOURCES += \ - src/tests/test_runner_main.cpp \ - src/tests/pipeline_smoke_tests.cpp \ - src/algorithms/preprocess/preprocess_algorithms.cpp \ - src/algorithms/reconstruction/surface_reconstruction.cpp \ - src/adapters/reconstruction/reconstruction_adapter.cpp \ - src/adapters/pcl/pcl_point_cloud_adapter.cpp \ - src/adapters/registration/registration_adapter.cpp \ - src/adapters/ros2/pointcloud2_adapter.cpp \ - src/adapters/ros2/tf2_adapter.cpp \ - src/adapters/sources/file_point_cloud_source.cpp \ - src/adapters/sources/ply_point_cloud_loader.cpp \ - src/adapters/sources/ros2_point_cloud_source.cpp \ - src/core/pipeline_config.cpp \ - src/core/pipeline_config_validation.cpp \ - src/core/hole_aware_score.cpp \ - src/core/pipeline_stage_defaults.cpp \ - src/core/tuning_service.cpp \ - src/core/pipeline_executor.cpp \ - src/core/pipeline_plugin_registry.cpp \ - src/factories/pipeline/desktop_pipeline_factory.cpp \ - src/strategies/preprocess_basic_stages.cpp \ - src/strategies/preprocess_pcl_stages.cpp \ - src/strategies/registration_icp_stage.cpp \ - src/strategies/transform_tf_stage.cpp - -HEADERS += \ - src/tests/pipeline_smoke_tests.h \ - src/algorithms/preprocess/preprocess_algorithms.h \ - src/algorithms/reconstruction/surface_reconstruction.h \ - src/adapters/reconstruction/reconstruction_adapter.h \ - src/adapters/pcl/pcl_point_cloud_adapter.h \ - src/adapters/registration/registration_adapter.h \ - src/adapters/ros2/pointcloud2_adapter.h \ - src/adapters/ros2/tf2_adapter.h \ - src/adapters/sources/file_point_cloud_source.h \ - src/adapters/sources/ply_point_cloud_loader.h \ - src/adapters/sources/ros2_point_cloud_source.h \ - src/core/data_source.h \ - src/core/point_cloud_types.h \ - src/core/pipeline_config.h \ - src/core/pipeline_config_validation.h \ - src/core/hole_aware_score.h \ - src/core/pipeline_stage_defaults.h \ - src/core/tuning_service.h \ - src/core/pipeline_stage.h \ - src/core/pipeline_executor.h \ - src/core/pipeline_plugin_registry.h \ - src/factories/pipeline/desktop_pipeline_factory.h \ - src/strategies/preprocess_basic_stages.h \ - src/strategies/preprocess_pcl_stages.h \ - src/strategies/reconstruction_surface_stage.h \ - src/strategies/reconstruction_pcl_greedy_stage.h \ - src/strategies/registration_icp_stage.h \ - src/strategies/transform_tf_stage.h diff --git a/DotsToSurface.pro b/DotsToSurface.pro new file mode 100644 index 0000000..8713543 --- /dev/null +++ b/DotsToSurface.pro @@ -0,0 +1,104 @@ +QT += core gui widgets opengl quick quickwidgets qml + +CONFIG += c++14 +TEMPLATE = app +TARGET = DotsToSurface + +win32:LIBS += -lopengl32 +unix:LIBS += -lGL + +contains(DEFINES, PCL_ENABLED) { + message(PCL support enabled) + win32 { + isEmpty(PCL_ROOT) { + PCL_ROOT = C:/PCL + } + INCLUDEPATH += $$PCL_ROOT/include $$PCL_ROOT/include/pcl-1.12 + LIBS += -L$$PCL_ROOT/lib \ + -lpcl_common \ + -lpcl_io \ + -lpcl_filters \ + -lpcl_features \ + -lpcl_kdtree \ + -lpcl_search \ + -lpcl_surface + } + + unix { + CONFIG += link_pkgconfig + PKGCONFIG += \ + pcl_common \ + pcl_io \ + pcl_filters \ + pcl_features \ + pcl_kdtree \ + pcl_search \ + pcl_surface + } +} + +SOURCES += \ + src/main.cpp \ + src/engine/cli/pipeline_cli_runner.cpp \ + src/engine/algorithms/preprocess/preprocess_algorithms.cpp \ + src/engine/algorithms/reconstruction/surface_reconstruction.cpp \ + src/engine/adapters/reconstruction/reconstruction_adapter.cpp \ + src/engine/adapters/pcl/pcl_point_cloud_adapter.cpp \ + src/engine/adapters/registration/registration_adapter.cpp \ + src/engine/adapters/ros2/pointcloud2_adapter.cpp \ + src/engine/adapters/ros2/tf2_adapter.cpp \ + src/engine/adapters/sources/file_point_cloud_source.cpp \ + src/engine/adapters/sources/ply_point_cloud_loader.cpp \ + src/engine/adapters/sources/ros2_point_cloud_source.cpp \ + src/engine/core/pipeline_config.cpp \ + src/engine/core/pipeline_config_validation.cpp \ + src/engine/core/hole_aware_score.cpp \ + src/engine/core/pipeline_stage_defaults.cpp \ + src/engine/core/tuning_service.cpp \ + src/engine/core/pipeline_executor.cpp \ + src/engine/core/pipeline_plugin_registry.cpp \ + src/engine/factories/pipeline/desktop_pipeline_factory.cpp \ + src/engine/strategies/registration_icp_stage.cpp \ + src/engine/strategies/preprocess_basic_stages.cpp \ + src/engine/strategies/preprocess_pcl_stages.cpp \ + src/engine/strategies/transform_tf_stage.cpp \ + src/engine/tests/pipeline_smoke_tests.cpp \ + src/desktop/mainwindow.cpp \ + src/desktop/glview.cpp + +HEADERS += \ + src/engine/cli/pipeline_cli_runner.h \ + src/engine/algorithms/preprocess/preprocess_algorithms.h \ + src/engine/algorithms/reconstruction/surface_reconstruction.h \ + src/engine/adapters/reconstruction/reconstruction_adapter.h \ + src/engine/adapters/pcl/pcl_point_cloud_adapter.h \ + src/engine/adapters/registration/registration_adapter.h \ + src/engine/adapters/ros2/pointcloud2_adapter.h \ + src/engine/adapters/ros2/tf2_adapter.h \ + src/engine/adapters/sources/file_point_cloud_source.h \ + src/engine/adapters/sources/ply_point_cloud_loader.h \ + src/engine/adapters/sources/ros2_point_cloud_source.h \ + src/engine/core/data_source.h \ + src/engine/core/point_cloud_types.h \ + src/engine/core/pipeline_config.h \ + src/engine/core/pipeline_config_validation.h \ + src/engine/core/hole_aware_score.h \ + src/engine/core/pipeline_stage_defaults.h \ + src/engine/core/tuning_service.h \ + src/engine/core/pipeline_stage.h \ + src/engine/core/pipeline_executor.h \ + src/engine/core/pipeline_plugin_registry.h \ + src/engine/factories/pipeline/desktop_pipeline_factory.h \ + src/engine/strategies/preprocess_basic_stages.h \ + src/engine/strategies/preprocess_pcl_stages.h \ + src/engine/strategies/registration_icp_stage.h \ + src/engine/strategies/reconstruction_pcl_greedy_stage.h \ + src/engine/strategies/reconstruction_pcl_poisson_stage.h \ + src/engine/strategies/reconstruction_surface_stage.h \ + src/engine/strategies/transform_tf_stage.h \ + src/engine/tests/pipeline_smoke_tests.h \ + src/desktop/mainwindow.h \ + src/desktop/glview.h + +RESOURCES += \ + src/desktop/qml/ui_qml.qrc diff --git a/DotsToSurfaceTests.pro b/DotsToSurfaceTests.pro new file mode 100644 index 0000000..c5d946c --- /dev/null +++ b/DotsToSurfaceTests.pro @@ -0,0 +1,92 @@ +QT += core + +CONFIG += console c++14 +CONFIG -= app_bundle +TEMPLATE = app +TARGET = DotsToSurfaceTests + +contains(DEFINES, PCL_ENABLED) { + message(PCL support enabled) + win32 { + isEmpty(PCL_ROOT) { + PCL_ROOT = C:/PCL + } + INCLUDEPATH += $$PCL_ROOT/include $$PCL_ROOT/include/pcl-1.12 + LIBS += -L$$PCL_ROOT/lib \ + -lpcl_common \ + -lpcl_io \ + -lpcl_filters \ + -lpcl_features \ + -lpcl_kdtree \ + -lpcl_search \ + -lpcl_surface + } + + unix { + CONFIG += link_pkgconfig + PKGCONFIG += \ + pcl_common \ + pcl_io \ + pcl_filters \ + pcl_features \ + pcl_kdtree \ + pcl_search \ + pcl_surface + } +} + +SOURCES += \ + src/engine/tests/test_runner_main.cpp \ + src/engine/tests/pipeline_smoke_tests.cpp \ + src/engine/algorithms/preprocess/preprocess_algorithms.cpp \ + src/engine/algorithms/reconstruction/surface_reconstruction.cpp \ + src/engine/adapters/reconstruction/reconstruction_adapter.cpp \ + src/engine/adapters/pcl/pcl_point_cloud_adapter.cpp \ + src/engine/adapters/registration/registration_adapter.cpp \ + src/engine/adapters/ros2/pointcloud2_adapter.cpp \ + src/engine/adapters/ros2/tf2_adapter.cpp \ + src/engine/adapters/sources/file_point_cloud_source.cpp \ + src/engine/adapters/sources/ply_point_cloud_loader.cpp \ + src/engine/adapters/sources/ros2_point_cloud_source.cpp \ + src/engine/core/pipeline_config.cpp \ + src/engine/core/pipeline_config_validation.cpp \ + src/engine/core/hole_aware_score.cpp \ + src/engine/core/pipeline_stage_defaults.cpp \ + src/engine/core/tuning_service.cpp \ + src/engine/core/pipeline_executor.cpp \ + src/engine/core/pipeline_plugin_registry.cpp \ + src/engine/factories/pipeline/desktop_pipeline_factory.cpp \ + src/engine/strategies/preprocess_basic_stages.cpp \ + src/engine/strategies/preprocess_pcl_stages.cpp \ + src/engine/strategies/registration_icp_stage.cpp \ + src/engine/strategies/transform_tf_stage.cpp + +HEADERS += \ + src/engine/tests/pipeline_smoke_tests.h \ + src/engine/algorithms/preprocess/preprocess_algorithms.h \ + src/engine/algorithms/reconstruction/surface_reconstruction.h \ + src/engine/adapters/reconstruction/reconstruction_adapter.h \ + src/engine/adapters/pcl/pcl_point_cloud_adapter.h \ + src/engine/adapters/registration/registration_adapter.h \ + src/engine/adapters/ros2/pointcloud2_adapter.h \ + src/engine/adapters/ros2/tf2_adapter.h \ + src/engine/adapters/sources/file_point_cloud_source.h \ + src/engine/adapters/sources/ply_point_cloud_loader.h \ + src/engine/adapters/sources/ros2_point_cloud_source.h \ + src/engine/core/data_source.h \ + src/engine/core/point_cloud_types.h \ + src/engine/core/pipeline_config.h \ + src/engine/core/pipeline_config_validation.h \ + src/engine/core/hole_aware_score.h \ + src/engine/core/pipeline_stage_defaults.h \ + src/engine/core/tuning_service.h \ + src/engine/core/pipeline_stage.h \ + src/engine/core/pipeline_executor.h \ + src/engine/core/pipeline_plugin_registry.h \ + src/engine/factories/pipeline/desktop_pipeline_factory.h \ + src/engine/strategies/preprocess_basic_stages.h \ + src/engine/strategies/preprocess_pcl_stages.h \ + src/engine/strategies/reconstruction_surface_stage.h \ + src/engine/strategies/reconstruction_pcl_greedy_stage.h \ + src/engine/strategies/registration_icp_stage.h \ + src/engine/strategies/transform_tf_stage.h diff --git a/README.md b/README.md index f6e04e8..7f3d053 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,19 @@ -# DotsToSirface (Qt 5.11) +# DotsToSurface (Qt 5.11) + +## Структура репозитория + +| Слой | Папка | Назначение | +|------|-------|------------| +| **Engine** | `src/engine/` | C++ пайплайн, PCL, CLI `--cli` | +| **Backend** | `backend/` | FastAPI — HTTP-обёртка над CLI | +| **Frontend (Web)** | `frontend/web/` | Vue 3 dashboard + Three.js | +| **Frontend (Desktop)** | `src/desktop/` | Qt QML dashboard | + +Подробнее: [ARCHITECTURE.md](ARCHITECTURE.md) + +Сборка Web-версии полностью воспроизводится в Docker (`docker compose up --build`). В git — исходники; артефакты (`build/`, `frontend/web/dist/`) не коммитятся. + +--- Демонстрационная программа на Qt 5.11 C++, которая: - принимает массив 3D-точек `QVector`; @@ -7,7 +22,7 @@ ## Вход/выход API -`src/algorithms/reconstruction/surface_reconstruction.h`: +`src/engine/algorithms/reconstruction/surface_reconstruction.h`: - `struct Point3f { float x, y, z; };` - `struct Triangle { int i0, i1, i2; };` @@ -38,7 +53,7 @@ ## Этап 1 (backend-first, без CLI) - Парсинг и применение `defaults` параметров стадий вынесены из UI в `core`: - `src/core/pipeline_stage_defaults.h` и `src/core/pipeline_stage_defaults.cpp`. + `src/engine/core/pipeline_stage_defaults.h` и `src/engine/core/pipeline_stage_defaults.cpp`. - `MainWindow` больше не содержит backend-логику разбора параметров, а вызывает `core::applyStageDefaultsToConfig(...)`. - Это позволяет использовать один и тот же backend API из GUI сейчас и из будущего @@ -78,10 +93,10 @@ Рабочий поток для этого проекта на Windows: собирать и запускать через WSL. ```bash -cd /mnt/d/yakupov/Projects/DotsToSirface/build-wsl -qmake ../DotsToSirface.pro 'DEFINES+=PCL_ENABLED' +cd /mnt/d/yakupov/Projects/DotsToSurface/build-wsl +qmake ../DotsToSurface.pro 'DEFINES+=PCL_ENABLED' make -j4 -LIBGL_ALWAYS_SOFTWARE=1 QT_QPA_PLATFORM=xcb ./DotsToSirface +LIBGL_ALWAYS_SOFTWARE=1 QT_QPA_PLATFORM=xcb ./DotsToSurface ``` Примечания: @@ -92,22 +107,22 @@ LIBGL_ALWAYS_SOFTWARE=1 QT_QPA_PLATFORM=xcb ./DotsToSirface Перезапуск после правок: ```bash -pkill -f '^./DotsToSirface$' || true -cd /mnt/d/yakupov/Projects/DotsToSirface/build-wsl -qmake ../DotsToSirface.pro 'DEFINES+=PCL_ENABLED' +pkill -f '^./DotsToSurface$' || true +cd /mnt/d/yakupov/Projects/DotsToSurface/build-wsl +qmake ../DotsToSurface.pro 'DEFINES+=PCL_ENABLED' make -j4 -LIBGL_ALWAYS_SOFTWARE=1 QT_QPA_PLATFORM=xcb ./DotsToSirface +LIBGL_ALWAYS_SOFTWARE=1 QT_QPA_PLATFORM=xcb ./DotsToSurface ``` ## Smoke tests (отдельный runner) Для быстрой проверки пайплайна без GUI добавлен отдельный консольный таргет: -`DotsToSirfaceTests.pro`. +`DotsToSurfaceTests.pro`. ```bash -qmake DotsToSirfaceTests.pro +qmake DotsToSurfaceTests.pro make -./release/DotsToSirfaceTests.exe +./release/DotsToSurfaceTests.exe ``` При успешном запуске runner печатает `Smoke tests passed.` и завершаетcя с кодом `0`. @@ -119,10 +134,10 @@ make Пример: ```bash -cd /mnt/d/yakupov/Projects/DotsToSirface/build-wsl -qmake ../DotsToSirface.pro 'DEFINES+=PCL_ENABLED' +cd /mnt/d/yakupov/Projects/DotsToSurface/build-wsl +qmake ../DotsToSurface.pro 'DEFINES+=PCL_ENABLED' make -j4 -./DotsToSirface --cli \ +./DotsToSurface --cli \ --input /mnt/d/path/to/cloud.xyz \ --profile desktop_debug \ --preprocess pcl_remove_nan,pcl_voxel_grid,pcl_statistical_outlier \ @@ -155,7 +170,7 @@ CLI печатает краткую сводку по метрикам (`input`, Запуск: ```bash -./DotsToSirface --cli --input /mnt/d/path/to/cloud.xyz --config-json /mnt/d/path/to/pipeline_config.json +./DotsToSurface --cli --input /mnt/d/path/to/cloud.xyz --config-json /mnt/d/path/to/pipeline_config.json ``` Если одновременно переданы `--config-json` и обычные CLI-флаги (`--preprocess`, `--reconstruction`, `--stage-default`), флаги командной строки имеют приоритет. @@ -193,7 +208,7 @@ CLI печатает краткую сводку по метрикам (`input`, Запуск: ```bash -./DotsToSirface --cli \ +./DotsToSurface --cli \ --input /mnt/d/path/to/cloud.xyz \ --autotune-config /mnt/d/path/to/autotune_config.json ``` @@ -208,13 +223,12 @@ CLI печатает краткую сводку по метрикам (`input`, Для запуска пайплайна с PCL в изолированной среде (без конфликтов с `libpq` на хосте) добавлены: -- `docker/Dockerfile` — сборка `DotsToSirface` с `PCL_ENABLED` + Vue 3 dashboard; +- `docker/Dockerfile` — сборка `DotsToSurface` с `PCL_ENABLED` + Vue 3 dashboard; - `docker/docker-compose.yml`; -- `api/main.py` — HTTP API (`/api/run`, `/api/presets`, `/api/validate-config`, …); -- `web-vue/` — Vue 3 + Pinia + Three.js (полный dashboard, паритет с Qt); -- `web/` — legacy MVP (fallback, если Vue `dist` не собран). +- `backend/main.py` — HTTP API (`/api/run`, `/api/presets`, `/api/validate-config`, …); +- `frontend/web/` — Vue 3 + Pinia + Three.js (полный dashboard, паритет с Qt); -**Qt desktop (`./DotsToSirface`) сохраняется** как локальный fallback без Docker. +**Qt desktop (`./DotsToSurface`) сохраняется** как локальный fallback без Docker. ### Быстрый старт @@ -228,7 +242,7 @@ docker compose up --build ### Локальная разработка frontend ```bash -cd web-vue +cd frontend/web npm install npm run dev ``` diff --git a/assets/airplane_reference.png b/assets/airplane_reference.png new file mode 100644 index 0000000000000000000000000000000000000000..19b357417a2f61f9d244d877848db82fa1d070e9 GIT binary patch literal 105091 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 tuple[int, int, str]: + """Map 0-based global index → (area, scene_number_1based, stem). + + Scene numbers restart at 0001 within each Area. + """ + if index < 0: + raise ValueError("scene index must be >= 0") + remaining = index + for area, count in AREA_LAYOUT: + if remaining < count: + scene_no = remaining + 1 + stem = f"Area_{area}_scene_{scene_no:04d}" + return area, scene_no, stem + remaining -= count + scene_no = AREA_LAYOUT[-1][1] + remaining + 1 + stem = f"Area_6_scene_{scene_no:04d}" + return 6, scene_no, stem + + +# --------------------------------------------------------------------------- +# Target object from user .obj +# --------------------------------------------------------------------------- + +def normalize_object_points(points: list[list[float]]) -> list[list[float]]: + xs = [p[0] for p in points] + ys = [p[1] for p in points] + zs = [p[2] for p in points] + cx = (min(xs) + max(xs)) * 0.5 + cy = (min(ys) + max(ys)) * 0.5 + cz = (min(zs) + max(zs)) * 0.5 + span = max(max(xs) - min(xs), max(ys) - min(ys), max(zs) - min(zs), 1e-6) + scale = 1.0 / span + return [[(p[0] - cx) * scale, (p[1] - cy) * scale, (p[2] - cz) * scale] for p in points] + + +def load_object_points_from_obj_text(text: str) -> list[list[float]]: + """Parse OBJ vertices and normalize to unit local frame (centered, max span ≈ 1).""" + points = parse_obj_points(text) + if len(points) < 3: + raise ValueError("OBJ model must contain at least 3 vertices.") + return normalize_object_points(points) + + +def resample_object_points( + template: list[list[float]], + count: int, + *, + noise: float = 0.0, + seed: int = 1, +) -> list[list[float]]: + """Subsample (or sample with replacement) template points to the requested count.""" + if not template: + raise ValueError("Object template is empty.") + rng = random.Random(int(seed)) + count = max(1, int(count)) + out: list[list[float]] = [] + n = len(template) + for _ in range(count): + src = template[rng.randrange(n)] + if noise > 0: + out.append( + [ + src[0] + rng.uniform(-noise, noise), + src[1] + rng.uniform(-noise, noise), + src[2] + rng.uniform(-noise, noise), + ] + ) + else: + out.append([src[0], src[1], src[2]]) + return out + + +def object_half_extent_z(points: list[list[float]]) -> float: + if not points: + return 0.35 + zs = [p[2] for p in points] + return max(0.05, (max(zs) - min(zs)) * 0.5) + + +# --------------------------------------------------------------------------- +# Seafloor / clutter +# --------------------------------------------------------------------------- + +def _seafloor_height( + x: float, + y: float, + *, + base_z: float, + amplitude: float, + frequency: float, + hills: list[tuple[float, float, float, float]], + valleys: list[tuple[float, float, float, float]], + bumps: list[tuple[float, float, float, float]], +) -> float: + z = base_z + z += amplitude * math.sin(frequency * x) * math.cos(frequency * 0.7 * y) + z += 0.35 * amplitude * math.sin(frequency * 1.7 * y + 0.4) + for cx, cy, height, radius in hills: + d2 = (x - cx) ** 2 + (y - cy) ** 2 + if d2 < radius * radius * 4: + z += height * math.exp(-d2 / max(radius * radius, 1e-6)) + for cx, cy, depth, radius in valleys: + d2 = (x - cx) ** 2 + (y - cy) ** 2 + if d2 < radius * radius * 4: + z -= depth * math.exp(-d2 / max(radius * radius, 1e-6)) + for cx, cy, height, radius in bumps: + d2 = (x - cx) ** 2 + (y - cy) ** 2 + if d2 < radius * radius * 4: + z += height * math.exp(-d2 / max(radius * radius * 0.5, 1e-6)) + return z + + +def _generate_seafloor( + rng: random.Random, + *, + beam_count: int = 45, + length_count: int | None = None, +) -> tuple[list[list[float]], dict[str, Any]]: + """Sample seafloor as a square relief grid. + + beam_count controls width resolution (X axis). + length_count controls length resolution (Y axis). + """ + beams = max(1, int(beam_count)) + length_points = beams if length_count is None else max(1, int(length_count)) + size_x = rng.uniform(8.0, 16.0) + size_y = rng.uniform(8.0, 16.0) + base_z = rng.uniform(-1.2, -0.2) + amplitude = rng.uniform(0.05, 0.35) + frequency = rng.uniform(0.4, 2.2) + noise = rng.uniform(0.005, 0.04) + + hills = [ + ( + rng.uniform(-size_x * 0.4, size_x * 0.4), + rng.uniform(-size_y * 0.4, size_y * 0.4), + rng.uniform(0.15, 0.7), + rng.uniform(0.6, 2.2), + ) + for _ in range(rng.randint(1, 4)) + ] + valleys = [ + ( + rng.uniform(-size_x * 0.4, size_x * 0.4), + rng.uniform(-size_y * 0.4, size_y * 0.4), + rng.uniform(0.1, 0.55), + rng.uniform(0.5, 2.0), + ) + for _ in range(rng.randint(1, 3)) + ] + bumps = [ + ( + rng.uniform(-size_x * 0.45, size_x * 0.45), + rng.uniform(-size_y * 0.45, size_y * 0.45), + rng.uniform(0.03, 0.18), + rng.uniform(0.15, 0.55), + ) + for _ in range(rng.randint(3, 12)) + ] + + meta = { + "sizeX": size_x, + "sizeY": size_y, + "baseZ": base_z, + "amplitude": amplitude, + "frequency": frequency, + "hills": hills, + "valleys": valleys, + "bumps": bumps, + "beamCount": beams, + "lengthCount": length_points, + "gridWidthPoints": beams, + "gridLengthPoints": length_points, + } + + half_x = size_x * 0.5 + half_y = size_y * 0.5 + points: list[list[float]] = [] + for yi in range(length_points): + y = -half_y if length_points == 1 else (-half_y + size_y * yi / (length_points - 1)) + for xi in range(beams): + x = -half_x if beams == 1 else (-half_x + size_x * xi / (beams - 1)) + x += rng.uniform(-noise * 2, noise * 2) + yj = y + rng.uniform(-noise * 2, noise * 2) + z = _seafloor_height( + x, + yj, + base_z=base_z, + amplitude=amplitude, + frequency=frequency, + hills=hills, + valleys=valleys, + bumps=bumps, + ) + z += rng.uniform(-noise, noise) + points.append([x, yj, z]) + + # Local noise clusters (false sonar clutter blobs) + for _ in range(rng.randint(1, 5)): + cx = rng.uniform(-half_x * 0.8, half_x * 0.8) + cy = rng.uniform(-half_y * 0.8, half_y * 0.8) + cz = _seafloor_height( + cx, + cy, + base_z=base_z, + amplitude=amplitude, + frequency=frequency, + hills=hills, + valleys=valleys, + bumps=bumps, + ) + rng.uniform(0.0, 0.25) + n_blob = rng.randint(40, 280) + spread = rng.uniform(0.15, 0.7) + for _ in range(n_blob): + points.append( + [ + cx + rng.gauss(0, spread), + cy + rng.gauss(0, spread), + cz + rng.gauss(0, spread * 0.35), + ] + ) + + meta["pingCount"] = length_points + meta["swathBeams"] = beams + return points, meta + + +def _height_at(x: float, y: float, meta: dict[str, Any]) -> float: + return _seafloor_height( + x, + y, + base_z=float(meta["baseZ"]), + amplitude=float(meta["amplitude"]), + frequency=float(meta["frequency"]), + hills=meta["hills"], + valleys=meta["valleys"], + bumps=meta["bumps"], + ) + + +def _generate_false_objects(rng: random.Random, meta: dict[str, Any]) -> list[list[float]]: + n_objects = rng.randint(0, 6) + points: list[list[float]] = [] + half_x = float(meta["sizeX"]) * 0.5 + half_y = float(meta["sizeY"]) * 0.5 + + for i in range(n_objects): + kind = rng.choice(["sphere", "box", "torus", "pipe"]) + count = rng.randint(80, 900) + noise = rng.uniform(0.005, 0.03) + seed = rng.randint(0, 10_000_000) + if kind == "sphere": + local = generate_sphere( + {"radius": rng.uniform(0.08, 0.55), "count": count, "noise": noise, "seed": seed} + ) + elif kind == "box": + local = generate_box( + { + "sizeX": rng.uniform(0.15, 1.2), + "sizeY": rng.uniform(0.15, 1.0), + "sizeZ": rng.uniform(0.08, 0.6), + "count": count, + "noise": noise, + "seed": seed, + } + ) + elif kind == "torus": + major = rng.uniform(0.15, 0.6) + local = generate_torus( + { + "majorR": major, + "minorR": rng.uniform(0.03, major * 0.4), + "count": count, + "noise": noise, + "seed": seed, + } + ) + else: + local = generate_pipe( + { + "length": rng.uniform(0.4, 2.5), + "radius": rng.uniform(0.04, 0.2), + "axis": rng.choice(["x", "y", "z"]), + "count": count, + "noise": noise, + "seed": seed, + } + ) + + tx = rng.uniform(-half_x * 0.75, half_x * 0.75) + ty = rng.uniform(-half_y * 0.75, half_y * 0.75) + floor_z = _height_at(tx, ty, meta) + # Rest on / slightly into seafloor + tz = floor_z + rng.uniform(-0.05, 0.35) + transform = { + "x": tx, + "y": ty, + "z": tz, + "rx": rng.uniform(-0.4, 0.4), + "ry": rng.uniform(-0.4, 0.4), + "rz": rng.uniform(0, 2 * math.pi), + } + world = apply_transform(local, transform) + # Drop points buried deep under seafloor + for p in world: + if p[2] >= _height_at(p[0], p[1], meta) - 0.02: + points.append(p) + return points + + + + +# --------------------------------------------------------------------------- +# Balance plan + single scene +# --------------------------------------------------------------------------- + +def plan_scene_labels(count: int, seed: int) -> list[str]: + """Return visibility label per scene: absent | nearly_hidden | partial | visible. + + ~50% absent; among present scenes, roughly equal nearly_hidden/partial/visible. + """ + count = max(0, int(count)) + rng = random.Random(int(seed) ^ 0xA5A5_5A5A) + n_with = (count + 1) // 2 # ceil → ~50% with object + n_without = count - n_with + labels: list[str] = ["absent"] * n_without + for i in range(n_with): + labels.append(VISIBILITY_TIERS[i % 3]) + rng.shuffle(labels) + return labels + + +def _place_object( + rng: random.Random, + meta: dict[str, Any], + visibility: str, + object_template: list[list[float]], + object_scale: float = 1.0, +) -> tuple[list[list[float]], dict[str, Any]]: + """Sample, transform, and bury target object; return surviving world points + info.""" + base_scale = max(0.01, float(object_scale)) + if visibility == "nearly_hidden": + count = rng.randint(80, 600) + burial = rng.uniform(0.35, 0.75) + scale = base_scale * rng.uniform(0.7, 1.15) + elif visibility == "partial": + count = rng.randint(400, 2500) + burial = rng.uniform(0.12, 0.4) + scale = base_scale * rng.uniform(0.8, 1.3) + else: # visible + count = rng.randint(1500, 8000) + burial = rng.uniform(-0.05, 0.15) + scale = base_scale * rng.uniform(0.85, 1.4) + + noise = rng.uniform(0.004, 0.025) + local = resample_object_points( + object_template, + count, + noise=noise, + seed=rng.randint(0, 10_000_000), + ) + # Apply world scale to unit-normalized template + local = [[p[0] * scale, p[1] * scale, p[2] * scale] for p in local] + + half_x = float(meta["sizeX"]) * 0.35 + half_y = float(meta["sizeY"]) * 0.35 + tx = rng.uniform(-half_x, half_x) + ty = rng.uniform(-half_y, half_y) + floor_z = _height_at(tx, ty, meta) + + half_h = object_half_extent_z(local) + tz = floor_z + half_h * (1.0 - 2.0 * burial) + + transform = { + "x": tx, + "y": ty, + "z": tz, + "rx": rng.uniform(-0.25, 0.25), + "ry": rng.uniform(-0.2, 0.2), + "rz": rng.uniform(0, 2 * math.pi), + } + world = apply_transform(local, transform) + + kept: list[list[float]] = [] + for p in world: + surface = _height_at(p[0], p[1], meta) + eps = 0.01 if visibility != "nearly_hidden" else -0.02 + if p[2] >= surface + eps: + kept.append(p) + + if visibility == "nearly_hidden" and len(kept) < 15 and world: + ranked = sorted(world, key=lambda p: p[2] - _height_at(p[0], p[1], meta), reverse=True) + kept = ranked[: max(15, min(40, len(ranked) // 8))] + + info = { + "visibility": visibility, + "transform": transform, + "requestedCount": count, + "keptCount": len(kept), + "scale": scale, + "objectScale": base_scale, + "burial": burial, + "classLabel": "object", + "classId": 1, + } + return kept, info + + +def generate_sonar_scene( + *, + seed: int, + visibility: str = "absent", + object_points: list[list[float]] | None = None, + object_scale: float = 1.0, + beam_count: int = 45, + length_count: int | None = None, +) -> dict[str, Any]: + """Build one unique sonar scene. visibility in absent|nearly_hidden|partial|visible.""" + rng = random.Random(int(seed)) + if visibility not in ("absent",) + VISIBILITY_TIERS: + raise ValueError(f"Unknown visibility: {visibility}") + if visibility != "absent" and not object_points: + raise ValueError("object_points required when visibility is not absent.") + + floor_pts, meta = _generate_seafloor(rng, beam_count=beam_count, length_count=length_count) + clutter = _generate_false_objects(rng, meta) + + jitter = rng.uniform(0.0, 0.015) + background = floor_pts + clutter + if jitter > 0: + background = [ + [ + p[0] + rng.uniform(-jitter, jitter), + p[1] + rng.uniform(-jitter, jitter), + p[2] + rng.uniform(-jitter, jitter), + ] + for p in background + ] + + drop = rng.uniform(0.0, 0.12) + if drop > 0: + background = [p for p in background if rng.random() >= drop] + + object_pts: list[list[float]] = [] + object_info: dict[str, Any] | None = None + if visibility != "absent": + object_pts, object_info = _place_object( + rng, + meta, + visibility, + object_points, + object_scale=object_scale, + ) + + # class 0 = background, class 1 = object + rows = points_to_pointnet_rows(background, 0.0) + rows.extend(points_to_pointnet_rows(object_pts, 1.0)) + rng.shuffle(rows) + + xyz = [[r[0], r[1], r[2]] for r in rows] + return { + "seed": int(seed), + "visibility": visibility, + "hasObject": visibility != "absent", + "object": object_info, + "pointCount": len(rows), + "objectPointCount": len(object_pts), + "backgroundPointCount": len(background), + "rows": rows, + "points": xyz, + "meta": { + "sizeX": meta["sizeX"], + "sizeY": meta["sizeY"], + "beamCount": meta.get("beamCount", beam_count), + "lengthCount": meta.get("lengthCount", length_count if length_count is not None else beam_count), + "gridWidthPoints": meta.get("gridWidthPoints", beam_count), + "gridLengthPoints": meta.get( + "gridLengthPoints", + length_count if length_count is not None else beam_count, + ), + "pingCount": meta.get("pingCount"), + "swathBeams": meta.get("swathBeams"), + "floorFeatures": { + "hills": len(meta["hills"]), + "valleys": len(meta["valleys"]), + "bumps": len(meta["bumps"]), + }, + }, + } + + +# --------------------------------------------------------------------------- +# Batch write / preview load +# --------------------------------------------------------------------------- + +def resolve_output_dir(output_dir: str | Path = "sonar_dataset") -> Path: + out = Path(output_dir) + if not out.is_absolute(): + project_root = Path(__file__).resolve().parent.parent + out = project_root / out + return out + + +def _downsample_points(points: list[list[float]], max_points: int) -> list[list[float]]: + max_points = max(100, int(max_points)) + if len(points) <= max_points: + return points + step = max(1, len(points) // max_points) + return points[::step][:max_points] + + +def load_npy_float64_rows(path: Path) -> list[list[float]]: + """Read float64 little-endian .npy array written by export_npy_float64.""" + import re + import struct + + data = path.read_bytes() + if data[:6] != b"\x93NUMPY": + raise ValueError(f"Not a NumPy .npy file: {path.name}") + major = data[6] + if major == 1: + hlen = struct.unpack_from(" dict[str, int]: + """Count classes from rows shaped [x, y, z, class].""" + counts: dict[str, int] = {} + for row in points: + key = str(int(round(float(row[3] if len(row) > 3 else 0)))) + counts[key] = counts.get(key, 0) + 1 + return counts + + +def _class_counts(rows: list[list[float]]) -> dict[str, int]: + counts: dict[str, int] = {} + for row in rows: + key = str(int(round(float(row[6] if len(row) > 6 else 0)))) + counts[key] = counts.get(key, 0) + 1 + return counts + + +def load_scene_preview( + *, + stem: str, + output_dir: str | Path = "sonar_dataset", + max_points: int = 25000, +) -> dict[str, Any]: + """Load labeled points for a written scene (prefer .npy) for the 3D viewer. + + Each preview point is [x, y, z, class]. + """ + safe = "".join(ch if ch.isalnum() or ch in "_-" else "" for ch in (stem or "")) + if not safe or safe != stem: + raise ValueError("Invalid scene stem.") + out = resolve_output_dir(output_dir) + npy_path = out / f"{safe}.npy" + obj_path = out / f"{safe}.obj" + + labeled: list[list[float]] + if npy_path.is_file(): + rows = load_npy_float64_rows(npy_path) + labeled = [[float(r[0]), float(r[1]), float(r[2]), float(r[6])] for r in rows] + elif obj_path.is_file(): + text = obj_path.read_text(encoding="utf-8", errors="ignore") + points = parse_obj_points(text) + labeled = [[p[0], p[1], p[2], 0.0] for p in points] + else: + raise FileNotFoundError(f"Scene not found: {safe}.npy / {safe}.obj") + + full_counts = _class_counts_from_labeled(labeled) + preview = _downsample_points(labeled, max_points) + return { + "stem": safe, + "outputDir": str(out), + "pointCount": len(labeled), + "previewCount": len(preview), + "points": preview, + "classCounts": full_counts, + "classLabels": {"0": "background", "1": "object"}, + "obj": str(obj_path) if obj_path.is_file() else None, + "npy": str(npy_path) if npy_path.is_file() else None, + } + + +def write_scene_files( + scene: dict[str, Any], + output_dir: Path, + stem: str, +) -> dict[str, str]: + output_dir.mkdir(parents=True, exist_ok=True) + npy_path = output_dir / f"{stem}.npy" + obj_path = output_dir / f"{stem}.obj" + npy_path.write_bytes(export_npy_float64(scene["rows"])) + obj_path.write_text(export_obj(scene["points"], object_name=stem), encoding="utf-8") + return {"npy": str(npy_path), "obj": str(obj_path), "stem": stem} + + +def generate_dataset( + *, + count: int = 5, + seed: int = 42, + output_dir: str | Path = "sonar_dataset", + object_points: list[list[float]], + object_name: str | None = None, + object_scale: float = 1.0, + beam_count: int = 45, + length_count: int | None = None, +) -> dict[str, Any]: + """Generate `count` unique scenes into output_dir with Area_X naming. + + object_points: normalized template vertices from user .obj (class 1 = object). + object_scale: relative size multiplier vs unit-normalized mesh (1.0 = default). + beam_count: number of width points for seafloor grid (X axis). + length_count: number of length points for seafloor grid (Y axis). Defaults to beam_count. + """ + count = int(count) + if count < 1: + raise ValueError("count must be >= 1") + if count > 5000: + raise ValueError("count must be <= 5000") + if not object_points or len(object_points) < 3: + raise ValueError("A valid .obj model with at least 3 vertices is required.") + object_scale = float(object_scale) + if object_scale <= 0: + raise ValueError("object_scale must be > 0") + if object_scale > 100: + raise ValueError("object_scale must be <= 100") + beam_count = int(beam_count) + if beam_count < 1: + raise ValueError("beam_count (Кол-во лучей) must be >= 1") + if beam_count > 1024: + raise ValueError("beam_count (Кол-во лучей) must be <= 1024") + if length_count is None: + length_count = beam_count + length_count = int(length_count) + if length_count < 1: + raise ValueError("length_count (Длина) must be >= 1") + if length_count > 1024: + raise ValueError("length_count (Длина) must be <= 1024") + + out = resolve_output_dir(output_dir) + template = normalize_object_points(object_points) + + labels = plan_scene_labels(count, seed) + written: list[dict[str, Any]] = [] + stats = { + "total": count, + "withObject": 0, + "withoutObject": 0, + "nearly_hidden": 0, + "partial": 0, + "visible": 0, + "absent": 0, + } + + preview_points: list[list[float]] | None = None + preview_stem: str | None = None + preview_has_object = False + + for i in range(count): + visibility = labels[i] + scene_seed = int(seed) + i * 10007 + 17 + scene = generate_sonar_scene( + seed=scene_seed, + visibility=visibility, + object_points=template, + object_scale=object_scale, + beam_count=beam_count, + length_count=length_count, + ) + area, scene_no, stem = scene_index_to_area_name(i) + paths = write_scene_files(scene, out, stem) + + entry = { + "index": i, + "area": area, + "scene": scene_no, + "stem": stem, + "visibility": visibility, + "hasObject": scene["hasObject"], + "pointCount": scene["pointCount"], + "objectPointCount": scene["objectPointCount"], + "files": paths, + } + written.append(entry) + + stats[visibility] = stats.get(visibility, 0) + 1 + if scene["hasObject"]: + stats["withObject"] += 1 + else: + stats["withoutObject"] += 1 + + if preview_points is None or (scene["hasObject"] and not preview_has_object): + preview_points = [[r[0], r[1], r[2], r[6]] for r in scene["rows"]] + preview_stem = stem + preview_has_object = bool(scene["hasObject"]) + + preview: dict[str, Any] | None = None + if preview_points is not None: + pts = _downsample_points(preview_points, 25000) + preview = { + "stem": preview_stem, + "points": pts, + "pointCount": len(preview_points), + "classCounts": _class_counts_from_labeled(preview_points), + "classLabels": {"0": "background", "1": "object"}, + } + + return { + "outputDir": str(out), + "count": count, + "seed": int(seed), + "beamCount": beam_count, + "lengthCount": length_count, + "objectName": object_name, + "objectScale": object_scale, + "objectVertexCount": len(template), + "classLabels": {"0": "background", "1": "object"}, + "stats": stats, + "written": written, + "preview": preview, + } diff --git a/api/demo_generator.py b/backend/demo_generator.py similarity index 100% rename from api/demo_generator.py rename to backend/demo_generator.py diff --git a/api/main.py b/backend/main.py similarity index 61% rename from api/main.py rename to backend/main.py index a5ba0ba..22fea00 100644 --- a/api/main.py +++ b/backend/main.py @@ -18,19 +18,32 @@ from pydantic import BaseModel from builtin_presets import BUILTIN_PRESETS, get_builtin_preset from demo_generator import DEMO_SURFACE_TYPES, demo_payload from pipeline_insights import compute_insights +from dataset_generator import generate_dataset, load_object_points_from_obj_text, load_scene_preview +from scene_generator import ( + catalog_payload as generator_catalog_payload, + export_npy_float64, + export_obj, + export_ply, + export_xyz, + generate_layer, + layers_to_pointnet_rows, + merge_layers_world, + parse_obj_points, + points_to_pointnet_rows, + resolve_intersections, +) from stage_meta import STAGE_META, STAGE_META_BY_ID, catalog_payload, defaults_for_stage APP_ROOT = Path(__file__).resolve().parent.parent -WEB_DIST = APP_ROOT / "web-vue" / "dist" -WEB_LEGACY = APP_ROOT / "web" +WEB_DIST = APP_ROOT / "frontend" / "web" / "dist" PRESETS_DIRS = [APP_ROOT / "presets", APP_ROOT / "docker"] USER_PRESETS_DIR = Path(os.environ.get("USER_PRESETS_DIR", "/app/data/user-presets")) DEFAULT_PIPELINE_CONFIG = Path( os.environ.get("PIPELINE_CONFIG", APP_ROOT / "docker" / "default_pipeline.json") ) -DOTSTOSIRFACE_BIN = Path(os.environ.get("DOTSTOSIRFACE_BIN", "/usr/local/bin/DotsToSirface")) +DOTSTOSURFACE_BIN = Path(os.environ.get("DOTSTOSURFACE_BIN", "/usr/local/bin/DotsToSurface")) -app = FastAPI(title="DotsToSirface Web API", version="2.0.0") +app = FastAPI(title="DotsToSurface Web API", version="2.0.0") app.add_middleware( CORSMiddleware, allow_origins=["*"], @@ -64,6 +77,52 @@ class UserPresetBody(BaseModel): stages: list[dict[str, Any]] +class GeneratorLayerBody(BaseModel): + kind: str + type: str + params: dict[str, Any] | None = None + + +class GeneratorLayerItem(BaseModel): + id: str | None = None + kind: str + type: str + name: str | None = None + params: dict[str, Any] | None = None + transform: dict[str, Any] | None = None + points: list[list[float]] | None = None + label: str | None = None + color: str | None = None + + +class GeneratorResolveBody(BaseModel): + layers: list[GeneratorLayerItem] + eps: float = 0.01 + clipSurfaceInsideObjects: bool = True + clipObjectsVsObjects: bool = True + + +class GeneratorExportBody(BaseModel): + points: list[list[float]] | None = None + layers: list[GeneratorLayerItem] | None = None + format: str = "xyz" + filename: str | None = None + # For points-only .npy export when layers are not provided (1=pipe, 0=other). + classLabel: float | None = None + + +class DatasetGenerateBody(BaseModel): + count: int = 5 + seed: int = 42 + outputDir: str = "sonar_dataset" + + +class DatasetPreviewBody(BaseModel): + stem: str + outputDir: str = "sonar_dataset" + maxPoints: int = 25000 + + def preset_to_pipeline_config(preset: dict[str, Any]) -> dict[str, Any]: if "preprocessPlugins" in preset and "reconstructionPlugin" in preset: return preset @@ -181,7 +240,7 @@ def run_pipeline_command(input_path: Path, config: dict[str, Any], output_path: config_path = output_path.parent / "pipeline_config.json" config_path.write_text(json.dumps(config, indent=2), encoding="utf-8") command = [ - str(DOTSTOSIRFACE_BIN), + str(DOTSTOSURFACE_BIN), "--cli", "--input", str(input_path), @@ -220,8 +279,8 @@ def enrich_result(result: dict[str, Any], config: dict[str, Any], stdout: str, w def health() -> dict[str, str]: return { "status": "ok", - "binary": str(DOTSTOSIRFACE_BIN), - "binaryExists": str(DOTSTOSIRFACE_BIN.is_file()), + "binary": str(DOTSTOSURFACE_BIN), + "binaryExists": str(DOTSTOSURFACE_BIN.is_file()), "webDist": str(WEB_DIST.is_dir()), } @@ -329,6 +388,159 @@ def demo_types() -> list[str]: return DEMO_SURFACE_TYPES +@app.get("/api/generator/catalog") +def generator_catalog() -> dict[str, Any]: + return generator_catalog_payload() + + +@app.post("/api/generator/layer") +def generator_layer(body: GeneratorLayerBody) -> dict[str, Any]: + try: + return generate_layer(body.kind, body.type, body.params) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + + +@app.post("/api/generator/resolve-intersections") +def generator_resolve_intersections(body: GeneratorResolveBody) -> dict[str, Any]: + try: + layers = [item.model_dump() for item in body.layers] + resolved = resolve_intersections( + layers, + eps=body.eps, + clip_surface_inside_objects=body.clipSurfaceInsideObjects, + clip_objects_vs_objects=body.clipObjectsVsObjects, + ) + return { + "layers": resolved, + "removedTotal": sum(int(layer.get("removedCount", 0)) for layer in resolved), + } + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + + +@app.post("/api/dataset/generate") +async def dataset_generate( + count: int = Form(5), + seed: int = Form(42), + outputDir: str = Form("sonar_dataset"), + objectScale: float = Form(1.0), + beamCount: int = Form(45), + lengthCount: int | None = Form(None), + model: UploadFile = File(...), +) -> dict[str, Any]: + filename = (model.filename or "").strip() + if not filename.lower().endswith(".obj"): + raise HTTPException(status_code=400, detail="Upload a .obj 3D model file.") + try: + raw = await model.read() + text = raw.decode("utf-8", errors="ignore") + object_points = load_object_points_from_obj_text(text) + return generate_dataset( + count=count, + seed=seed, + output_dir=outputDir or "sonar_dataset", + object_points=object_points, + object_name=filename, + object_scale=objectScale, + beam_count=beamCount, + length_count=lengthCount, + ) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + except OSError as exc: + raise HTTPException(status_code=500, detail=f"Failed to write dataset: {exc}") from exc + + +@app.post("/api/dataset/preview") +def dataset_preview(body: DatasetPreviewBody) -> dict[str, Any]: + try: + return load_scene_preview( + stem=body.stem, + output_dir=body.outputDir or "sonar_dataset", + max_points=body.maxPoints, + ) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + except FileNotFoundError as exc: + raise HTTPException(status_code=404, detail=str(exc)) from exc + except OSError as exc: + raise HTTPException(status_code=500, detail=f"Failed to load scene: {exc}") from exc + + +@app.post("/api/generator/export") +def generator_export(body: GeneratorExportBody) -> Response: + from urllib.parse import quote + + fmt = (body.format or "xyz").lower().lstrip(".") + if fmt not in ("xyz", "ply", "obj", "npy"): + raise HTTPException(status_code=400, detail="Supported formats: xyz, ply, obj, npy") + + filename = body.filename + content: bytes + media: str + ext: str + + if fmt == "npy": + try: + if body.layers: + rows = layers_to_pointnet_rows([item.model_dump() for item in body.layers]) + elif body.points is not None: + label = 0.0 if body.classLabel is None else float(body.classLabel) + rows = points_to_pointnet_rows(body.points, label) + else: + raise HTTPException(status_code=400, detail="Provide points or layers to export.") + content = export_npy_float64(rows) + except HTTPException: + raise + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + media = "application/octet-stream" + ext = "npy" + else: + points = body.points + if points is None and body.layers: + try: + points = merge_layers_world([item.model_dump() for item in body.layers]) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + if points is None: + raise HTTPException(status_code=400, detail="Provide points or layers to export.") + + if fmt == "ply": + text = export_ply(points) + media = "application/octet-stream" + ext = "ply" + elif fmt == "obj": + stem = Path(body.filename or "cloud").stem or "cloud" + text = export_obj(points, object_name=stem) + media = "text/plain; charset=utf-8" + ext = "obj" + else: + text = export_xyz(points) + media = "text/plain; charset=utf-8" + ext = "xyz" + content = text.encode("utf-8") + + out_name = filename or f"cloud.{ext}" + if not out_name.lower().endswith(f".{ext}"): + out_name = f"{out_name}.{ext}" + + ascii_name = "".join(ch if 32 <= ord(ch) < 127 and ch not in '\\/"' else "_" for ch in out_name) + if not ascii_name.lower().endswith(f".{ext}"): + ascii_name = f"cloud.{ext}" + disposition = ( + f"attachment; filename=\"{ascii_name}\"; " + f"filename*=UTF-8''{quote(out_name)}" + ) + + return Response( + content=content, + media_type=media, + headers={"Content-Disposition": disposition}, + ) + + @app.get("/api/user-presets") def get_user_presets() -> list[dict[str, Any]]: return load_user_presets() @@ -363,11 +575,11 @@ async def run_pipeline( demo_surface: str | None = Form(default=None), geometry_format: str = Form(default="json"), ) -> dict[str, Any]: - if not DOTSTOSIRFACE_BIN.is_file(): - raise HTTPException(status_code=500, detail=f"Binary not found: {DOTSTOSIRFACE_BIN}") + if not DOTSTOSURFACE_BIN.is_file(): + raise HTTPException(status_code=500, detail=f"Binary not found: {DOTSTOSURFACE_BIN}") work_id = uuid.uuid4().hex - work_dir = Path(tempfile.gettempdir()) / "dotstosirface" / work_id + work_dir = Path(tempfile.gettempdir()) / "dottosurface" / work_id work_dir.mkdir(parents=True, exist_ok=True) output_path = work_dir / "result.json" @@ -378,9 +590,24 @@ async def run_pipeline( lines = [f"{p[0]} {p[1]} {p[2]}" for p in demo["points"]] input_path.write_text("\n".join(lines) + "\n", encoding="utf-8") elif file is not None: - suffix = Path(file.filename or "cloud.ply").suffix or ".ply" - input_path = work_dir / f"input{suffix}" - input_path.write_bytes(await file.read()) + suffix = Path(file.filename or "cloud.ply").suffix.lower() or ".ply" + raw = await file.read() + if suffix == ".obj": + try: + text = raw.decode("utf-8", errors="ignore") + obj_points = parse_obj_points(text) + except Exception as exc: + raise HTTPException(status_code=400, detail=f"Invalid OBJ: {exc}") from exc + if not obj_points: + raise HTTPException(status_code=400, detail="OBJ has no vertices (v x y z).") + input_path = work_dir / "input.xyz" + input_path.write_text( + "\n".join(f"{p[0]} {p[1]} {p[2]}" for p in obj_points) + "\n", + encoding="utf-8", + ) + else: + input_path = work_dir / f"input{suffix}" + input_path.write_bytes(raw) else: raise HTTPException(status_code=400, detail="Provide file or demo_surface.") @@ -438,21 +665,45 @@ async def run_pipeline( @app.get("/api/geometry/{work_id}") def get_geometry(work_id: str) -> Response: - bin_path = Path(tempfile.gettempdir()) / "dotstosirface" / work_id / "geometry.bin" + bin_path = Path(tempfile.gettempdir()) / "dottosurface" / work_id / "geometry.bin" if not bin_path.is_file(): raise HTTPException(status_code=404, detail="Geometry not found.") return Response(content=bin_path.read_bytes(), media_type="application/octet-stream") +@app.get("/airplane_reference.png") +def airplane_reference_image() -> FileResponse: + candidates = [ + WEB_DIST / "airplane_reference.png", + APP_ROOT / "assets" / "airplane_reference.png", + APP_ROOT / "frontend" / "web" / "public" / "airplane_reference.png", + ] + for path in candidates: + if path.is_file(): + return FileResponse(path, media_type="image/png") + raise HTTPException(status_code=404, detail="Airplane reference image not found.") + + @app.get("/") def index() -> FileResponse: - if (WEB_DIST / "index.html").is_file(): - return FileResponse(WEB_DIST / "index.html") - return FileResponse(WEB_LEGACY / "index.html") + index_path = WEB_DIST / "index.html" + if not index_path.is_file(): + raise HTTPException( + status_code=503, + detail="Frontend not built. Run: cd frontend/web && npm install && npm run build", + ) + return FileResponse(index_path) + + +@app.get("/generator") +def generator_spa() -> FileResponse: + return index() + + +@app.get("/dataset") +def dataset_spa() -> FileResponse: + return index() if (WEB_DIST / "assets").is_dir(): app.mount("/assets", StaticFiles(directory=WEB_DIST / "assets"), name="assets") - -if WEB_LEGACY.is_dir() and not (WEB_DIST / "index.html").is_file(): - app.mount("/static", StaticFiles(directory=WEB_LEGACY), name="static") diff --git a/api/pipeline_insights.py b/backend/pipeline_insights.py similarity index 94% rename from api/pipeline_insights.py rename to backend/pipeline_insights.py index bdaff09..f01a392 100644 --- a/api/pipeline_insights.py +++ b/backend/pipeline_insights.py @@ -57,7 +57,7 @@ def compute_insights( recommendation = warnings[0] else: chain_health = "OK" - recommendation = "Pipeline looks balanced. Use Compare snapshots for A/B tuning." + recommendation = "Пайплайн сбалансирован. Используйте «Сохр. конф.» для A/B-сравнения." return { "chainHealth": chain_health, diff --git a/api/requirements.txt b/backend/requirements.txt similarity index 100% rename from api/requirements.txt rename to backend/requirements.txt diff --git a/backend/scene_generator.py b/backend/scene_generator.py new file mode 100644 index 0000000..78409fc --- /dev/null +++ b/backend/scene_generator.py @@ -0,0 +1,674 @@ +"""Parametric scene generator: objects, terrain surfaces, intersection clipping, export.""" + +from __future__ import annotations + +import math +import random +from typing import Any + +# --------------------------------------------------------------------------- +# Catalog / default params +# --------------------------------------------------------------------------- + +LAYER_CATALOG: list[dict[str, Any]] = [ + { + "kind": "object", + "type": "pipe", + "label": "Труба", + "params": [ + {"key": "length", "label": "Длина", "type": "number", "default": 2.6, "min": 0.2, "max": 20, "step": 0.1}, + {"key": "radius", "label": "Радиус", "type": "number", "default": 0.22, "min": 0.02, "max": 5, "step": 0.01}, + {"key": "axis", "label": "Ось", "type": "select", "default": "y", "options": ["x", "y", "z"]}, + {"key": "count", "label": "Точек", "type": "number", "default": 2500, "min": 100, "max": 100000, "step": 100}, + {"key": "noise", "label": "Шум", "type": "number", "default": 0.01, "min": 0, "max": 0.5, "step": 0.005}, + {"key": "seed", "label": "Seed", "type": "number", "default": 1, "min": 0, "max": 999999, "step": 1}, + ], + }, + { + "kind": "object", + "type": "sphere", + "label": "Сфера", + "params": [ + {"key": "radius", "label": "Радиус", "type": "number", "default": 0.5, "min": 0.05, "max": 10, "step": 0.05}, + {"key": "count", "label": "Точек", "type": "number", "default": 2000, "min": 100, "max": 100000, "step": 100}, + {"key": "noise", "label": "Шум", "type": "number", "default": 0.02, "min": 0, "max": 0.5, "step": 0.005}, + {"key": "seed", "label": "Seed", "type": "number", "default": 1, "min": 0, "max": 999999, "step": 1}, + ], + }, + { + "kind": "object", + "type": "box", + "label": "Параллелепипед", + "params": [ + {"key": "sizeX", "label": "Размер X", "type": "number", "default": 1.0, "min": 0.1, "max": 20, "step": 0.1}, + {"key": "sizeY", "label": "Размер Y", "type": "number", "default": 0.6, "min": 0.1, "max": 20, "step": 0.1}, + {"key": "sizeZ", "label": "Размер Z", "type": "number", "default": 0.4, "min": 0.1, "max": 20, "step": 0.1}, + {"key": "count", "label": "Точек", "type": "number", "default": 2000, "min": 100, "max": 100000, "step": 100}, + {"key": "noise", "label": "Шум", "type": "number", "default": 0.01, "min": 0, "max": 0.5, "step": 0.005}, + {"key": "seed", "label": "Seed", "type": "number", "default": 1, "min": 0, "max": 999999, "step": 1}, + ], + }, + { + "kind": "object", + "type": "torus", + "label": "Тор", + "params": [ + {"key": "majorR", "label": "Большой R", "type": "number", "default": 1.0, "min": 0.1, "max": 10, "step": 0.05}, + {"key": "minorR", "label": "Малый R", "type": "number", "default": 0.35, "min": 0.02, "max": 5, "step": 0.01}, + {"key": "count", "label": "Точек", "type": "number", "default": 2500, "min": 100, "max": 100000, "step": 100}, + {"key": "noise", "label": "Шум", "type": "number", "default": 0.01, "min": 0, "max": 0.5, "step": 0.005}, + {"key": "seed", "label": "Seed", "type": "number", "default": 1, "min": 0, "max": 999999, "step": 1}, + ], + }, + { + "kind": "surface", + "type": "ocean_floor", + "label": "Дно океана", + "params": [ + {"key": "sizeX", "label": "Размер X", "type": "number", "default": 4.0, "min": 0.5, "max": 50, "step": 0.1}, + {"key": "sizeY", "label": "Размер Y", "type": "number", "default": 3.0, "min": 0.5, "max": 50, "step": 0.1}, + {"key": "amplitude", "label": "Амплитуда", "type": "number", "default": 0.12, "min": 0, "max": 2, "step": 0.01}, + {"key": "frequency", "label": "Частота", "type": "number", "default": 2.2, "min": 0.1, "max": 20, "step": 0.1}, + {"key": "channel", "label": "Канал", "type": "number", "default": 0.08, "min": 0, "max": 1, "step": 0.01}, + {"key": "baseZ", "label": "База Z", "type": "number", "default": -0.45, "min": -20, "max": 20, "step": 0.05}, + {"key": "count", "label": "Точек", "type": "number", "default": 4000, "min": 100, "max": 100000, "step": 100}, + {"key": "noise", "label": "Шум", "type": "number", "default": 0.02, "min": 0, "max": 0.5, "step": 0.005}, + {"key": "seed", "label": "Seed", "type": "number", "default": 1, "min": 0, "max": 999999, "step": 1}, + ], + }, + { + "kind": "surface", + "type": "wave", + "label": "Волна", + "params": [ + {"key": "sizeX", "label": "Размер X", "type": "number", "default": 2.4, "min": 0.5, "max": 50, "step": 0.1}, + {"key": "sizeY", "label": "Размер Y", "type": "number", "default": 2.4, "min": 0.5, "max": 50, "step": 0.1}, + {"key": "amplitude", "label": "Амплитуда", "type": "number", "default": 0.35, "min": 0, "max": 5, "step": 0.05}, + {"key": "frequency", "label": "Частота", "type": "number", "default": 2.5, "min": 0.1, "max": 20, "step": 0.1}, + {"key": "count", "label": "Точек", "type": "number", "default": 3000, "min": 100, "max": 100000, "step": 100}, + {"key": "noise", "label": "Шум", "type": "number", "default": 0.015, "min": 0, "max": 0.5, "step": 0.005}, + {"key": "seed", "label": "Seed", "type": "number", "default": 1, "min": 0, "max": 999999, "step": 1}, + ], + }, + { + "kind": "surface", + "type": "flat", + "label": "Плоскость", + "params": [ + {"key": "sizeX", "label": "Размер X", "type": "number", "default": 4.0, "min": 0.5, "max": 50, "step": 0.1}, + {"key": "sizeY", "label": "Размер Y", "type": "number", "default": 4.0, "min": 0.5, "max": 50, "step": 0.1}, + {"key": "z", "label": "Высота Z", "type": "number", "default": -0.5, "min": -20, "max": 20, "step": 0.05}, + {"key": "count", "label": "Точек", "type": "number", "default": 2500, "min": 100, "max": 100000, "step": 100}, + {"key": "noise", "label": "Шум", "type": "number", "default": 0.01, "min": 0, "max": 0.5, "step": 0.005}, + {"key": "seed", "label": "Seed", "type": "number", "default": 1, "min": 0, "max": 999999, "step": 1}, + ], + }, +] + +_CATALOG_BY_KEY = {(item["kind"], item["type"]): item for item in LAYER_CATALOG} + +LAYER_COLORS = { + ("object", "pipe"): "#f59e0b", + ("object", "sphere"): "#38bdf8", + ("object", "box"): "#a78bfa", + ("object", "torus"): "#34d399", + ("surface", "ocean_floor"): "#64748b", + ("surface", "wave"): "#94a3b8", + ("surface", "flat"): "#78716c", +} + + +def catalog_payload() -> dict[str, Any]: + return {"layers": LAYER_CATALOG, "colors": {f"{k[0]}:{k[1]}": v for k, v in LAYER_COLORS.items()}} + + +def default_params(kind: str, type_name: str) -> dict[str, Any]: + entry = _CATALOG_BY_KEY.get((kind, type_name)) + if entry is None: + raise ValueError(f"Unknown layer type: {kind}/{type_name}") + return {p["key"]: p["default"] for p in entry["params"]} + + +def merge_params(kind: str, type_name: str, params: dict[str, Any] | None) -> dict[str, Any]: + merged = default_params(kind, type_name) + if params: + for key, value in params.items(): + if key in merged: + merged[key] = value + # Coerce numeric fields + entry = _CATALOG_BY_KEY[(kind, type_name)] + for p in entry["params"]: + key = p["key"] + if p["type"] == "number" and key in merged: + try: + merged[key] = float(merged[key]) + if key in ("count", "seed"): + merged[key] = int(merged[key]) + except (TypeError, ValueError): + merged[key] = p["default"] + if p["type"] == "select" and key in merged: + options = p.get("options") or [] + if merged[key] not in options: + merged[key] = p["default"] + return merged + + +# --------------------------------------------------------------------------- +# Generation +# --------------------------------------------------------------------------- + +def _jitter(rng: random.Random, noise: float) -> float: + if noise <= 0: + return 0.0 + return rng.uniform(-noise, noise) + + +def generate_pipe(params: dict[str, Any]) -> list[list[float]]: + rng = random.Random(int(params["seed"])) + count = max(1, int(params["count"])) + length = float(params["length"]) + radius = float(params["radius"]) + noise = float(params["noise"]) + axis = params.get("axis", "y") + points: list[list[float]] = [] + half = length * 0.5 + for _ in range(count): + angle = rng.random() * 2.0 * math.pi + t = rng.uniform(-half, half) + radial = radius + _jitter(rng, noise) + cx = radial * math.cos(angle) + cy = radial * math.sin(angle) + if axis == "x": + points.append([t, cx, cy]) + elif axis == "z": + points.append([cx, cy, t]) + else: + points.append([cx, t, cy]) + return points + + +def generate_sphere(params: dict[str, Any]) -> list[list[float]]: + rng = random.Random(int(params["seed"])) + count = max(1, int(params["count"])) + radius = float(params["radius"]) + noise = float(params["noise"]) + points: list[list[float]] = [] + for _ in range(count): + u = rng.uniform(-1.0, 1.0) + theta = rng.random() * 2.0 * math.pi + r = radius + _jitter(rng, noise) + s = math.sqrt(max(0.0, 1.0 - u * u)) + points.append([r * s * math.cos(theta), r * s * math.sin(theta), r * u]) + return points + + +def generate_box(params: dict[str, Any]) -> list[list[float]]: + """Sample points on the box surface.""" + rng = random.Random(int(params["seed"])) + count = max(1, int(params["count"])) + sx = float(params["sizeX"]) * 0.5 + sy = float(params["sizeY"]) * 0.5 + sz = float(params["sizeZ"]) * 0.5 + noise = float(params["noise"]) + faces = [ + ("x", sx, sy, sz), + ("x", -sx, sy, sz), + ("y", sy, sx, sz), + ("y", -sy, sx, sz), + ("z", sz, sx, sy), + ("z", -sz, sx, sy), + ] + areas = [abs(a[2]) * abs(a[3]) * 4.0 for a in faces] + total = sum(areas) or 1.0 + points: list[list[float]] = [] + for _ in range(count): + pick = rng.random() * total + acc = 0.0 + face = faces[0] + for f, area in zip(faces, areas): + acc += area + if pick <= acc: + face = f + break + axis, fixed, u_max, v_max = face + u = rng.uniform(-u_max, u_max) + v = rng.uniform(-v_max, v_max) + jx, jy, jz = _jitter(rng, noise), _jitter(rng, noise), _jitter(rng, noise) + if axis == "x": + points.append([fixed + jx, u + jy, v + jz]) + elif axis == "y": + points.append([u + jx, fixed + jy, v + jz]) + else: + points.append([u + jx, v + jy, fixed + jz]) + return points + + +def generate_torus(params: dict[str, Any]) -> list[list[float]]: + rng = random.Random(int(params["seed"])) + count = max(1, int(params["count"])) + major_r = float(params["majorR"]) + minor_r = float(params["minorR"]) + noise = float(params["noise"]) + points: list[list[float]] = [] + for _ in range(count): + u = rng.random() * 2.0 * math.pi + v = rng.random() * 2.0 * math.pi + radial = minor_r + _jitter(rng, noise) + x = (major_r + radial * math.cos(v)) * math.cos(u) + y = (major_r + radial * math.cos(v)) * math.sin(u) + z = radial * math.sin(v) + points.append([x, y, z]) + return points + + +def height_ocean_floor(x: float, y: float, params: dict[str, Any]) -> float: + amplitude = float(params["amplitude"]) + frequency = float(params["frequency"]) + channel = float(params["channel"]) + base_z = float(params["baseZ"]) + waviness = amplitude * math.cos(frequency * y) + channel_term = channel * x * x + return base_z + channel_term + waviness + + +def height_wave(x: float, y: float, params: dict[str, Any]) -> float: + amplitude = float(params["amplitude"]) + frequency = float(params["frequency"]) + return amplitude * math.sin(frequency * x) * math.cos(frequency * y) + + +def height_flat(_x: float, _y: float, params: dict[str, Any]) -> float: + return float(params["z"]) + + +def surface_height_fn(type_name: str): + if type_name == "ocean_floor": + return height_ocean_floor + if type_name == "wave": + return height_wave + if type_name == "flat": + return height_flat + raise ValueError(f"Unknown surface type: {type_name}") + + +def generate_surface(type_name: str, params: dict[str, Any]) -> list[list[float]]: + rng = random.Random(int(params["seed"])) + count = max(1, int(params["count"])) + size_x = float(params.get("sizeX", 2.0)) + size_y = float(params.get("sizeY", 2.0)) + noise = float(params["noise"]) + height_fn = surface_height_fn(type_name) + half_x = size_x * 0.5 + half_y = size_y * 0.5 + points: list[list[float]] = [] + for _ in range(count): + x = rng.uniform(-half_x, half_x) + y = rng.uniform(-half_y, half_y) + z = height_fn(x, y, params) + _jitter(rng, noise) + points.append([x, y, z]) + return points + + +_GENERATORS = { + ("object", "pipe"): generate_pipe, + ("object", "sphere"): generate_sphere, + ("object", "box"): generate_box, + ("object", "torus"): generate_torus, +} + + +def generate_layer(kind: str, type_name: str, params: dict[str, Any] | None = None) -> dict[str, Any]: + if (kind, type_name) not in _CATALOG_BY_KEY: + raise ValueError(f"Unknown layer type: {kind}/{type_name}") + merged = merge_params(kind, type_name, params) + if kind == "surface": + points = generate_surface(type_name, merged) + else: + points = _GENERATORS[(kind, type_name)](merged) + entry = _CATALOG_BY_KEY[(kind, type_name)] + return { + "kind": kind, + "type": type_name, + "label": entry["label"], + "params": merged, + "pointCount": len(points), + "points": points, + "color": LAYER_COLORS.get((kind, type_name), "#7dd3fc"), + } + + +# --------------------------------------------------------------------------- +# Transforms & intersections +# --------------------------------------------------------------------------- + +def normalize_transform(transform: dict[str, Any] | None) -> dict[str, float]: + t = transform or {} + return { + "x": float(t.get("x", 0.0) or 0.0), + "y": float(t.get("y", 0.0) or 0.0), + "z": float(t.get("z", 0.0) or 0.0), + "rx": float(t.get("rx", 0.0) or 0.0), + "ry": float(t.get("ry", 0.0) or 0.0), + "rz": float(t.get("rz", 0.0) or 0.0), + } + + +def _rotate_xyz(x: float, y: float, z: float, rx: float, ry: float, rz: float) -> tuple[float, float, float]: + """Euler XYZ (same as Three.js Object3D.rotation default order).""" + cx, sx = math.cos(rx), math.sin(rx) + cy, sy = math.cos(ry), math.sin(ry) + cz, sz = math.cos(rz), math.sin(rz) + y, z = y * cx - z * sx, y * sx + z * cx + x, z = x * cy + z * sy, -x * sy + z * cy + x, y = x * cz - y * sz, x * sz + y * cz + return x, y, z + + +def _rotate_xyz_inverse(x: float, y: float, z: float, rx: float, ry: float, rz: float) -> tuple[float, float, float]: + cx, sx = math.cos(rx), math.sin(rx) + cy, sy = math.cos(ry), math.sin(ry) + cz, sz = math.cos(rz), math.sin(rz) + x, y = x * cz + y * sz, -x * sz + y * cz + x, z = x * cy - z * sy, x * sy + z * cy + y, z = y * cx + z * sx, -y * sx + z * cx + return x, y, z + + +def apply_transform(points: list[list[float]], transform: dict[str, Any] | None) -> list[list[float]]: + t = normalize_transform(transform) + out: list[list[float]] = [] + for p in points: + x, y, z = _rotate_xyz(p[0], p[1], p[2], t["rx"], t["ry"], t["rz"]) + out.append([x + t["x"], y + t["y"], z + t["z"]]) + return out + + +def _world_to_local(point: list[float], transform: dict[str, Any] | None) -> tuple[float, float, float]: + t = normalize_transform(transform) + x = point[0] - t["x"] + y = point[1] - t["y"] + z = point[2] - t["z"] + return _rotate_xyz_inverse(x, y, z, t["rx"], t["ry"], t["rz"]) + + +def object_sdf(type_name: str, params: dict[str, Any], local: tuple[float, float, float]) -> float: + """Signed distance: negative = inside.""" + if type_name == "imported": + # No analytic SDF for imported clouds — skip solid clipping. + return 1.0 + x, y, z = local + if type_name == "sphere": + return math.sqrt(x * x + y * y + z * z) - float(params["radius"]) + if type_name == "pipe": + radius = float(params["radius"]) + half = float(params["length"]) * 0.5 + axis = params.get("axis", "y") + if axis == "x": + radial = math.sqrt(y * y + z * z) - radius + axial = abs(x) - half + elif axis == "z": + radial = math.sqrt(x * x + y * y) - radius + axial = abs(z) - half + else: + radial = math.sqrt(x * x + z * z) - radius + axial = abs(y) - half + # Approximate solid cylinder: inside if radial < 0 and axial < 0 + outside = max(radial, axial) + if radial < 0 and axial < 0: + return max(radial, axial) + if axial > 0 and radial < 0: + return axial + if radial > 0 and axial < 0: + return radial + return math.sqrt(max(radial, 0) ** 2 + max(axial, 0) ** 2) if outside > 0 else outside + if type_name == "box": + hx = float(params["sizeX"]) * 0.5 + hy = float(params["sizeY"]) * 0.5 + hz = float(params["sizeZ"]) * 0.5 + qx = abs(x) - hx + qy = abs(y) - hy + qz = abs(z) - hz + outside = math.sqrt(max(qx, 0) ** 2 + max(qy, 0) ** 2 + max(qz, 0) ** 2) + inside = min(max(qx, qy, qz), 0.0) + return outside + inside + if type_name == "torus": + major_r = float(params["majorR"]) + minor_r = float(params["minorR"]) + q = math.sqrt(x * x + y * y) - major_r + return math.sqrt(q * q + z * z) - minor_r + return 1.0 + + +def point_below_surface( + world_pt: list[float], + surf_type: str, + surf_params: dict[str, Any], + surf_transform: dict[str, Any] | None, + eps: float, +) -> bool: + """True if world point is below the heightfield in the surface local frame.""" + lx, ly, lz = _world_to_local(world_pt, surf_transform) + size_x = float(surf_params.get("sizeX", 1e9)) + size_y = float(surf_params.get("sizeY", 1e9)) + if abs(lx) > size_x * 0.5 + eps or abs(ly) > size_y * 0.5 + eps: + return False + h = surface_height_fn(surf_type)(lx, ly, surf_params) + return lz < h + eps + + +def resolve_intersections( + layers: list[dict[str, Any]], + *, + eps: float = 0.01, + clip_surface_inside_objects: bool = True, + clip_objects_vs_objects: bool = True, +) -> list[dict[str, Any]]: + """Return layers with points updated (local coords preserved via inverse transform).""" + prepared: list[dict[str, Any]] = [] + for layer in layers: + kind = layer["kind"] + type_name = layer["type"] + transform = normalize_transform(layer.get("transform")) + local_points = layer.get("points") + if type_name == "imported": + params = dict(layer.get("params") or {}) + if not local_points: + raise ValueError("Imported layer has no points.") + label = layer.get("label") or "OBJ" + color = layer.get("color") or "#f472b6" + else: + params = merge_params(kind, type_name, layer.get("params")) + if not local_points: + generated = generate_layer(kind, type_name, params) + local_points = generated["points"] + label = layer.get("label") or _CATALOG_BY_KEY[(kind, type_name)]["label"] + color = layer.get("color") or LAYER_COLORS.get((kind, type_name), "#7dd3fc") + world = apply_transform(local_points, transform) + prepared.append({ + "id": layer.get("id"), + "kind": kind, + "type": type_name, + "params": params, + "transform": transform, + "local_points": local_points, + "world_points": world, + "color": color, + "label": label, + }) + + surfaces = [p for p in prepared if p["kind"] == "surface"] + objects = [p for p in prepared if p["kind"] == "object"] + + result: list[dict[str, Any]] = [] + for layer in prepared: + keep_local: list[list[float]] = [] + keep_world: list[list[float]] = [] + for local_pt, world_pt in zip(layer["local_points"], layer["world_points"]): + drop = False + + if layer["kind"] == "object": + for surf in surfaces: + if point_below_surface( + world_pt, surf["type"], surf["params"], surf["transform"], eps + ): + drop = True + break + if not drop and clip_objects_vs_objects: + for other in objects: + if other is layer: + continue + local_in_other = _world_to_local(world_pt, other["transform"]) + if object_sdf(other["type"], other["params"], local_in_other) < -eps: + drop = True + break + + elif layer["kind"] == "surface" and clip_surface_inside_objects: + for obj in objects: + local_in_obj = _world_to_local(world_pt, obj["transform"]) + if object_sdf(obj["type"], obj["params"], local_in_obj) < -eps: + drop = True + break + + if not drop: + keep_local.append([local_pt[0], local_pt[1], local_pt[2]]) + keep_world.append(world_pt) + + result.append({ + "id": layer["id"], + "kind": layer["kind"], + "type": layer["type"], + "label": layer["label"], + "params": layer["params"], + "transform": layer["transform"], + "color": layer["color"], + "pointCount": len(keep_local), + "points": keep_local, + "removedCount": len(layer["local_points"]) - len(keep_local), + }) + + return result + + +def merge_layers_world(layers: list[dict[str, Any]]) -> list[list[float]]: + merged: list[list[float]] = [] + for layer in layers: + kind = layer["kind"] + type_name = layer["type"] + transform = normalize_transform(layer.get("transform")) + points = layer.get("points") + if not points: + if type_name == "imported": + continue + params = merge_params(kind, type_name, layer.get("params")) + points = generate_layer(kind, type_name, params)["points"] + merged.extend(apply_transform(points, transform)) + return merged + + +def layer_semantic_class(layer: dict[str, Any]) -> float: + """Binary PointNet label: 1 = pipe, 0 = everything else.""" + type_name = str(layer.get("type") or "").lower() + if type_name == "pipe": + return 1.0 + # Optional name hint for renamed imported clouds + name = str(layer.get("name") or layer.get("label") or "").lower() + if "pipe" in name or "труб" in name: + return 1.0 + return 0.0 + + +def points_to_pointnet_rows(points: list[list[float]], class_label: float) -> list[list[float]]: + """XYZRGB+class rows; RGB forced to 0; float values (stored as float64 in .npy).""" + c = float(class_label) + rows: list[list[float]] = [] + for p in points: + rows.append([float(p[0]), float(p[1]), float(p[2]), 0.0, 0.0, 0.0, c]) + return rows + + +def layers_to_pointnet_rows(layers: list[dict[str, Any]]) -> list[list[float]]: + rows: list[list[float]] = [] + for layer in layers: + kind = layer.get("kind") or "object" + type_name = layer.get("type") or "imported" + transform = normalize_transform(layer.get("transform")) + points = layer.get("points") + if not points: + if type_name == "imported": + continue + params = merge_params(kind, type_name, layer.get("params")) + points = generate_layer(kind, type_name, params)["points"] + world = apply_transform(points, transform) + rows.extend(points_to_pointnet_rows(world, layer_semantic_class(layer))) + return rows + + +def export_npy_float64(rows: list[list[float]]) -> bytes: + """Write NumPy .npy v1.0 binary array shape (N, C) dtype float64 little-endian.""" + import struct + + n = len(rows) + cols = len(rows[0]) if n else 7 + if n and any(len(r) != cols for r in rows): + raise ValueError("All rows must have the same length for .npy export.") + + header = "{'descr': ' str: + return "\n".join(f"{p[0]:.8f} {p[1]:.8f} {p[2]:.8f}" for p in points) + ("\n" if points else "") + + +def export_ply(points: list[list[float]]) -> str: + header = ( + "ply\n" + "format ascii 1.0\n" + f"element vertex {len(points)}\n" + "property float x\n" + "property float y\n" + "property float z\n" + "end_header\n" + ) + body = "\n".join(f"{p[0]:.8f} {p[1]:.8f} {p[2]:.8f}" for p in points) + return header + body + ("\n" if points else "") + + +def export_obj(points: list[list[float]], object_name: str = "cloud") -> str: + safe_name = "".join(ch if ch.isalnum() or ch in "_-" else "_" for ch in (object_name or "cloud")) or "cloud" + lines = [f"# DotsToSurface point cloud ({len(points)} vertices)", f"o {safe_name}"] + for p in points: + lines.append(f"v {p[0]:.8f} {p[1]:.8f} {p[2]:.8f}") + return "\n".join(lines) + "\n" + +def parse_obj_points(text: str) -> list[list[float]]: + """Extract vertex positions from Wavefront OBJ (ignores faces/materials).""" + points: list[list[float]] = [] + for raw in text.splitlines(): + line = raw.strip() + if not line or line.startswith("#"): + continue + if line.lower().startswith("v "): + parts = line.split() + if len(parts) < 4: + continue + try: + points.append([float(parts[1]), float(parts[2]), float(parts[3])]) + except ValueError: + continue + return points diff --git a/api/stage_meta.py b/backend/stage_meta.py similarity index 100% rename from api/stage_meta.py rename to backend/stage_meta.py diff --git a/docker/Dockerfile b/docker/Dockerfile index 204063c..a05f694 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,10 +1,10 @@ FROM node:20-bookworm-slim AS web-builder WORKDIR /web -COPY web-vue/package.json ./ +COPY frontend/web/package.json ./ RUN npm install -COPY web-vue/ ./ +COPY frontend/web/ ./ RUN npm run build FROM ubuntu:24.04 AS builder @@ -25,13 +25,13 @@ WORKDIR /src COPY . . RUN mkdir -p build && cd build \ - && qmake ../DotsToSirface.pro 'DEFINES+=PCL_ENABLED' \ + && qmake ../DotsToSurface.pro 'DEFINES+=PCL_ENABLED' \ && make -j"$(nproc)" FROM ubuntu:24.04 ENV DEBIAN_FRONTEND=noninteractive -ENV DOTSTOSIRFACE_BIN=/usr/local/bin/DotsToSirface +ENV DOTSTOSURFACE_BIN=/usr/local/bin/DotsToSurface ENV PIPELINE_CONFIG=/app/docker/default_pipeline.json ENV USER_PRESETS_DIR=/app/data/user-presets ENV PYTHONUNBUFFERED=1 @@ -58,13 +58,14 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ libgl1 \ && rm -rf /var/lib/apt/lists/* -COPY --from=builder /src/build/DotsToSirface /usr/local/bin/DotsToSirface -COPY --from=web-builder /web/dist /app/web-vue/dist -COPY api /app/api +COPY --from=builder /src/build/DotsToSurface /usr/local/bin/DotsToSurface +COPY --from=web-builder /web/dist /app/frontend/web/dist +COPY backend /app/backend COPY docker /app/docker COPY presets /app/presets +COPY assets /app/assets -WORKDIR /app/api +WORKDIR /app/backend RUN python3 -m venv /opt/venv \ && /opt/venv/bin/pip install --no-cache-dir -r requirements.txt diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 0000000..db6292c --- /dev/null +++ b/docker/README.md @@ -0,0 +1,44 @@ +# Docker + +**Infra** — сборка и запуск Web-стека (Frontend + Backend + Engine). + +## Быстрый старт + +Запуск **только в контейнере** (на хосте ставить Python/npm для работы приложения не нужно). + +```bash +cd docker +docker compose up --build +# → http://localhost:8080 +``` + +Или из корня репозитория: + +```bash +./scripts/run-docker.sh # с логами в терминале +./scripts/run-docker.sh -d # в фоне +``` + +Если порт **8080** занят, в `docker-compose.yml` измените маппинг, например `"8081:8080"`. + +Перед первым запуском должен быть запущен Docker Engine (`docker ps` без ошибки). + +## Multi-stage сборка + +| Stage | Результат | +|-------|-----------| +| `web-builder` | `frontend/web/dist` | +| `builder` | `DotsToSurface` binary | +| runtime | API + binary + dist | + +## Runtime-контейнер + +| Путь | Содержимое | +|------|------------| +| `/usr/local/bin/DotsToSurface` | C++ Engine (CLI) | +| `/app/frontend/web/dist` | Vue frontend | +| `/app/backend` | FastAPI | + +**CMD:** `uvicorn main:app --host 0.0.0.0 --port 8080` (WORKDIR `/app/backend`) + +См. также: [../ARCHITECTURE.md](../ARCHITECTURE.md) diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index f802ab2..9808a00 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -1,17 +1,17 @@ services: - dotstosirface-web: + dottosurface-web: build: context: .. dockerfile: docker/Dockerfile ports: - "8080:8080" environment: - DOTSTOSIRFACE_BIN: /usr/local/bin/DotsToSirface + DOTSTOSURFACE_BIN: /usr/local/bin/DotsToSurface PIPELINE_CONFIG: /app/docker/default_pipeline.json USER_PRESETS_DIR: /app/data/user-presets volumes: - ../presets:/app/presets:ro - - dotstosirface-user-presets:/app/data/user-presets + - dottosurface-user-presets:/app/data/user-presets volumes: - dotstosirface-user-presets: + dottosurface-user-presets: diff --git a/web-vue/.gitignore b/frontend/web/.gitignore similarity index 100% rename from web-vue/.gitignore rename to frontend/web/.gitignore diff --git a/frontend/web/README.md b/frontend/web/README.md new file mode 100644 index 0000000..42e0d18 --- /dev/null +++ b/frontend/web/README.md @@ -0,0 +1,15 @@ +# Frontend (Web) + +**Слой:** Frontend — браузерный dashboard. Обработка точек — в C++ Engine через Backend. + +## Команды + +```bash +npm install +npm run dev # http://localhost:5173 +npm run build # → dist/ (обязательно перед запуском Backend без Docker) +``` + +API: [`src/api/client.js`](src/api/client.js). В dev Vite проксирует `/api` на `:8080` ([`vite.config.js`](vite.config.js)). + +См. также: [../../ARCHITECTURE.md](../../ARCHITECTURE.md) diff --git a/web-vue/index.html b/frontend/web/index.html similarity index 88% rename from web-vue/index.html rename to frontend/web/index.html index 77b8986..eb549a9 100644 --- a/web-vue/index.html +++ b/frontend/web/index.html @@ -3,7 +3,7 @@ - DotsToSirface + DotsToSurface
diff --git a/web-vue/package-lock.json b/frontend/web/package-lock.json similarity index 99% rename from web-vue/package-lock.json rename to frontend/web/package-lock.json index 796ffa2..fd09454 100644 --- a/web-vue/package-lock.json +++ b/frontend/web/package-lock.json @@ -1,11 +1,11 @@ { - "name": "dotstosirface-web", + "name": "dottosurface-web", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "dotstosirface-web", + "name": "dottosurface-web", "version": "1.0.0", "dependencies": { "@vueuse/core": "^11.3.0", diff --git a/web-vue/package.json b/frontend/web/package.json similarity index 93% rename from web-vue/package.json rename to frontend/web/package.json index 9d5969a..5bc1855 100644 --- a/web-vue/package.json +++ b/frontend/web/package.json @@ -1,5 +1,5 @@ { - "name": "dotstosirface-web", + "name": "dottosurface-web", "private": true, "version": "1.0.0", "type": "module", diff --git a/frontend/web/public/airplane_reference.png b/frontend/web/public/airplane_reference.png new file mode 100644 index 0000000000000000000000000000000000000000..19b357417a2f61f9d244d877848db82fa1d070e9 GIT binary patch literal 105091 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 +import { onMounted, ref } from "vue"; +import { RouterLink, RouterView, useRoute } from "vue-router"; +import { api } from "@/api/client"; +import { useTheme } from "@/composables/useTheme"; + +const route = useRoute(); +const { theme, toggleTheme } = useTheme(); +const healthText = ref("Checking API..."); + +onMounted(async () => { + try { + const health = await api.health(); + healthText.value = health.binaryExists === "True" || health.binaryExists === true + ? "API online, pipeline binary ready" + : "API online, binary missing"; + } catch (error) { + healthText.value = `API error: ${error.message}`; + } +}); + + + + + diff --git a/frontend/web/src/api/client.js b/frontend/web/src/api/client.js new file mode 100644 index 0000000..4581362 --- /dev/null +++ b/frontend/web/src/api/client.js @@ -0,0 +1,325 @@ +const API_BASE = ""; + +async function request(path, options = {}) { + const response = await fetch(`${API_BASE}${path}`, options); + const payload = await response.json().catch(() => ({})); + if (!response.ok) { + throw new Error(formatApiError(payload.detail, `Request failed: ${path}`)); + } + return payload; +} + +async function requestBlob(path, options = {}) { + const response = await fetch(`${API_BASE}${path}`, options); + if (!response.ok) { + const payload = await response.json().catch(() => ({})); + throw new Error(formatApiError(payload.detail, `Request failed: ${path}`)); + } + const blob = await response.blob(); + const disposition = response.headers.get("Content-Disposition") || ""; + const utfMatch = /filename\*\s*=\s*UTF-8''([^;]+)/i.exec(disposition); + const plainMatch = /filename="?([^";]+)"?/i.exec(disposition); + let filename = "cloud.xyz"; + if (utfMatch?.[1]) { + try { + filename = decodeURIComponent(utfMatch[1]); + } catch { + filename = utfMatch[1]; + } + } else if (plainMatch?.[1]) { + filename = plainMatch[1]; + } + return { blob, filename }; +} + +function triggerDownload(blob, filename) { + const url = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.download = filename || "cloud.xyz"; + link.rel = "noopener"; + link.style.display = "none"; + document.body.appendChild(link); + link.click(); + link.remove(); + setTimeout(() => URL.revokeObjectURL(url), 1500); +} + +function exportFilename(name, format) { + const allowed = new Set(["xyz", "ply", "obj", "npy"]); + const ext = allowed.has(format) ? format : "xyz"; + const base = String(name || "cloud") + .trim() + .replace(/\s+/g, "_") + .replace(/[^\w.\-]+/g, "_") + .replace(/_+/g, "_") + .replace(/^_|_$/g, "") + .toLowerCase() || "cloud"; + return base.toLowerCase().endsWith(`.${ext}`) ? base : `${base}.${ext}`; +} + +function formatApiError(detail, fallback) { + if (!detail) return fallback; + if (typeof detail === "string") return detail; + if (Array.isArray(detail)) { + return detail.map((item) => item.msg || JSON.stringify(item)).join("; "); + } + return String(detail); +} + +function pointsToXyz(points) { + return points.map((p) => `${p[0]} ${p[1]} ${p[2]}`).join("\n") + (points.length ? "\n" : ""); +} + +function pointsToPly(points) { + const header = [ + "ply", + "format ascii 1.0", + `element vertex ${points.length}`, + "property float x", + "property float y", + "property float z", + "end_header", + ].join("\n"); + const body = points.map((p) => `${p[0]} ${p[1]} ${p[2]}`).join("\n"); + return `${header}\n${body}${points.length ? "\n" : ""}`; +} + +function pointsToObj(points, objectName = "cloud") { + const safe = String(objectName || "cloud").replace(/[^\w\-]+/g, "_") || "cloud"; + const lines = [`# DotsToSurface point cloud (${points.length} vertices)`, `o ${safe}`]; + for (const p of points) { + lines.push(`v ${p[0]} ${p[1]} ${p[2]}`); + } + return `${lines.join("\n")}\n`; +} + +function parseObjPoints(text) { + const points = []; + for (const raw of String(text || "").split(/\r?\n/)) { + const line = raw.trim(); + if (!line || line.startsWith("#")) continue; + if (!/^v\s/i.test(line)) continue; + const parts = line.split(/\s+/); + if (parts.length < 4) continue; + const x = Number(parts[1]); + const y = Number(parts[2]); + const z = Number(parts[3]); + if (![x, y, z].every(Number.isFinite)) continue; + points.push([x, y, z]); + } + return points; +} + +function parseXyzPoints(text) { + const points = []; + for (const raw of String(text || "").split(/\r?\n/)) { + const line = raw.trim(); + if (!line || line.startsWith("#") || line.startsWith("//")) continue; + const parts = line.split(/[\s,;]+/).filter(Boolean); + if (parts.length < 3) continue; + const x = Number(parts[0]); + const y = Number(parts[1]); + const z = Number(parts[2]); + if (![x, y, z].every(Number.isFinite)) continue; + points.push([x, y, z]); + } + return points; +} + +function parsePlyPoints(text) { + const lines = String(text || "").split(/\r?\n/); + if (!lines.length || !/^ply\b/i.test(lines[0].trim())) { + return parseXyzPoints(text); + } + let i = 1; + let vertexCount = 0; + let format = "ascii"; + const props = []; + let inVertexElement = false; + for (; i < lines.length; i += 1) { + const line = lines[i].trim(); + if (!line) continue; + const lower = line.toLowerCase(); + if (lower.startsWith("format ")) { + format = lower.split(/\s+/)[1] || "ascii"; + continue; + } + if (lower.startsWith("element vertex")) { + inVertexElement = true; + vertexCount = Number(line.split(/\s+/)[2]) || 0; + props.length = 0; + continue; + } + if (lower.startsWith("element ")) { + inVertexElement = false; + continue; + } + if (inVertexElement && lower.startsWith("property ")) { + if (lower.includes("list")) continue; + const tokens = line.split(/\s+/); + props.push(tokens[tokens.length - 1].toLowerCase()); + continue; + } + if (/^end_header\b/i.test(lower)) { + i += 1; + break; + } + } + if (!format.startsWith("ascii")) { + throw new Error("Поддерживается только ASCII PLY (не binary)."); + } + const ix = props.indexOf("x"); + const iy = props.indexOf("y"); + const iz = props.indexOf("z"); + if (ix < 0 || iy < 0 || iz < 0) { + // Some PLY dumps put only xyz numbers after header without named props tracked — try sequential. + return parseXyzPoints(lines.slice(i).join("\n")); + } + const points = []; + const limit = vertexCount > 0 ? Math.min(lines.length, i + vertexCount) : lines.length; + for (; i < limit; i += 1) { + const line = lines[i].trim(); + if (!line) continue; + const parts = line.split(/\s+/); + const x = Number(parts[ix]); + const y = Number(parts[iy]); + const z = Number(parts[iz]); + if (![x, y, z].every(Number.isFinite)) continue; + points.push([x, y, z]); + } + return points; +} + +function detectCloudFormat(filename, text) { + const ext = String(filename || "").toLowerCase().split(".").pop(); + if (ext === "obj") return "obj"; + if (ext === "ply") return "ply"; + if (ext === "xyz" || ext === "txt" || ext === "csv") return "xyz"; + const head = String(text || "").slice(0, 200).trim().toLowerCase(); + if (head.startsWith("ply")) return "ply"; + if (/(^|\n)\s*v\s+[-+]?\d/i.test(String(text || "").slice(0, 2000))) return "obj"; + return "xyz"; +} + +function parseCloudPoints(text, filename = "") { + const format = detectCloudFormat(filename, text); + let points; + if (format === "obj") points = parseObjPoints(text); + else if (format === "ply") points = parsePlyPoints(text); + else points = parseXyzPoints(text); + return { format, points }; +} + +function downloadPointsLocally(points, format, filename) { + let text; + if (format === "ply") text = pointsToPly(points); + else if (format === "obj") text = pointsToObj(points, filename.replace(/\.\w+$/, "")); + else text = pointsToXyz(points); + const blob = new Blob([text], { type: "text/plain;charset=utf-8" }); + triggerDownload(blob, exportFilename(filename, format)); +} + +export const api = { + health: () => request("/api/health"), + catalog: () => request("/api/catalog"), + presets: () => request("/api/presets"), + builtinPresets: () => request("/api/builtin-presets"), + defaultConfig: () => request("/api/default-config"), + stageDefaults: (stageId) => request(`/api/stage-defaults/${encodeURIComponent(stageId)}`), + validateConfig: (config) => + request("/api/validate-config", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ config }), + }), + wizard: (wizardProfile, wizardGoal) => + request("/api/wizard", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ wizardProfile, wizardGoal }), + }), + demoTypes: () => request("/api/demo/types"), + demo: (surfaceType) => request(`/api/demo?surfaceType=${encodeURIComponent(surfaceType)}`), + userPresets: () => request("/api/user-presets"), + saveUserPreset: (preset) => + request("/api/user-presets", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(preset), + }), + runPipeline: ({ file, config, presetId, demoSurface, geometryFormat = "json" }) => { + const formData = new FormData(); + if (file) formData.append("file", file); + if (presetId) formData.append("preset_id", presetId); + if (config) formData.append("config_json", JSON.stringify(config)); + if (demoSurface) formData.append("demo_surface", demoSurface); + formData.append("geometry_format", geometryFormat); + return request("/api/run", { method: "POST", body: formData }); + }, + fetchGeometry: async (workId) => { + const response = await fetch(`/api/geometry/${workId}`); + if (!response.ok) throw new Error("Failed to load geometry"); + return response.arrayBuffer(); + }, + generatorCatalog: () => request("/api/generator/catalog"), + generatorLayer: (kind, type, params) => + request("/api/generator/layer", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ kind, type, params }), + }), + generatorResolveIntersections: (payload) => + request("/api/generator/resolve-intersections", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }), + generatorExport: async ({ points, layers, format = "xyz", filename, classLabel }) => { + const safeName = exportFilename(filename, format); + // .npy is binary PointNet format — always via backend. + if (format !== "npy" && Array.isArray(points) && points.length) { + downloadPointsLocally(points, format, safeName); + return; + } + const { blob, filename: suggested } = await requestBlob("/api/generator/export", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ points, layers, format, filename: safeName, classLabel }), + }); + triggerDownload(blob, safeName || suggested); + }, + datasetGenerate: ({ + count = 5, + seed = 42, + outputDir = "sonar_dataset", + objectScale = 1, + beamCount = 45, + lengthCount = 45, + modelFile, + } = {}) => { + if (!modelFile) { + return Promise.reject(new Error("Выберите файл модели .obj")); + } + const formData = new FormData(); + formData.append("count", String(count)); + formData.append("seed", String(seed)); + formData.append("outputDir", outputDir || "sonar_dataset"); + formData.append("objectScale", String(objectScale ?? 1)); + formData.append("beamCount", String(beamCount ?? 45)); + formData.append("lengthCount", String(lengthCount ?? 45)); + formData.append("model", modelFile, modelFile.name || "model.obj"); + return request("/api/dataset/generate", { + method: "POST", + body: formData, + }); + }, + datasetPreview: ({ stem, outputDir = "sonar_dataset", maxPoints = 25000 } = {}) => + request("/api/dataset/preview", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ stem, outputDir, maxPoints }), + }), +}; + +export { exportFilename, parseObjPoints, parseXyzPoints, parsePlyPoints, parseCloudPoints }; diff --git a/web-vue/src/catalog/pipelineUiCatalog.js b/frontend/web/src/catalog/pipelineUiCatalog.js similarity index 100% rename from web-vue/src/catalog/pipelineUiCatalog.js rename to frontend/web/src/catalog/pipelineUiCatalog.js diff --git a/web-vue/src/catalog/theme.js b/frontend/web/src/catalog/theme.js similarity index 100% rename from web-vue/src/catalog/theme.js rename to frontend/web/src/catalog/theme.js diff --git a/web-vue/src/components/PipelineDashboard.vue b/frontend/web/src/components/PipelineDashboard.vue similarity index 85% rename from web-vue/src/components/PipelineDashboard.vue rename to frontend/web/src/components/PipelineDashboard.vue index 9a12f30..e5128b7 100644 --- a/web-vue/src/components/PipelineDashboard.vue +++ b/frontend/web/src/components/PipelineDashboard.vue @@ -38,7 +38,7 @@ async function onGenerateDemo() { @@ -46,7 +46,12 @@ async function onGenerateDemo() {
- +
@@ -65,8 +70,8 @@ async function onGenerateDemo() {
- - + + -

Upload a PLY file and click Run.

- - - - -
-

3D Viewer

-
-

Drag to rotate, wheel to zoom. Points and mesh are shown after pipeline run.

-
- - - - - - diff --git a/web/style.css b/web/style.css deleted file mode 100644 index 539dc62..0000000 --- a/web/style.css +++ /dev/null @@ -1,154 +0,0 @@ -* { - box-sizing: border-box; -} - -body { - margin: 0; - font-family: Inter, Segoe UI, Roboto, sans-serif; - background: #0f1720; - color: #e8eef5; -} - -.header { - display: flex; - justify-content: space-between; - align-items: center; - gap: 16px; - padding: 20px 24px; - border-bottom: 1px solid #243447; - background: #152231; -} - -.header h1 { - margin: 0 0 4px; - font-size: 24px; -} - -.header p { - margin: 0; - color: #9db0c3; -} - -.health { - padding: 8px 12px; - border-radius: 8px; - background: #1f2f40; - font-size: 13px; -} - -.health.ok { - background: #173528; - color: #9be7b5; -} - -.health.error { - background: #3a1d24; - color: #ffb4c0; -} - -.layout { - display: grid; - grid-template-columns: 360px 1fr; - gap: 16px; - padding: 16px; - min-height: calc(100vh - 96px); -} - -.panel { - background: #152231; - border: 1px solid #243447; - border-radius: 12px; - padding: 16px; -} - -.panel h2 { - margin: 0 0 16px; - font-size: 18px; -} - -.field { - display: flex; - flex-direction: column; - gap: 8px; - margin-bottom: 14px; - font-size: 14px; -} - -.field input, -.field select, -button { - font: inherit; -} - -.field input, -.field select { - padding: 10px 12px; - border-radius: 8px; - border: 1px solid #31465d; - background: #0f1720; - color: inherit; -} - -button { - width: 100%; - padding: 12px 14px; - border: 0; - border-radius: 8px; - background: #3b82f6; - color: white; - cursor: pointer; - font-weight: 600; -} - -button:disabled { - opacity: 0.6; - cursor: wait; -} - -.status { - margin: 14px 0 0; - color: #9db0c3; - font-size: 14px; - line-height: 1.4; - white-space: pre-wrap; -} - -.metrics { - margin-top: 16px; - padding-top: 16px; - border-top: 1px solid #243447; - display: grid; - gap: 8px; - font-size: 14px; -} - -.hidden { - display: none; -} - -.viewer { - display: flex; - flex-direction: column; - min-height: 70vh; -} - -#viewport { - flex: 1; - min-height: 520px; - border-radius: 10px; - overflow: hidden; - border: 1px solid #243447; - background: #0b1118; -} - -.hint { - margin: 12px 0 0; - color: #7f93a8; - font-size: 13px; -} - -@media (max-width: 960px) { - .layout { - grid-template-columns: 1fr; - } -}