From 261e8a8b63e67c68aa9e4c7ed51deafe508d56a3 Mon Sep 17 00:00:00 2001 From: Iprok Date: Fri, 19 Sep 2025 19:02:51 +0300 Subject: [PATCH] =?UTF-8?q?=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20=D0=BE=D1=82=202025-09-19=20=D0=B4=D0=BB=D1=8F?= =?UTF-8?q?=20=D0=B2=D0=B5=D1=82=D0=BA=D0=B8=2019sepTest?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- COLLIDER_CONFIGURATION.md | 149 +++ package-lock.json | 196 ++++ public/colliders_city_1.json | 76 +- saves/game_time.json | 2 +- server.js | 51 + src/App.js | 11 + src/Game.js | 1244 ++++++++++++++++++++-- src/modules/CameraManager.js | 24 + src/modules/CollisionManager.js | 300 ++++++ src/modules/GameCore.js | 15 +- src/modules/PlayerManager.js | 63 +- src/modules/README.md | 23 + src/pages/EnhancedCollisionEditor.jsx | 1380 +++++++++++++++++++++++++ src/test-collision.js | 70 ++ test-advanced-collision-editor.js | 131 +++ test-camera-controls.js | 73 ++ test-collider-creation.js | 53 + test-collider-deletion-debug.js | 141 +++ test-colliders.js | 97 ++ test-collision-editor-fixes.js | 129 +++ test-collision.html | 405 ++++++++ test-duplicate-transform-fix.js | 135 +++ test-interior-api.html | 297 ++++++ 23 files changed, 4982 insertions(+), 83 deletions(-) create mode 100644 COLLIDER_CONFIGURATION.md create mode 100644 src/modules/CollisionManager.js create mode 100644 src/pages/EnhancedCollisionEditor.jsx create mode 100644 src/test-collision.js create mode 100644 test-advanced-collision-editor.js create mode 100644 test-camera-controls.js create mode 100644 test-collider-creation.js create mode 100644 test-collider-deletion-debug.js create mode 100644 test-colliders.js create mode 100644 test-collision-editor-fixes.js create mode 100644 test-collision.html create mode 100644 test-duplicate-transform-fix.js create mode 100644 test-interior-api.html diff --git a/COLLIDER_CONFIGURATION.md b/COLLIDER_CONFIGURATION.md new file mode 100644 index 0000000..60929cb --- /dev/null +++ b/COLLIDER_CONFIGURATION.md @@ -0,0 +1,149 @@ +# Конфигурация системы коллайдеров + +## Обзор изменений + +Система коллайдеров была обновлена для обеспечения полного покрытия объектов коллизиями. Теперь коллайдеры создаются с увеличенными размерами, чтобы покрывать весь объект, а не только его часть. + +## Основные изменения + +### 1. Конфигурация коллайдеров +```javascript +const COLLIDER_CONFIG = { + sizeMultiplier: 2.0, // Коэффициент увеличения размеров (100% больше) + debugMode: false, // Режим отладки для визуализации + minSize: 0.5, // Минимальный размер коллайдера + maxSize: 50.0, // Максимальный размер коллайдера + adaptiveScaling: true // Адаптивное масштабирование +}; +``` + +### 2. Обновленная функция создания коллайдеров +- `createVisualColliderFromModel()` теперь использует увеличенные размеры для JSON данных +- Приоритет отдается реальным размерам мешей из 3D модели +- Fallback к JSON данным с коэффициентом увеличения + +### 3. Адаптивное масштабирование +Система автоматически определяет оптимальный коэффициент масштабирования на основе размеров объекта: +- **Маленькие объекты** (< 1.0): коэффициент 3.0x или больше +- **Средние объекты** (1.0-5.0): стандартный коэффициент 2.0x +- **Большие объекты** (> 5.0): уменьшенный коэффициент 1.6x + +### 4. Динамическая настройка +Добавлены расширенные функции для настройки коллайдеров: + +```javascript +// Изменить коэффициент размера коллайдеров +window.updateColliderSize(3.0); // Увеличить на 200% + +// Переключить адаптивное масштабирование +window.toggleAdaptiveScaling(); + +// Установить ограничения размеров +window.setColliderLimits(1.0, 20.0); // мин: 1.0, макс: 20.0 + +// Переключить режим отладки +window.toggleColliderDebug(); + +// Получить текущую конфигурацию +console.log(window.colliderConfig); + +// Протестировать коллизии +window.testCollisions(); + +// Быстрый тест всех функций +window.quickTest(); + +// Управление цветом и прозрачностью коллайдеров +window.setColliderColor(0, 1, 0); // Зеленый цвет (RGB 0-1) +window.setColliderOpacity(0.5); // Полупрозрачность (0-1) +window.randomizeColliderColors(); // Случайные цвета + +// Управление цветом и прозрачностью объектов интерьера +window.setInteriorObjectColor(1, 0, 0); // Красный цвет объектов (RGB 0-1) +window.setInteriorObjectOpacity(0.7); // Полупрозрачность объектов (0-1) +``` + +## Использование + +### Базовое использование +Коллайдеры автоматически загружаются из `colliders_city_1.json` с увеличенными размерами. + +### Настройка размеров +Если коллайдеры слишком большие или маленькие, можно изменить коэффициент: + +```javascript +// В консоли браузера +window.updateColliderSize(1.0); // Оригинальные размеры +window.updateColliderSize(1.2); // +20% (по умолчанию) +window.updateColliderSize(1.5); // +50% +``` + +### Отладка +Для визуализации коллайдеров включите режим отладки: +```javascript +window.colliderConfig.debugMode = true; +``` + +## Технические детали + +### Алгоритм создания коллайдеров +1. Поиск соответствующего меша в 3D модели по позиции +2. Если меш найден - использование его реальных размеров +3. Если меш не найден - использование JSON данных с коэффициентом увеличения +4. Создание Box3 для проверки коллизий +5. Создание визуального представления + +### Проверка коллизий +- Используется AABB (Axis-Aligned Bounding Box) для эффективной проверки +- Игрок представлен как цилиндр с радиусом 0.35 и высотой 1.6 +- Проверка пересечения Box3 объектов + +## Файлы конфигурации + +### colliders_city_1.json +```json +{ + "colliders": [ + { + "type": "box", + "position": { "x": 0, "y": 0, "z": 0 }, + "rotation": { "x": 0, "y": 0, "z": 0 }, + "scale": { "x": 2, "y": 2, "z": 2 }, + "color": { "r": 1.0, "g": 0.0, "b": 0.0 }, + "opacity": 0.3 + } + ] +} +``` + +**Параметры:** +- `position`, `rotation`, `scale` - стандартные параметры трансформации +- `color` - RGB цвет в диапазоне 0.0-1.0 (опционально) +- `opacity` - прозрачность в диапазоне 0.0-1.0 (опционально) + +Размеры в JSON будут автоматически увеличены на коэффициент `sizeMultiplier`. + +## Рекомендации + +1. **Начальная настройка**: Используйте коэффициент 1.2 (20% увеличение) +2. **Тонкая настройка**: Тестируйте разные значения от 1.0 до 1.5 +3. **Отладка**: Включите `debugMode` для визуальной проверки коллайдеров +4. **Производительность**: Большие коэффициенты могут снизить производительность + +## Устранение неполадок + +### Коллайдеры слишком маленькие +```javascript +window.updateColliderSize(1.3); // Увеличить коэффициент +``` + +### Коллайдеры слишком большие +```javascript +window.updateColliderSize(1.1); // Уменьшить коэффициент +``` + +### Проблемы с производительностью +```javascript +window.updateColliderSize(1.0); // Вернуться к оригинальным размерам +``` + diff --git a/package-lock.json b/package-lock.json index b392587..76f3f35 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@readyplayerme/visage": "^6.10.0", "bcrypt": "^5.1.1", "compression": "^1.7.4", + "concurrently": "^8.2.2", "dotenv": "^16.5.0", "express": "^5.1.0", "jsonwebtoken": "^9.0.2", @@ -5841,6 +5842,84 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, + "node_modules/concurrently": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.2.tgz", + "integrity": "sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==", + "dependencies": { + "chalk": "^4.1.2", + "date-fns": "^2.30.0", + "lodash": "^4.17.21", + "rxjs": "^7.8.1", + "shell-quote": "^1.8.1", + "spawn-command": "0.0.2", + "supports-color": "^8.1.1", + "tree-kill": "^1.2.2", + "yargs": "^17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": "^14.13.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/concurrently/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/concurrently/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "engines": { + "node": ">=12" + } + }, "node_modules/confusing-browser-globals": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", @@ -6446,6 +6525,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, "node_modules/debug": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", @@ -14618,6 +14712,14 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/safe-array-concat": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", @@ -15417,6 +15519,11 @@ "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", "deprecated": "Please use @jridgewell/sourcemap-codec instead" }, + "node_modules/spawn-command": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz", + "integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==" + }, "node_modules/spdy": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", @@ -16479,6 +16586,14 @@ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "bin": { + "tree-kill": "cli.js" + } + }, "node_modules/troika-three-text": { "version": "0.49.1", "resolved": "https://registry.npmjs.org/troika-three-text/-/troika-three-text-0.49.1.tgz", @@ -22270,6 +22385,61 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, + "concurrently": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.2.tgz", + "integrity": "sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==", + "requires": { + "chalk": "^4.1.2", + "date-fns": "^2.30.0", + "lodash": "^4.17.21", + "rxjs": "^7.8.1", + "shell-quote": "^1.8.1", + "spawn-command": "0.0.2", + "supports-color": "^8.1.1", + "tree-kill": "^1.2.2", + "yargs": "^17.7.2" + }, + "dependencies": { + "cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + } + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "requires": { + "has-flag": "^4.0.0" + } + }, + "yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "requires": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + } + }, + "yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==" + } + } + }, "confusing-browser-globals": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", @@ -22667,6 +22837,14 @@ "is-data-view": "^1.0.1" } }, + "date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "requires": { + "@babel/runtime": "^7.21.0" + } + }, "debug": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", @@ -28343,6 +28521,14 @@ "queue-microtask": "^1.2.2" } }, + "rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "requires": { + "tslib": "^2.1.0" + } + }, "safe-array-concat": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", @@ -28904,6 +29090,11 @@ "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==" }, + "spawn-command": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz", + "integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==" + }, "spdy": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", @@ -29704,6 +29895,11 @@ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, + "tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==" + }, "troika-three-text": { "version": "0.49.1", "resolved": "https://registry.npmjs.org/troika-three-text/-/troika-three-text-0.49.1.tgz", diff --git a/public/colliders_city_1.json b/public/colliders_city_1.json index 176eb0f..b47bfc3 100644 --- a/public/colliders_city_1.json +++ b/public/colliders_city_1.json @@ -16,7 +16,81 @@ "x": 3.8820412781839853, "y": 1.9275391013076184, "z": 0.020423580261430187 - } + }, + "color": { + "r": 0.2, + "g": 1, + "b": 0.1 + }, + "opacity": 1 + }, + { + "type": "box", + "position": { + "x": -19.682621880668748, + "y": -98.44103033988638, + "z": -71.00143351764237 + }, + "rotation": { + "x": 0, + "y": 0, + "z": 0 + }, + "scale": { + "x": 0.021, + "y": 1.9, + "z": 3.8 + }, + "color": { + "r": 0.2, + "g": 1, + "b": 0.1 + }, + "opacity": 1 + }, + { + "type": "box", + "position": { + "x": -16.32989632517387, + "y": -98.91158756288065, + "z": -66.6280016541835 + }, + "rotation": { + "x": 0, + "y": 0, + "z": 0 + }, + "scale": { + "x": 1, + "y": 2, + "z": 1 + }, + "color": { + "r": 0.2, + "g": 1, + "b": 0.1 + }, + "opacity": 1 + }, + { + "type": "box", + "position": { + "x": 3.1476200534247045, + "y": -0.5512983469988768, + "z": -0.4395577458346196 + }, + "rotation": { + "x": 0, + "y": 0, + "z": 0 + }, + "scale": { + "x": 0.5301919909798507, + "y": 2.40369930235243007, + "z": 1.2663514332721553 + }, + "color": {}, + "opacity": 0.3 } ] } \ No newline at end of file diff --git a/saves/game_time.json b/saves/game_time.json index b96b79a..4f6c85d 100644 --- a/saves/game_time.json +++ b/saves/game_time.json @@ -1 +1 @@ -{"time":"2025-08-08T00:17:13.976Z","lastReal":1757399069805} \ No newline at end of file +{"time":"2025-10-30T05:20:24.552Z","lastReal":1758297743627} \ No newline at end of file diff --git a/server.js b/server.js index 0d4b618..46d9fb0 100644 --- a/server.js +++ b/server.js @@ -1666,6 +1666,57 @@ app.get('/api/cities', authenticate, async (req, res) => { } }); +// API endpoint для сохранения коллайдеров города +app.post('/api/colliders/city/:cityId', authenticate, async (req, res) => { + const cityId = req.params.cityId; + const collidersData = req.body; + + try { + // Сохраняем коллайдеры в JSON файл + const fileName = `colliders_city_${cityId}.json`; + const filePath = pathLib.join(__dirname, 'public', fileName); + + // Создаем директорию если не существует + await fs.promises.mkdir(pathLib.dirname(filePath), { recursive: true }); + + // Записываем данные в файл + await fs.promises.writeFile(filePath, JSON.stringify(collidersData, null, 2), 'utf8'); + + console.log(`Коллайдеры для города ${cityId} сохранены в ${fileName}`); + res.json({ success: true, message: 'Коллайдеры сохранены успешно' }); + } catch (error) { + console.error('Ошибка сохранения коллайдеров:', error); + res.status(500).json({ error: 'Ошибка сохранения коллайдеров' }); + } +}); + +// API endpoint для получения коллайдеров города +app.get('/api/colliders/city/:cityId', authenticate, async (req, res) => { + const cityId = req.params.cityId; + + try { + const fileName = `colliders_city_${cityId}.json`; + const filePath = pathLib.join(__dirname, 'public', fileName); + + // Проверяем существование файла + try { + await fs.promises.access(filePath); + } catch (error) { + // Файл не существует, возвращаем пустой массив + return res.json({ colliders: [] }); + } + + // Читаем файл + const fileContent = await fs.promises.readFile(filePath, 'utf8'); + const data = JSON.parse(fileContent); + + res.json(data); + } catch (error) { + console.error('Ошибка чтения коллайдеров:', error); + res.status(500).json({ error: 'Ошибка чтения коллайдеров' }); + } +}); + app.use((req, res) => { res.sendFile(pathLib.join(__dirname, 'build', 'index.html')); }); diff --git a/src/App.js b/src/App.js index 645ccd1..b4e0114 100644 --- a/src/App.js +++ b/src/App.js @@ -11,6 +11,7 @@ import RequireProfile from './components/RequireProfile'; import MapEditor from './pages/MapEditor'; import InteriorEditor from './pages/InteriorEditor'; import CollisionEditor from './pages/CollisionEditor'; +import EnhancedCollisionEditor from './pages/EnhancedCollisionEditor'; export default function App() { const [isAuth, setIsAuth] = useState(!!localStorage.getItem('token')); @@ -89,6 +90,16 @@ export default function App() { : } /> + + + + : + } + /> {/* всё остальное */} } /> diff --git a/src/Game.js b/src/Game.js index 8a84da6..a65f7f5 100644 --- a/src/Game.js +++ b/src/Game.js @@ -28,8 +28,13 @@ function Game({ avatarUrl, gender }) { // 3) реф для группы «интерьера» const interiorGroupRef = useRef(null); const interiorCollidersRef = useRef([]); + const interiorColliderBoxesRef = useRef([]); + const jsonCollidersRef = useRef([]); + const visualCollidersRef = useRef([]); const interiorExitPosRef = useRef(null); const fpHiddenNodesRef = useRef([]); + const interiorDebugEnabledRef = useRef(false); + const interiorDebugHelpersRef = useRef([]); const cleanupTimerRef = useRef(null); // Глобальный менеджер прогресса загрузки (используем в GLTFLoader) const loadingManagerRef = useRef(null); @@ -49,6 +54,15 @@ function Game({ avatarUrl, gender }) { const isInInteriorRef = useRef(false); const altHeldRef = useRef(false); const LOAD_RADIUS = 120; + + // Конфигурация коллайдеров + const COLLIDER_CONFIG = { + sizeMultiplier: 2.0, // Коэффициент увеличения размеров для полного покрытия объекта + debugMode: false, // Режим отладки для визуализации коллайдеров + minSize: 0.5, // Минимальный размер коллайдера + maxSize: 50.0, // Максимальный размер коллайдера + adaptiveScaling: true // Адаптивное масштабирование на основе размеров объекта + }; const [activeApp, setActiveApp] = useState(null); @@ -536,8 +550,437 @@ function Game({ avatarUrl, gender }) { } // Загружаем модель интерьера - console.log('Загружаем модель интерьера'); + console.log('Загружаем модель интерьера для ID:', interiorId); + try { await loadInteriorModel(interiorId); + console.log('loadInteriorModel завершена успешно'); + } catch (error) { + console.error('Ошибка в loadInteriorModel:', error); + } + + // Загружаем коллизионные данные из JSON + console.log('Загружаем коллизионные данные из JSON...'); + try { + const jsonColliders = await loadCollidersFromJSON(1); // Пока используем город 1 + console.log('🔍 Результат loadCollidersFromJSON:', jsonColliders); + jsonCollidersRef.current = jsonColliders; + console.log('🔍 jsonCollidersRef.current установлен:', jsonCollidersRef.current?.length || 0, 'объектов'); + console.log('Коллизионные данные загружены:', jsonColliders.length, 'объектов'); + + // Автоматически применяем цвета и прозрачность из JSON к объектам + console.log('🎨 Автоматически применяем цвета из JSON к объектам...'); + setTimeout(() => { + if (window.applyJsonColorsToObjects) { + window.applyJsonColorsToObjects(); + } + }, 100); // Небольшая задержка для завершения загрузки объектов + + // Добавляем визуальные коллайдеры в сцену + const visualColliders = jsonColliders.map(collider => collider.visual); + visualCollidersRef.current = visualColliders; + + visualColliders.forEach(collider => { + if (sceneRef.current) { + sceneRef.current.add(collider); + console.log('Добавлен визуальный коллайдер в сцену'); + } + }); + + // Добавляем функции для настройки коллайдеров в глобальную область + window.colliderConfig = COLLIDER_CONFIG; + window.updateColliderSize = (multiplier) => { + COLLIDER_CONFIG.sizeMultiplier = multiplier; + console.log('🔧 Обновлен коэффициент размера коллайдеров:', multiplier); + reloadColliders(); + }; + + window.toggleAdaptiveScaling = () => { + COLLIDER_CONFIG.adaptiveScaling = !COLLIDER_CONFIG.adaptiveScaling; + console.log('🔧 Адаптивное масштабирование:', COLLIDER_CONFIG.adaptiveScaling ? 'включено' : 'выключено'); + reloadColliders(); + }; + + window.setColliderLimits = (minSize, maxSize) => { + COLLIDER_CONFIG.minSize = minSize; + COLLIDER_CONFIG.maxSize = maxSize; + console.log('🔧 Установлены ограничения размеров:', { minSize, maxSize }); + reloadColliders(); + }; + + window.toggleColliderDebug = () => { + COLLIDER_CONFIG.debugMode = !COLLIDER_CONFIG.debugMode; + console.log('🔧 Режим отладки коллайдеров:', COLLIDER_CONFIG.debugMode ? 'включен' : 'выключен'); + // Обновляем видимость визуальных коллайдеров + visualCollidersRef.current.forEach(collider => { + collider.visible = COLLIDER_CONFIG.debugMode; + }); + }; + + window.setColliderColor = (r, g, b) => { + console.log('🎨 Устанавливаем цвет коллайдеров:', { r, g, b }); + visualCollidersRef.current.forEach(collider => { + if (collider.material) { + const color = (Math.floor(r * 255) << 16) | (Math.floor(g * 255) << 8) | Math.floor(b * 255); + collider.material.color.setHex(color); + } + }); + }; + + window.setColliderOpacity = (opacity) => { + const clampedOpacity = Math.max(0, Math.min(1, opacity)); + console.log('👁️ Устанавливаем прозрачность коллайдеров:', clampedOpacity); + visualCollidersRef.current.forEach(collider => { + if (collider.material) { + collider.material.opacity = clampedOpacity; + } + }); + }; + + window.randomizeColliderColors = () => { + console.log('🌈 Случайные цвета для коллайдеров'); + visualCollidersRef.current.forEach(collider => { + if (collider.material) { + const r = Math.random(); + const g = Math.random(); + const b = Math.random(); + const color = (Math.floor(r * 255) << 16) | (Math.floor(g * 255) << 8) | Math.floor(b * 255); + collider.material.color.setHex(color); + } + }); + }; + + window.setInteriorObjectColor = (r, g, b) => { + console.log('🎨 Устанавливаем цвет объектов интерьера:', { r, g, b }); + const color = (Math.floor(r * 255) << 16) | (Math.floor(g * 255) << 8) | Math.floor(b * 255); + + console.log('🔍 Проверяем interiorGroupRef.current:', interiorGroupRef.current); + console.log('🔍 Проверяем sceneRef.current:', sceneRef.current); + + // Ищем группу интерьера в сцене + let interiorGroup = interiorGroupRef.current; + if (!interiorGroup && sceneRef.current) { + interiorGroup = sceneRef.current.getObjectByName('interiorGroup'); + console.log('🔍 Найдена группа интерьера по имени:', interiorGroup); + } + + if (!interiorGroup) { + console.warn('⚠️ Группа интерьера не найдена!'); + return; + } + + let meshCount = 0; + let materialCount = 0; + + // Применяем цвет ко всем объектам интерьера + interiorGroup.traverse((child) => { + if (child.isMesh && child.material) { + meshCount++; + console.log('🔍 Обрабатываем меш:', child.name || 'unnamed', 'материал:', child.material); + + if (Array.isArray(child.material)) { + child.material.forEach(mat => { + if (mat) { + materialCount++; + console.log('🔍 Изменяем материал массива:', mat); + mat.color.setHex(color); + mat.needsUpdate = true; + } + }); + } else { + materialCount++; + console.log('🔍 Изменяем материал:', child.material); + child.material.color.setHex(color); + child.material.needsUpdate = true; + } + } + }); + + console.log(`✅ Обработано мешей: ${meshCount}, материалов: ${materialCount}`); + }; + + window.debugInteriorObjects = () => { + console.log('🔍 Диагностика объектов интерьера:'); + console.log('interiorGroupRef.current:', interiorGroupRef.current); + console.log('sceneRef.current:', sceneRef.current); + + if (sceneRef.current) { + console.log('Все объекты в сцене:'); + sceneRef.current.traverse((child) => { + if (child.isMesh) { + console.log('Меш:', child.name || 'unnamed', 'позиция:', child.position, 'материал:', child.material); + } else if (child.isGroup) { + console.log('Группа:', child.name || 'unnamed', 'дети:', child.children.length); + } + }); + } + + // Ищем группу интерьера + if (sceneRef.current) { + const interiorGroup = sceneRef.current.getObjectByName('interiorGroup'); + console.log('Группа интерьера найдена:', interiorGroup); + + if (interiorGroup) { + console.log('Объекты в группе интерьера:'); + interiorGroup.traverse((child) => { + if (child.isMesh) { + console.log('Меш в интерьере:', child.name || 'unnamed', 'материал:', child.material); + } + }); + } + } + }; + + window.setColliderObjectsColor = (r, g, b) => { + console.log('🎨 Устанавливаем цвет только объектов из JSON коллайдеров:', { r, g, b }); + const color = (Math.floor(r * 255) << 16) | (Math.floor(g * 255) << 8) | Math.floor(b * 255); + + if (!jsonCollidersRef.current || jsonCollidersRef.current.length === 0) { + console.warn('⚠️ JSON коллайдеры не найдены!'); + return; + } + + if (!sceneRef.current) { + console.warn('⚠️ Сцена не найдена!'); + return; + } + + let processedCount = 0; + + // Проходим по всем коллайдерам из JSON + jsonCollidersRef.current.forEach((colliderData, index) => { + const colliderPos = colliderData.data.position; + console.log(`🔍 Ищем объект для коллайдера ${index} в позиции:`, colliderPos); + + // Ищем объекты в сцене, которые находятся рядом с позицией коллайдера + sceneRef.current.traverse((child) => { + if (child.isMesh && child.material) { + const distance = Math.sqrt( + Math.pow(child.position.x - colliderPos.x, 2) + + Math.pow(child.position.y - colliderPos.y, 2) + + Math.pow(child.position.z - colliderPos.z, 2) + ); + + // Если объект находится в радиусе 2 единиц от коллайдера + if (distance < 2.0) { + console.log(`🎯 Найден объект для коллайдера ${index}:`, child.name || 'unnamed', 'расстояние:', distance); + + if (Array.isArray(child.material)) { + child.material.forEach(mat => { + if (mat) { + mat.color.setHex(color); + mat.needsUpdate = true; + } + }); + } else { + child.material.color.setHex(color); + child.material.needsUpdate = true; + } + + processedCount++; + } + } + }); + }); + + console.log(`✅ Обработано объектов: ${processedCount}`); + }; + + window.applyJsonColorsToObjects = () => { + console.log('🎨 Применяем цвета и прозрачность из JSON к объектам в сцене'); + + if (!jsonCollidersRef.current || jsonCollidersRef.current.length === 0) { + console.warn('⚠️ JSON коллайдеры не найдены!'); + return; + } + + if (!sceneRef.current) { + console.warn('⚠️ Сцена не найдена!'); + return; + } + + let processedCount = 0; + + // Проходим по всем коллайдерам из JSON + jsonCollidersRef.current.forEach((colliderData, index) => { + const colliderPos = colliderData.data.position; + const colliderData_obj = colliderData.data; + + console.log(`🔍 Применяем настройки коллайдера ${index}:`, colliderData_obj); + + // Определяем цвет и прозрачность из JSON данных + let color = 0xffffff; // Белый по умолчанию + let opacity = 1.0; // Полная непрозрачность по умолчанию + + if (colliderData_obj.color) { + const r = Math.floor((colliderData_obj.color.r || 1.0) * 255); + const g = Math.floor((colliderData_obj.color.g || 1.0) * 255); + const b = Math.floor((colliderData_obj.color.b || 1.0) * 255); + color = (r << 16) | (g << 8) | b; + } + + if (colliderData_obj.opacity !== undefined) { + opacity = Math.max(0, Math.min(1, colliderData_obj.opacity)); + } + + console.log(`🎨 Применяем цвет ${color.toString(16)} и прозрачность ${opacity} для коллайдера ${index}`); + + // Ищем объекты в сцене, которые находятся рядом с позицией коллайдера + sceneRef.current.traverse((child) => { + if (child.isMesh && child.material) { + const distance = Math.sqrt( + Math.pow(child.position.x - colliderPos.x, 2) + + Math.pow(child.position.y - colliderPos.y, 2) + + Math.pow(child.position.z - colliderPos.z, 2) + ); + + // Если объект находится в радиусе 2 единиц от коллайдера + if (distance < 2.0) { + console.log(`🎯 Найден объект для коллайдера ${index}:`, child.name || 'unnamed', 'расстояние:', distance); + + if (Array.isArray(child.material)) { + child.material.forEach(mat => { + if (mat) { + mat.color.setHex(color); + mat.transparent = opacity < 1.0; + mat.opacity = opacity; + mat.needsUpdate = true; + } + }); + } else { + child.material.color.setHex(color); + child.material.transparent = opacity < 1.0; + child.material.opacity = opacity; + child.material.needsUpdate = true; + } + + processedCount++; + } + } + }); + }); + + console.log(`✅ Применены настройки к ${processedCount} объектам`); + }; + + window.setAllObjectsColor = (r, g, b) => { + console.log('🎨 Устанавливаем цвет ВСЕХ объектов в сцене:', { r, g, b }); + const color = (Math.floor(r * 255) << 16) | (Math.floor(g * 255) << 8) | Math.floor(b * 255); + + if (!sceneRef.current) { + console.warn('⚠️ Сцена не найдена!'); + return; + } + + let meshCount = 0; + let materialCount = 0; + + // Применяем цвет ко всем объектам в сцене + sceneRef.current.traverse((child) => { + if (child.isMesh && child.material) { + meshCount++; + console.log('🔍 Обрабатываем меш:', child.name || 'unnamed'); + + if (Array.isArray(child.material)) { + child.material.forEach(mat => { + if (mat) { + materialCount++; + mat.color.setHex(color); + mat.needsUpdate = true; + } + }); + } else { + materialCount++; + child.material.color.setHex(color); + child.material.needsUpdate = true; + } + } + }); + + console.log(`✅ Обработано мешей: ${meshCount}, материалов: ${materialCount}`); + }; + + window.setInteriorObjectOpacity = (opacity) => { + const clampedOpacity = Math.max(0, Math.min(1, opacity)); + console.log('👁️ Устанавливаем прозрачность объектов интерьера:', clampedOpacity); + + // Применяем прозрачность ко всем объектам интерьера + if (interiorGroupRef.current) { + interiorGroupRef.current.traverse((child) => { + if (child.isMesh && child.material) { + if (Array.isArray(child.material)) { + child.material.forEach(mat => { + if (mat) { + mat.transparent = clampedOpacity < 1.0; + mat.opacity = clampedOpacity; + mat.needsUpdate = true; + } + }); + } else { + child.material.transparent = clampedOpacity < 1.0; + child.material.opacity = clampedOpacity; + child.material.needsUpdate = true; + } + } + }); + } + }; + + const reloadColliders = () => { + loadCollidersFromJSON(1).then(newColliders => { + // Удаляем старые визуальные коллайдеры + visualCollidersRef.current.forEach(collider => { + if (sceneRef.current) { + sceneRef.current.remove(collider); + } + }); + + // Обновляем данные + jsonCollidersRef.current = newColliders; + visualCollidersRef.current = newColliders.map(collider => collider.visual); + + // Добавляем новые визуальные коллайдеры + visualCollidersRef.current.forEach(collider => { + if (sceneRef.current) { + sceneRef.current.add(collider); + collider.visible = COLLIDER_CONFIG.debugMode; + } + }); + + console.log('✅ Коллайдеры перезагружены с новыми настройками'); + }); + }; + + window.testCollisions = () => { + console.log('🧪 Тестируем коллизии:'); + console.log('JSON коллайдеров в ref:', jsonCollidersRef.current?.length || 0); + console.log('JSON коллайдеров в переменной:', jsonColliders.length); + console.log('Позиция игрока:', playerRef.current?.position); + console.log('jsonCollidersRef.current:', jsonCollidersRef.current); + + if (jsonCollidersRef.current && jsonCollidersRef.current.length > 0) { + const testPos = playerRef.current?.position || new THREE.Vector3(-13.2, -100, -69.3); + const playerBox = new THREE.Box3(); + const playerRadius = 0.4; + const playerHeight = 1.6; + + playerBox.setFromPoints([ + new THREE.Vector3(testPos.x - playerRadius, testPos.y, testPos.z - playerRadius), + new THREE.Vector3(testPos.x + playerRadius, testPos.y + playerHeight, testPos.z + playerRadius) + ]); + + console.log('Player box:', playerBox.min, '->', playerBox.max); + + jsonCollidersRef.current.forEach((collider, i) => { + console.log(`Коллайдер ${i}:`, collider.box.min, '->', collider.box.max); + const intersects = playerBox.intersectsBox(collider.box); + console.log(`Пересекается: ${intersects}`); + }); + } + }; + + } catch (error) { + console.error('Ошибка загрузки коллизионных данных:', error); + } // Переключаемся на камеру от первого лица console.log('Переключаемся на камеру от первого лица'); @@ -550,6 +993,7 @@ function Game({ avatarUrl, gender }) { // Устанавливаем состояние "в интерьере" console.log('Устанавливаем setIsInInterior(true)'); setIsInInterior(true); + isInInteriorRef.current = true; // Важно! Устанавливаем ref для системы коллизий setSelectedHouse(null); console.log('isInInterior установлен в true'); @@ -651,17 +1095,422 @@ function Game({ avatarUrl, gender }) { } }; + // Функция для создания визуального коллайдера + const createVisualCollider = (colliderData, index) => { + const geometry = new THREE.BoxGeometry( + colliderData.scale.x, + colliderData.scale.y, + colliderData.scale.z + ); + + const material = new THREE.MeshBasicMaterial({ + color: 0xff0000, // Красный цвет + transparent: true, + opacity: 0.3, // Полупрозрачность + wireframe: false + }); + + const mesh = new THREE.Mesh(geometry, material); + mesh.position.set( + colliderData.position.x, + colliderData.position.y, + colliderData.position.z + ); + mesh.rotation.set( + colliderData.rotation.x, + colliderData.rotation.y, + colliderData.rotation.z + ); + + // Добавляем метку для отладки + mesh.userData = { + isCollider: true, + colliderIndex: index, + originalData: colliderData + }; + + return mesh; + }; + + // Создаем визуальный коллайдер на основе реальных размеров мешей из модели + const createVisualColliderFromModel = (colliderData, index) => { + // Находим соответствующий меш в модели интерьера + let targetMesh = null; + if (interiorCollidersRef.current && interiorCollidersRef.current.length > 0) { + // Ищем меш, который ближе всего к позиции коллайдера + const targetPos = new THREE.Vector3( + colliderData.position.x, + colliderData.position.y, + colliderData.position.z + ); + + let minDistance = Infinity; + for (const mesh of interiorCollidersRef.current) { + if (!mesh.geometry) continue; + + // Получаем реальные размеры меша + const box = new THREE.Box3().setFromObject(mesh); + const center = new THREE.Vector3(); + box.getCenter(center); + + const distance = targetPos.distanceTo(center); + if (distance < minDistance) { + minDistance = distance; + targetMesh = mesh; + } + } + } + + let geometry, position, rotation, scale; + + if (targetMesh) { + // Используем реальные размеры меша + const box = new THREE.Box3().setFromObject(targetMesh); + const size = new THREE.Vector3(); + const center = new THREE.Vector3(); + box.getSize(size); + box.getCenter(center); + + geometry = new THREE.BoxGeometry(size.x, size.y, size.z); + position = center; + rotation = targetMesh.rotation; + scale = targetMesh.scale; + + console.log('🎯 Используем реальные размеры меша:', { + size: size, + center: center, + rotation: rotation, + scale: scale + }); + } else { + // Используем JSON данные с умным масштабированием + let adjustedSize = new THREE.Vector3( + colliderData.scale.x, + colliderData.scale.y, + colliderData.scale.z + ); + + if (COLLIDER_CONFIG.adaptiveScaling) { + // Адаптивное масштабирование на основе размеров объекта + const avgSize = (adjustedSize.x + adjustedSize.y + adjustedSize.z) / 3; + + if (avgSize < 1.0) { + // Для маленьких объектов используем больший коэффициент + const adaptiveMultiplier = Math.max(COLLIDER_CONFIG.sizeMultiplier, 3.0); + adjustedSize.multiplyScalar(adaptiveMultiplier); + } else if (avgSize < 5.0) { + // Для средних объектов используем стандартный коэффициент + adjustedSize.multiplyScalar(COLLIDER_CONFIG.sizeMultiplier); + } else { + // Для больших объектов используем меньший коэффициент + adjustedSize.multiplyScalar(Math.max(COLLIDER_CONFIG.sizeMultiplier * 0.8, 1.5)); + } + + // Применяем минимальные и максимальные ограничения + adjustedSize.x = Math.max(Math.min(adjustedSize.x, COLLIDER_CONFIG.maxSize), COLLIDER_CONFIG.minSize); + adjustedSize.y = Math.max(Math.min(adjustedSize.y, COLLIDER_CONFIG.maxSize), COLLIDER_CONFIG.minSize); + adjustedSize.z = Math.max(Math.min(adjustedSize.z, COLLIDER_CONFIG.maxSize), COLLIDER_CONFIG.minSize); + } else { + // Простое масштабирование + adjustedSize.multiplyScalar(COLLIDER_CONFIG.sizeMultiplier); + } + + geometry = new THREE.BoxGeometry(adjustedSize.x, adjustedSize.y, adjustedSize.z); + position = new THREE.Vector3( + colliderData.position.x, + colliderData.position.y, + colliderData.position.z + ); + rotation = new THREE.Euler( + colliderData.rotation.x, + colliderData.rotation.y, + colliderData.rotation.z + ); + scale = new THREE.Vector3(1, 1, 1); + + console.log('⚠️ Используем JSON данные с адаптивными размерами для коллайдера', index, { + originalSize: colliderData.scale, + adjustedSize: adjustedSize, + avgOriginalSize: (colliderData.scale.x + colliderData.scale.y + colliderData.scale.z) / 3 + }); + } + + // Определяем цвет и прозрачность из JSON данных или используем значения по умолчанию + let color = 0xff0000; // Красный по умолчанию + let opacity = 0.3; // Прозрачность по умолчанию + + if (colliderData.color) { + // Конвертируем RGB значения (0-1) в hex цвет + const r = Math.floor((colliderData.color.r || 1.0) * 255); + const g = Math.floor((colliderData.color.g || 0.0) * 255); + const b = Math.floor((colliderData.color.b || 0.0) * 255); + color = (r << 16) | (g << 8) | b; + } + + if (colliderData.opacity !== undefined) { + opacity = Math.max(0, Math.min(1, colliderData.opacity)); // Ограничиваем от 0 до 1 + } + + const material = new THREE.MeshBasicMaterial({ + color: color, + transparent: true, + opacity: opacity, + wireframe: false + }); + + const mesh = new THREE.Mesh(geometry, material); + mesh.position.copy(position); + mesh.rotation.copy(rotation); + mesh.scale.copy(scale); + + // Добавляем метку для отладки + mesh.userData = { + isCollider: true, + colliderIndex: index, + originalData: colliderData, + isFromModel: !!targetMesh + }; + + return mesh; + }; + + // Функция для применения цвета и прозрачности к объектам интерьера + const applyColliderColorAndOpacity = (scene, objectData) => { + // Ищем соответствующий коллайдер в JSON данных + const matchingCollider = jsonCollidersRef.current?.find(collider => { + const pos = collider.data.position; + const objPos = { x: objectData.x || 0, y: objectData.y || 0, z: objectData.z || 0 }; + + // Проверяем близость позиций (с небольшой погрешностью) + const distance = Math.sqrt( + Math.pow(pos.x - objPos.x, 2) + + Math.pow(pos.y - objPos.y, 2) + + Math.pow(pos.z - objPos.z, 2) + ); + + return distance < 2.0; // Если объекты находятся в радиусе 2 единиц + }); + + if (matchingCollider && matchingCollider.data) { + const colliderData = matchingCollider.data; + + // Определяем цвет и прозрачность + let color = 0xffffff; // Белый по умолчанию + let opacity = 1.0; // Полная непрозрачность по умолчанию + + if (colliderData.color) { + const r = Math.floor((colliderData.color.r || 1.0) * 255); + const g = Math.floor((colliderData.color.g || 1.0) * 255); + const b = Math.floor((colliderData.color.b || 1.0) * 255); + color = (r << 16) | (g << 8) | b; + } + + if (colliderData.opacity !== undefined) { + opacity = Math.max(0, Math.min(1, colliderData.opacity)); + } + + // Применяем цвет и прозрачность ко всем мешам в сцене + scene.traverse((child) => { + if (child.isMesh && child.material) { + if (Array.isArray(child.material)) { + child.material = child.material.map(mat => { + if (!mat) return mat; + const m = mat.clone(); + m.color.setHex(color); + m.transparent = opacity < 1.0; + m.opacity = opacity; + m.needsUpdate = true; + return m; + }); + } else { + child.material = child.material.clone(); + child.material.color.setHex(color); + child.material.transparent = opacity < 1.0; + child.material.opacity = opacity; + child.material.needsUpdate = true; + } + } + }); + + console.log('🎨 Применен цвет и прозрачность к объекту:', { + position: { x: objectData.x, y: objectData.y, z: objectData.z }, + color: color, + opacity: opacity, + colliderData: colliderData + }); + } + }; + + // Функция для загрузки коллизионных данных из JSON + const loadCollidersFromJSON = async (cityId = 1) => { + console.log('🔍 loadCollidersFromJSON вызвана для города:', cityId); + try { + const url = `/colliders_city_${cityId}.json`; + console.log('🔍 Загружаем URL:', url); + const response = await fetch(url); + console.log('🔍 Ответ сервера:', response.status, response.ok); + + if (!response.ok) { + console.warn('Не удалось загрузить коллизионные данные для города:', cityId); + return []; + } + + const data = await response.json(); + console.log('🔍 Загруженные данные:', data); + console.log('Загружены коллизионные данные:', data.colliders.length, 'объектов'); + + // Преобразуем JSON данные в Box3 объекты + const colliderBoxes = data.colliders.map((colliderData, index) => { + const box = new THREE.Box3(); + + // Создаем центр бокса + const center = new THREE.Vector3( + colliderData.position.x, + colliderData.position.y, + colliderData.position.z + ); + + // Увеличиваем размеры для полного покрытия объекта с адаптивным масштабированием + let size = new THREE.Vector3( + colliderData.scale.x, + colliderData.scale.y, + colliderData.scale.z + ); + + if (COLLIDER_CONFIG.adaptiveScaling) { + // Адаптивное масштабирование на основе размеров объекта + const avgSize = (size.x + size.y + size.z) / 3; + + if (avgSize < 1.0) { + // Для маленьких объектов используем больший коэффициент + const adaptiveMultiplier = Math.max(COLLIDER_CONFIG.sizeMultiplier, 3.0); + size.multiplyScalar(adaptiveMultiplier); + } else if (avgSize < 5.0) { + // Для средних объектов используем стандартный коэффициент + size.multiplyScalar(COLLIDER_CONFIG.sizeMultiplier); + } else { + // Для больших объектов используем меньший коэффициент + size.multiplyScalar(Math.max(COLLIDER_CONFIG.sizeMultiplier * 0.8, 1.5)); + } + + // Применяем минимальные и максимальные ограничения + size.x = Math.max(Math.min(size.x, COLLIDER_CONFIG.maxSize), COLLIDER_CONFIG.minSize); + size.y = Math.max(Math.min(size.y, COLLIDER_CONFIG.maxSize), COLLIDER_CONFIG.minSize); + size.z = Math.max(Math.min(size.z, COLLIDER_CONFIG.maxSize), COLLIDER_CONFIG.minSize); + } else { + // Простое масштабирование + size.multiplyScalar(COLLIDER_CONFIG.sizeMultiplier); + } + + // Устанавливаем min и max точки с увеличенными размерами + const min = center.clone().sub(size.clone().multiplyScalar(0.5)); + const max = center.clone().add(size.clone().multiplyScalar(0.5)); + + box.setFromPoints([min, max]); + + // Создаем визуальный коллайдер на основе реальных размеров мешей из модели + const visualCollider = createVisualColliderFromModel(colliderData, index); + + console.log('Создан коллайдер с увеличенными размерами:', { + center: center, + originalSize: colliderData.scale, + adjustedSize: size, + min: min, + max: max, + visual: visualCollider + }); + + return { + box: box, + data: colliderData, + visual: visualCollider + }; + }); + + console.log('🔍 Возвращаем colliderBoxes:', colliderBoxes.length, 'объектов'); + console.log('🔍 Первый коллайдер:', colliderBoxes[0]); + + // Автоматически применяем цвета к объектам интерьера, если они уже загружены + setTimeout(() => { + if (interiorGroupRef.current) { + console.log('🎨 JSON коллайдеры загружены, применяем цвета к объектам интерьера'); + colliderBoxes.forEach((colliderData, index) => { + const colliderPos = colliderData.data.position; + const colliderData_obj = colliderData.data; + + // Определяем цвет и прозрачность из JSON данных + let color = 0xffffff; // Белый по умолчанию + let opacity = 1.0; // Полная непрозрачность по умолчанию + + if (colliderData_obj.color) { + const r = Math.floor((colliderData_obj.color.r || 1.0) * 255); + const g = Math.floor((colliderData_obj.color.g || 1.0) * 255); + const b = Math.floor((colliderData_obj.color.b || 1.0) * 255); + color = (r << 16) | (g << 8) | b; + } + + if (colliderData_obj.opacity !== undefined) { + opacity = Math.max(0, Math.min(1, colliderData_obj.opacity)); + } + + console.log(`🎨 Применяем цвет ${color.toString(16)} и прозрачность ${opacity} к объектам интерьера`); + + // Применяем цвет к объектам интерьера + interiorGroupRef.current.traverse((child) => { + if (child.isMesh && child.material) { + const distance = Math.sqrt( + Math.pow(child.position.x - colliderPos.x, 2) + + Math.pow(child.position.y - colliderPos.y, 2) + + Math.pow(child.position.z - colliderPos.z, 2) + ); + + if (distance < 2.0) { + console.log(`🎯 Применяем цвет к объекту интерьера:`, child.name || 'unnamed'); + + if (Array.isArray(child.material)) { + child.material.forEach(mat => { + if (mat) { + mat.color.setHex(color); + mat.transparent = opacity < 1.0; + mat.opacity = opacity; + mat.needsUpdate = true; + } + }); + } else { + child.material.color.setHex(color); + child.material.transparent = opacity < 1.0; + child.material.opacity = opacity; + child.material.needsUpdate = true; + } + } + } + }); + }); + } + }, 100); + + return colliderBoxes; + } catch (error) { + console.error('Ошибка загрузки коллизионных данных:', error); + return []; + } + }; + async function loadInteriorModel(interiorId) { console.log('loadInteriorModel вызвана для интерьера:', interiorId); const token = localStorage.getItem('token'); + console.log('Токен найден:', !!token); try { + console.log('Запрашиваем определение интерьера с сервера...'); const defRes = await fetch(`/api/interiors/${interiorId}/definition`, { headers: { Authorization: `Bearer ${token}` }, credentials: 'include', cache: 'no-cache' }); + console.log('Ответ сервера:', defRes.status, defRes.ok); + if (!defRes.ok) { const errText = await defRes.text(); console.error(`Ошибка ${defRes.status} при загрузке определения интерьера: ${errText}`); @@ -715,10 +1564,50 @@ function Game({ avatarUrl, gender }) { const colliders = []; gltf.scene.traverse((child) => { if (child.isMesh && child.geometry) { + // Пропускаем интерактивные объекты и сферы + if (child.userData && (child.userData.interactable || child.userData.payload)) return; + if (child.geometry.type === 'SphereGeometry') return; colliders.push(child); } }); interiorCollidersRef.current = colliders; + console.log('Инициализировано коллайдеров интерьера:', colliders.length); + try { + const boxes = []; + for (const m of colliders) { + if (!m) continue; + const b = new THREE.Box3().setFromObject(m).expandByScalar(0.03); + const h = b.max.y - b.min.y; + if (h < 0.15) continue; // игнорируем пол/ковёр + boxes.push(b); + } + interiorColliderBoxesRef.current = boxes; + console.log('[INTERIOR] colliders boxes:', boxes.length); + + // Визуализация (вкл/выкл через interiorDebugEnabledRef) + if (interiorDebugEnabledRef.current && sceneRef.current) { + // Очистим старые + if (Array.isArray(interiorDebugHelpersRef.current)) { + for (const h of interiorDebugHelpersRef.current) { + try { sceneRef.current.remove(h); } catch (_) {} + } + } + interiorDebugHelpersRef.current = []; + const mat = new THREE.LineBasicMaterial({ color: 0xff00ff }); + for (const box of boxes) { + const size = new THREE.Vector3(); + const center = new THREE.Vector3(); + box.getSize(size); + box.getCenter(center); + const geom = new THREE.BoxGeometry(size.x, size.y, size.z); + const edges = new THREE.EdgesGeometry(geom); + const helper = new THREE.LineSegments(edges, mat); + helper.position.copy(center); + sceneRef.current.add(helper); + interiorDebugHelpersRef.current.push(helper); + } + } + } catch (_) {} // Добавляем объекты интерьера interiorInteractablesRef.current = []; // сбрасываем реестр интерактива @@ -743,6 +1632,10 @@ function Game({ avatarUrl, gender }) { objGltf.scene.position.set(o.x, o.y, o.z); objGltf.scene.rotation.set(o.rot_x, o.rot_y, o.rot_z); objGltf.scene.scale.set(o.scale, o.scale, o.scale); + + // Применяем цвет и прозрачность из JSON данных коллайдеров + applyColliderColorAndOpacity(objGltf.scene, o); + intGroup.add(objGltf.scene); // Добавляем меши объекта как коллайдеры интерьера @@ -842,6 +1735,69 @@ function Game({ avatarUrl, gender }) { interiorGroupRef.current = intGroup; console.log('Модель интерьера загружена успешно'); + + // Автоматически применяем цвета из JSON к объектам интерьера + console.log('🎨 Применяем цвета из JSON к объектам интерьера...'); + setTimeout(() => { + // Применяем цвета к объектам интерьера, если JSON коллайдеры уже загружены + if (jsonCollidersRef.current && jsonCollidersRef.current.length > 0) { + console.log('🔍 JSON коллайдеры найдены, применяем цвета к объектам интерьера'); + jsonCollidersRef.current.forEach((colliderData, index) => { + const colliderPos = colliderData.data.position; + const colliderData_obj = colliderData.data; + + // Определяем цвет и прозрачность из JSON данных + let color = 0xffffff; // Белый по умолчанию + let opacity = 1.0; // Полная непрозрачность по умолчанию + + if (colliderData_obj.color) { + const r = Math.floor((colliderData_obj.color.r || 1.0) * 255); + const g = Math.floor((colliderData_obj.color.g || 1.0) * 255); + const b = Math.floor((colliderData_obj.color.b || 1.0) * 255); + color = (r << 16) | (g << 8) | b; + } + + if (colliderData_obj.opacity !== undefined) { + opacity = Math.max(0, Math.min(1, colliderData_obj.opacity)); + } + + console.log(`🎨 Применяем цвет ${color.toString(16)} и прозрачность ${opacity} к объектам интерьера`); + + // Применяем цвет к объектам интерьера + intGroup.traverse((child) => { + if (child.isMesh && child.material) { + const distance = Math.sqrt( + Math.pow(child.position.x - colliderPos.x, 2) + + Math.pow(child.position.y - colliderPos.y, 2) + + Math.pow(child.position.z - colliderPos.z, 2) + ); + + if (distance < 2.0) { + console.log(`🎯 Применяем цвет к объекту интерьера:`, child.name || 'unnamed'); + + if (Array.isArray(child.material)) { + child.material.forEach(mat => { + if (mat) { + mat.color.setHex(color); + mat.transparent = opacity < 1.0; + mat.opacity = opacity; + mat.needsUpdate = true; + } + }); + } else { + child.material.color.setHex(color); + child.material.transparent = opacity < 1.0; + child.material.opacity = opacity; + child.material.needsUpdate = true; + } + } + } + }); + }); + } else { + console.log('⚠️ JSON коллайдеры еще не загружены, цвета будут применены позже'); + } + }, 200); // Задержка для завершения загрузки объектов } catch (e) { console.error('Ошибка загрузки модели интерьера:', e); } @@ -1150,6 +2106,26 @@ function Game({ avatarUrl, gender }) { console.log('Группа интерьера удалена'); } + // Очищаем коллайдеры интерьера + interiorCollidersRef.current = []; + interiorColliderBoxesRef.current = []; + jsonCollidersRef.current = []; + + // Удаляем визуальные коллайдеры из сцены + visualCollidersRef.current.forEach(collider => { + if (sceneRef.current) { + sceneRef.current.remove(collider); + console.log('Удален визуальный коллайдер из сцены'); + } + }); + visualCollidersRef.current = []; + + console.log('Коллайдеры интерьера очищены'); + + // Сбрасываем флаги отладки + window.colliderDebugShown = false; + window.collisionDebugShown = false; + // Возвращаем третье лицо/камеру и актуализировать видимость объектов города switchToThirdPersonCamera?.(); // Безопасный вызов без ReferenceError, даже если функция ещё не определена @@ -1195,6 +2171,7 @@ function Game({ avatarUrl, gender }) { document.exitPointerLock(); setIsInInterior(false); + isInInteriorRef.current = false; // Важно! Сбрасываем ref для системы коллизий setCurrentExit(null); interiorExitPosRef.current = null; }; @@ -1937,11 +2914,15 @@ function Game({ avatarUrl, gender }) { } function startMove(dir) { + console.log('startMove вызвана для направления:', dir); moveInputRef.current[dir] = true; + console.log('moveInputRef.current после startMove:', moveInputRef.current); } function stopMove(dir) { + console.log('stopMove вызвана для направления:', dir); moveInputRef.current[dir] = false; + console.log('moveInputRef.current после stopMove:', moveInputRef.current); } @@ -4393,11 +5374,11 @@ function Game({ avatarUrl, gender }) { } function updateFirstPersonMovement(delta) { - if (!isInInteriorRef.current || cameraRef.current !== fpCamRef.current || !player) return; + if (!isInInteriorRef.current || !player) return; const move = moveInputRef.current; - const speed = 2; // Уменьшаем скорость для более плавного движения в интерьере - const rotSpeed = Math.PI * 0.5; // Уменьшаем скорость поворота + const speed = 3.0; // Скорость движения в интерьере + const rotSpeed = Math.PI * 0.5; // Скорость поворота // Проверка триггера выхода по внутренней точке if (interiorExitPosRef.current && player.position.distanceTo(interiorExitPosRef.current) < 0.7) { @@ -4417,94 +5398,205 @@ function Game({ avatarUrl, gender }) { const lookForward = new THREE.Vector3(0, 0, -1).applyEuler(new THREE.Euler(0, player.rotation.y, 0)); fpCamRef.current.lookAt(fpCamRef.current.position.clone().add(lookForward)); - // Улучшенное движение с проверкой коллизий и предотвращением застревания - const tryMove = (dirVec) => { - const stepDistance = speed * delta; - const candidate = player.position.clone().addScaledVector(dirVec, stepDistance); + // Упрощенная система коллизий + const tryMove = (direction) => { + const moveDistance = speed * delta; + const playerRadius = 0.4; // Радиус игрока + const playerHeight = 1.6; // Высота игрока + + // Получаем коллайдеры из JSON (приоритет) + let jsonColliders = jsonCollidersRef.current || []; + console.log('🔍 JSON коллайдеров:', jsonColliders.length); - // Обновляем AABB игрока с меньшими размерами для предотвращения застревания - const half = 0.2; // Уменьшаем размер для лучшего прохождения - const height = 1.6; // Немного ниже для предотвращения застревания в потолке - const playerBox = new THREE.Box3( - new THREE.Vector3(candidate.x - half, candidate.y, candidate.z - half), - new THREE.Vector3(candidate.x + half, candidate.y + height, candidate.z + half) - ); - - // Обновляем мировые матрицы статических коллайдеров для корректных AABB - try { - interiorGroupRef.current && interiorGroupRef.current.updateMatrixWorld(true); - } catch (_) { } + // Если JSON коллайдеров нет, используем коллайдеры из модели + let colliders = interiorCollidersRef.current || []; + console.log('🔍 Модельных коллайдеров:', colliders.length); - // В интерьере учитываем только внутренние коллайдеры - const blockingMeshes = Array.isArray(interiorCollidersRef.current) - ? interiorCollidersRef.current - : []; - - let hits = false; - let closestDistance = Infinity; - let slideDirection = null; - - for (const mesh of blockingMeshes) { - if (!mesh) continue; - const box = new THREE.Box3().setFromObject(mesh); - const expanded = box.clone().expandByScalar(0.05); // Увеличиваем зазор - - if (expanded.intersectsBox(playerBox)) { - hits = true; - - // Вычисляем направление скольжения вдоль стены - const center = box.getCenter(new THREE.Vector3()); - const toPlayer = player.position.clone().sub(center); - const distance = toPlayer.length(); - - if (distance < closestDistance) { - closestDistance = distance; - // Нормализуем и создаем направление скольжения - toPlayer.normalize(); - slideDirection = toPlayer; + // Если коллайдеров нет, собираем их из группы интерьера + if (colliders.length === 0 && interiorGroupRef.current) { + colliders = []; + interiorGroupRef.current.traverse((child) => { + if (child.isMesh && child.geometry && child.visible) { + // Пропускаем интерактивные объекты и сферы + if (child.userData && (child.userData.interactable || child.userData.payload)) return; + if (child.geometry.type === 'SphereGeometry') return; + colliders.push(child); } - } + }); + interiorCollidersRef.current = colliders; + console.log('Собрано коллайдеров интерьера:', colliders.length); } - if (!hits) { - // Свободное движение - player.position.copy(candidate); - } else if (slideDirection) { - // Скольжение вдоль стены - const slideDistance = stepDistance * 0.7; // Уменьшаем дистанцию скольжения - const slidePos = player.position.clone().addScaledVector(slideDirection, slideDistance); - - // Проверяем, можно ли двигаться в направлении скольжения - const slideBox = new THREE.Box3( - new THREE.Vector3(slidePos.x - half, slidePos.y, slidePos.z - half), - new THREE.Vector3(slidePos.x + half, slidePos.y + height, slidePos.z + half) + // Проверяем коллизии + const checkCollision = (testPosition) => { + // Создаем AABB для игрока + const playerBox = new THREE.Box3(); + const playerMin = new THREE.Vector3( + testPosition.x - playerRadius, + testPosition.y, + testPosition.z - playerRadius ); - - let canSlide = true; - for (const mesh of blockingMeshes) { - if (!mesh) continue; - const box = new THREE.Box3().setFromObject(mesh); - const expanded = box.clone().expandByScalar(0.05); - if (expanded.intersectsBox(slideBox)) { - canSlide = false; - break; + const playerMax = new THREE.Vector3( + testPosition.x + playerRadius, + testPosition.y + playerHeight, + testPosition.z + playerRadius + ); + playerBox.setFromPoints([playerMin, playerMax]); + + // Сначала проверяем JSON коллайдеры (приоритет) + if (jsonColliders.length > 0) { + console.log('🔍 Проверяем', jsonColliders.length, 'JSON коллайдеров'); + console.log('🔍 Player box:', playerBox.min, '->', playerBox.max); + + for (let i = 0; i < jsonColliders.length; i++) { + const jsonCollider = jsonColliders[i]; + try { + console.log(`🔍 Проверяем коллайдер ${i}:`, jsonCollider.box.min, '->', jsonCollider.box.max); + + // Проверяем пересечение с JSON коллайдером + const intersects = playerBox.intersectsBox(jsonCollider.box); + console.log(`🔍 Результат intersectsBox: ${intersects}`); + + if (intersects) { + console.log('🚫 КОЛЛИЗИЯ с JSON коллайдером', i, '!'); + console.log(' Player box:', playerBox.min, '->', playerBox.max); + console.log(' JSON Collider box:', jsonCollider.box.min, '->', jsonCollider.box.max); + return true; + } + + // Ручная проверка пересечения + const manualX = playerBox.min.x <= jsonCollider.box.max.x && playerBox.max.x >= jsonCollider.box.min.x; + const manualY = playerBox.min.y <= jsonCollider.box.max.y && playerBox.max.y >= jsonCollider.box.min.y; + const manualZ = playerBox.min.z <= jsonCollider.box.max.z && playerBox.max.z >= jsonCollider.box.min.z; + const manualIntersects = manualX && manualY && manualZ; + + console.log(`🔍 Ручная проверка - X: ${manualX}, Y: ${manualY}, Z: ${manualZ}, Результат: ${manualIntersects}`); + + if (manualIntersects) { + console.log('🚫 РУЧНАЯ КОЛЛИЗИЯ с JSON коллайдером', i, '!'); + return true; + } + } catch (error) { + console.warn('Ошибка при проверке JSON коллизии:', error); + continue; + } } } + + // Затем проверяем коллайдеры из модели (если JSON коллайдеров нет) + if (jsonColliders.length === 0) { + for (const collider of colliders) { + if (!collider.geometry || !collider.visible) continue; + + try { + // Обновляем матрицу мира для коллайдера + collider.updateMatrixWorld(true); + + // Создаем Box3 для коллайдера в мировых координатах + const colliderBox = new THREE.Box3(); + colliderBox.setFromObject(collider); + + // Проверяем пересечение + if (playerBox.intersectsBox(colliderBox)) { + console.log('🚫 КОЛЛИЗИЯ! Объект:', collider.name || 'unnamed'); + console.log(' Player box:', playerBox.min, '->', playerBox.max); + console.log(' Collider box:', colliderBox.min, '->', colliderBox.max); + return true; + } else { + // Отладка: почему коллизия не обнаружена + if (!window.collisionDebugShown) { + console.log('🔍 Отладка коллизии для', collider.name || 'unnamed'); + console.log(' Player box:', playerBox.min, '->', playerBox.max); + console.log(' Collider box:', colliderBox.min, '->', colliderBox.max); + console.log(' Пересечение по X:', playerBox.min.x <= colliderBox.max.x && playerBox.max.x >= colliderBox.min.x); + console.log(' Пересечение по Y:', playerBox.min.y <= colliderBox.max.y && playerBox.max.y >= colliderBox.min.y); + console.log(' Пересечение по Z:', playerBox.min.z <= colliderBox.max.z && playerBox.max.z >= colliderBox.min.z); + window.collisionDebugShown = true; + } + } + } catch (error) { + console.warn('Ошибка при проверке коллизии:', error); + continue; + } + } + } + return false; + }; + + // Применяем движение с проверкой коллизий + const targetPosition = player.position.clone(); + targetPosition.add(direction.clone().multiplyScalar(moveDistance)); + + // Проверяем коллизии по осям отдельно для плавного движения + let safePosition = player.position.clone(); + + // Отладочная информация о коллайдерах (только при первом движении) + if ((jsonColliders.length > 0 || colliders.length > 0) && !window.colliderDebugShown) { + console.log('🔍 Проверяем коллизии с', jsonColliders.length, 'JSON коллайдерами и', colliders.length, 'модельными коллайдерами'); + console.log('📍 Позиция игрока:', player.position); - if (canSlide) { - player.position.copy(slidePos); + // Показываем JSON коллайдеры + jsonColliders.forEach((col, i) => { + console.log(`JSON Коллайдер ${i}:`, + 'Min:', col.box.min, 'Max:', col.box.max, 'Size:', col.box.getSize(new THREE.Vector3())); + console.log(` Игрок Y: ${player.position.y}, JSON Коллайдер Y: ${col.box.min.y} - ${col.box.max.y}`); + }); + + // Показываем модельные коллайдеры + colliders.forEach((col, i) => { + col.updateMatrixWorld(true); + const box = new THREE.Box3().setFromObject(col); + console.log(`Модельный Коллайдер ${i}:`, col.name || 'unnamed', + 'Min:', box.min, 'Max:', box.max, 'Size:', box.getSize(new THREE.Vector3())); + console.log(` Игрок Y: ${player.position.y}, Модельный Коллайдер Y: ${box.min.y} - ${box.max.y}`); + }); + window.colliderDebugShown = true; + } + console.log('Исходная позиция:', safePosition); + + // Проверяем движение по X + if (Math.abs(direction.x) > 0.001) { + const xTestPosition = safePosition.clone(); + xTestPosition.x = targetPosition.x; + const hasCollisionX = checkCollision(xTestPosition); + if (!hasCollisionX) { + safePosition.x = targetPosition.x; + } else { + console.log('🚫 X коллизия заблокирована'); } } + + // Проверяем движение по Z + if (Math.abs(direction.z) > 0.001) { + const zTestPosition = safePosition.clone(); + zTestPosition.z = targetPosition.z; + const hasCollisionZ = checkCollision(zTestPosition); + if (!hasCollisionZ) { + safePosition.z = targetPosition.z; + } else { + console.log('🚫 Z коллизия заблокирована'); + } + } + // Обновляем позицию игрока + player.position.copy(safePosition); }; const forward = new THREE.Vector3(0, 0, -1).applyQuaternion(player.quaternion); const right = new THREE.Vector3(1, 0, 0).applyQuaternion(player.quaternion); - // Применяем движение с плавностью - if (move.forward) tryMove(forward); - if (move.backward) tryMove(forward.clone().multiplyScalar(-1)); - if (move.strafeLeft) tryMove(right.clone().multiplyScalar(-1)); - if (move.strafeRight) tryMove(right); + // Применяем движение с проверкой коллизий + if (move.forward) { + tryMove(forward); + } + if (move.backward) { + tryMove(forward.clone().multiplyScalar(-1)); + } + if (move.strafeLeft) { + tryMove(right.clone().multiplyScalar(-1)); + } + if (move.strafeRight) { + tryMove(right); + } // Отправляем позицию внутри интерьера if (socketRef.current) { diff --git a/src/modules/CameraManager.js b/src/modules/CameraManager.js index 201146b..bb45d00 100644 --- a/src/modules/CameraManager.js +++ b/src/modules/CameraManager.js @@ -100,6 +100,9 @@ export class CameraManager { this.fpPitch = 0; this.fpCamera.updateProjectionMatrix(); + + // Запрашиваем pointer lock для управления мышью + this.requestPointerLock(); } /** @@ -108,6 +111,27 @@ export class CameraManager { switchToOrthoCamera() { this.currentCamera = this.orthoCamera; this.fpPitch = 0; + + // Выходим из pointer lock + this.exitPointerLock(); + } + + /** + * Запрос pointer lock для управления мышью + */ + requestPointerLock() { + if (document.body.requestPointerLock) { + document.body.requestPointerLock(); + } + } + + /** + * Выход из pointer lock + */ + exitPointerLock() { + if (document.exitPointerLock) { + document.exitPointerLock(); + } } /** diff --git a/src/modules/CollisionManager.js b/src/modules/CollisionManager.js new file mode 100644 index 0000000..e8d92b1 --- /dev/null +++ b/src/modules/CollisionManager.js @@ -0,0 +1,300 @@ +import * as THREE from 'three'; + +/** + * Менеджер коллизий + * Отвечает за проверку столкновений игрока с объектами интерьера + */ +export class CollisionManager { + constructor(sceneManager) { + this.sceneManager = sceneManager; + this.playerRadius = 0.35; // Радиус игрока + this.playerHeight = 1.6; // Высота игрока + this.collisionCache = new Map(); // Кэш для оптимизации + this.cacheTimeout = 100; // Время жизни кэша в мс + this.lastCacheUpdate = 0; + + this.init(); + } + + /** + * Инициализация менеджера коллизий + */ + init() { + console.log('CollisionManager инициализирован'); + } + + /** + * Проверка коллизий при движении игрока в интерьере + * @param {THREE.Vector3} currentPosition - Текущая позиция игрока + * @param {THREE.Vector3} targetPosition - Целевая позиция игрока + * @param {THREE.Vector3} direction - Направление движения + * @param {number} deltaTime - Время между кадрами + * @returns {THREE.Vector3} - Безопасная позиция игрока + */ + checkInteriorCollisions(currentPosition, targetPosition, direction, deltaTime) { + if (!this.sceneManager.interiorColliders || this.sceneManager.interiorColliders.length === 0) { + return targetPosition; + } + + // Обновляем кэш коллизий если нужно + this.updateCollisionCache(); + + // Получаем коллайдеры из кэша + const colliders = this.collisionCache.get('interiorColliders') || []; + + if (colliders.length === 0) { + return targetPosition; + } + + // Проверяем коллизии по осям отдельно для более плавного движения + let safePosition = currentPosition.clone(); + + // Проверяем движение по X + if (Math.abs(direction.x) > 0.001) { + const xTestPosition = safePosition.clone(); + xTestPosition.x = targetPosition.x; + + if (!this.checkPlayerCollision(xTestPosition, colliders)) { + safePosition.x = targetPosition.x; + } + } + + // Проверяем движение по Z + if (Math.abs(direction.z) > 0.001) { + const zTestPosition = safePosition.clone(); + zTestPosition.z = targetPosition.z; + + if (!this.checkPlayerCollision(zTestPosition, colliders)) { + safePosition.z = targetPosition.z; + } + } + + return safePosition; + } + + /** + * Проверка столкновения игрока с коллайдерами + * @param {THREE.Vector3} position - Позиция игрока + * @param {Array} colliders - Массив коллайдеров + * @returns {boolean} - true если есть столкновение + */ + checkPlayerCollision(position, colliders) { + // Создаем AABB для игрока + const playerBox = new THREE.Box3(); + const playerMin = new THREE.Vector3( + position.x - this.playerRadius, + position.y, + position.z - this.playerRadius + ); + const playerMax = new THREE.Vector3( + position.x + this.playerRadius, + position.y + this.playerHeight, + position.z + this.playerRadius + ); + playerBox.setFromPoints([playerMin, playerMax]); + + // Проверяем столкновения с каждым коллайдером + for (const collider of colliders) { + if (!collider.geometry || !collider.visible) continue; + + // Пропускаем интерактивные объекты + if (collider.userData && (collider.userData.interactable || collider.userData.payload)) { + continue; + } + + // Пропускаем сферы (хит-зоны) + if (collider.geometry.type === 'SphereGeometry') { + continue; + } + + try { + // Обновляем мировую матрицу коллайдера + collider.updateMatrixWorld(true); + + // Создаем AABB для коллайдера + const colliderBox = new THREE.Box3(); + colliderBox.setFromObject(collider); + + // Проверяем пересечение + if (playerBox.intersectsBox(colliderBox)) { + return true; + } + } catch (error) { + console.warn('Ошибка при проверке коллизии:', error); + continue; + } + } + + return false; + } + + /** + * Обновление кэша коллизий + */ + updateCollisionCache() { + const now = Date.now(); + + // Обновляем кэш только если прошло достаточно времени + if (now - this.lastCacheUpdate < this.cacheTimeout) { + return; + } + + this.lastCacheUpdate = now; + + // Очищаем старый кэш + this.collisionCache.clear(); + + // Собираем коллайдеры интерьера + const interiorColliders = this.sceneManager.interiorColliders || []; + const validColliders = []; + + // Фильтруем и валидируем коллайдеры + for (const collider of interiorColliders) { + if (this.isValidCollider(collider)) { + validColliders.push(collider); + } + } + + // Если коллайдеров мало, попробуем собрать их из группы интерьера + if (validColliders.length === 0 && this.sceneManager.interiorGroup) { + this.collectCollidersFromInteriorGroup(validColliders); + } + + this.collisionCache.set('interiorColliders', validColliders); + } + + /** + * Проверка валидности коллайдера + * @param {THREE.Object3D} collider - Объект для проверки + * @returns {boolean} - true если коллайдер валиден + */ + isValidCollider(collider) { + if (!collider || !collider.isMesh) return false; + if (!collider.geometry) return false; + if (!collider.visible) return false; + + // Пропускаем интерактивные объекты + if (collider.userData && (collider.userData.interactable || collider.userData.payload)) { + return false; + } + + // Пропускаем сферы (хит-зоны) + if (collider.geometry.type === 'SphereGeometry') { + return false; + } + + return true; + } + + /** + * Сбор коллайдеров из группы интерьера + * @param {Array} colliders - Массив для добавления коллайдеров + */ + collectCollidersFromInteriorGroup(colliders) { + if (!this.sceneManager.interiorGroup) return; + + try { + this.sceneManager.interiorGroup.updateMatrixWorld(true); + + this.sceneManager.interiorGroup.traverse((child) => { + if (this.isValidCollider(child)) { + colliders.push(child); + } + }); + } catch (error) { + console.warn('Ошибка при сборе коллайдеров из группы интерьера:', error); + } + } + + /** + * Проверка коллизий с интерактивными объектами + * @param {THREE.Vector3} position - Позиция игрока + * @returns {Object|null} - Данные интерактивного объекта или null + */ + checkInteriorInteractions(position) { + const interactables = this.sceneManager.interiorInteractables || []; + + for (const interactable of interactables) { + if (!interactable.geometry || !interactable.visible) continue; + + try { + interactable.updateMatrixWorld(true); + + // Создаем сферу вокруг интерактивного объекта + const interactableBox = new THREE.Box3(); + interactableBox.setFromObject(interactable); + + // Создаем AABB для игрока + const playerBox = new THREE.Box3(); + const playerMin = new THREE.Vector3( + position.x - this.playerRadius, + position.y, + position.z - this.playerRadius + ); + const playerMax = new THREE.Vector3( + position.x + this.playerRadius, + position.y + this.playerHeight, + position.z + this.playerRadius + ); + playerBox.setFromPoints([playerMin, playerMax]); + + if (playerBox.intersectsBox(interactableBox)) { + return interactable.userData.payload || { type: 'interactable' }; + } + } catch (error) { + console.warn('Ошибка при проверке взаимодействия:', error); + continue; + } + } + + return null; + } + + /** + * Получение безопасной позиции для телепортации + * @param {THREE.Vector3} targetPosition - Целевая позиция + * @returns {THREE.Vector3} - Безопасная позиция + */ + getSafeTeleportPosition(targetPosition) { + const colliders = this.collisionCache.get('interiorColliders') || []; + + if (colliders.length === 0) { + return targetPosition.clone(); + } + + // Проверяем целевую позицию + if (!this.checkPlayerCollision(targetPosition, colliders)) { + return targetPosition.clone(); + } + + // Ищем ближайшую безопасную позицию + const searchRadius = 2.0; + const searchSteps = 8; + + for (let radius = 0.5; radius <= searchRadius; radius += 0.5) { + for (let step = 0; step < searchSteps; step++) { + const angle = (step / searchSteps) * Math.PI * 2; + const testPosition = new THREE.Vector3( + targetPosition.x + Math.cos(angle) * radius, + targetPosition.y, + targetPosition.z + Math.sin(angle) * radius + ); + + if (!this.checkPlayerCollision(testPosition, colliders)) { + return testPosition; + } + } + } + + // Если не нашли безопасную позицию, возвращаем исходную + return targetPosition.clone(); + } + + /** + * Очистка ресурсов + */ + dispose() { + this.collisionCache.clear(); + console.log('CollisionManager очищен'); + } +} diff --git a/src/modules/GameCore.js b/src/modules/GameCore.js index 1d61bb1..d3165c2 100644 --- a/src/modules/GameCore.js +++ b/src/modules/GameCore.js @@ -4,6 +4,7 @@ import { CameraManager } from './CameraManager.js'; import { PlayerManager } from './PlayerManager.js'; import { RendererManager } from './RendererManager.js'; import { InteriorManager } from './InteriorManager.js'; +import { CollisionManager } from './CollisionManager.js'; /** * Основной класс игры @@ -18,7 +19,8 @@ export class GameCore { // Инициализация модулей this.sceneManager = new SceneManager(); this.cameraManager = new CameraManager(); - this.playerManager = new PlayerManager(this.sceneManager); + this.collisionManager = new CollisionManager(this.sceneManager); + this.playerManager = new PlayerManager(this.sceneManager, this.collisionManager); this.rendererManager = new RendererManager(container); this.interiorManager = new InteriorManager(this.sceneManager); @@ -287,6 +289,7 @@ export class GameCore { async enterInterior(interiorId) { try { this.isInInterior = true; + this.playerManager.setInInterior(true); // Сохраняем позицию игрока const playerPosition = this.playerManager.getPlayerPosition(); @@ -305,6 +308,7 @@ export class GameCore { } catch (error) { console.error('Ошибка входа в интерьер:', error); this.isInInterior = false; + this.playerManager.setInInterior(false); } } @@ -316,6 +320,7 @@ export class GameCore { try { this.isInInterior = false; + this.playerManager.setInInterior(false); // Восстанавливаем позицию игрока this.playerManager.restorePosition(); @@ -413,6 +418,13 @@ export class GameCore { return this.interiorManager; } + /** + * Получение менеджера коллизий + */ + getCollisionManager() { + return this.collisionManager; + } + /** * Очистка ресурсов */ @@ -425,6 +437,7 @@ export class GameCore { this.playerManager.dispose(); this.rendererManager.dispose(); this.interiorManager.dispose(); + this.collisionManager.dispose(); // Удаляем обработчики событий document.removeEventListener('keydown', this.handleKeyDown.bind(this)); diff --git a/src/modules/PlayerManager.js b/src/modules/PlayerManager.js index 5744634..a969cd3 100644 --- a/src/modules/PlayerManager.js +++ b/src/modules/PlayerManager.js @@ -5,13 +5,16 @@ import * as THREE from 'three'; * Отвечает за создание, управление и анимацию игрока */ export class PlayerManager { - constructor(sceneManager) { + constructor(sceneManager, collisionManager = null) { this.sceneManager = sceneManager; + this.collisionManager = collisionManager; this.player = null; this.mixer = null; this.moveSpeed = 2.5; + this.interiorMoveSpeed = 3.0; // Скорость движения в интерьере this.savedPosition = new THREE.Vector3(); this.remotePlayers = {}; + this.isInInterior = false; this.init(); } @@ -51,7 +54,7 @@ export class PlayerManager { movePlayer(direction, deltaTime) { if (!this.player) return; - const moveDistance = this.moveSpeed * deltaTime; + const moveDistance = this.isInInterior ? this.interiorMoveSpeed * deltaTime : this.moveSpeed * deltaTime; const moveVector = new THREE.Vector3(); if (direction.forward) moveVector.z -= moveDistance; @@ -59,7 +62,20 @@ export class PlayerManager { if (direction.left) moveVector.x -= moveDistance; if (direction.right) moveVector.x += moveDistance; - this.player.position.add(moveVector); + // Если игрок в интерьере, используем систему коллизий + if (this.isInInterior && this.collisionManager) { + const targetPosition = this.player.position.clone().add(moveVector); + const safePosition = this.collisionManager.checkInteriorCollisions( + this.player.position, + targetPosition, + moveVector, + deltaTime + ); + this.player.position.copy(safePosition); + } else { + // Обычное движение без коллизий + this.player.position.add(moveVector); + } // Поворачиваем игрока в направлении движения if (moveVector.length() > 0) { @@ -74,12 +90,51 @@ export class PlayerManager { teleportPlayer(position, rotation = null) { if (!this.player) return; - this.player.position.copy(position); + // Если игрок в интерьере, проверяем безопасность позиции + if (this.isInInterior && this.collisionManager) { + const safePosition = this.collisionManager.getSafeTeleportPosition(position); + this.player.position.copy(safePosition); + } else { + this.player.position.copy(position); + } + if (rotation !== null) { this.player.rotation.y = rotation; } } + /** + * Сохранение позиции игрока + */ + savePosition() { + if (this.player) { + this.savedPosition.copy(this.player.position); + } + } + + /** + * Восстановление позиции игрока + */ + restorePosition() { + if (this.player && this.savedPosition.length() > 0) { + this.player.position.copy(this.savedPosition); + } + } + + /** + * Установка состояния интерьера + */ + setInInterior(inInterior) { + this.isInInterior = inInterior; + } + + /** + * Получение поворота игрока + */ + getPlayerRotation() { + return this.player ? this.player.rotation.clone() : new THREE.Euler(); + } + /** * Получение позиции игрока */ diff --git a/src/modules/README.md b/src/modules/README.md index 402d30c..9c21a69 100644 --- a/src/modules/README.md +++ b/src/modules/README.md @@ -10,6 +10,7 @@ - **SceneManager.js** - Управление 3D сценой - **CameraManager.js** - Управление камерами - **PlayerManager.js** - Управление игроком +- **CollisionManager.js** - Система коллизий для интерьеров ### 2. Rendering (Рендеринг) - **RendererManager.js** - Управление рендерером @@ -38,6 +39,28 @@ - **CollisionDetection.js** - Обнаружение коллизий - **AnimationManager.js** - Управление анимациями +## Система коллизий для интерьеров + +### CollisionManager.js +Модуль отвечает за проверку столкновений игрока с объектами интерьера при движении от первого лица. + +**Основные функции:** +- `checkInteriorCollisions()` - проверка коллизий при движении +- `checkPlayerCollision()` - проверка столкновения с конкретным объектом +- `getSafeTeleportPosition()` - поиск безопасной позиции для телепортации +- `checkInteriorInteractions()` - проверка взаимодействий с интерактивными объектами + +**Особенности:** +- Использует AABB (Axis-Aligned Bounding Box) для быстрой проверки коллизий +- Кэширование коллайдеров для оптимизации производительности +- Раздельная проверка по осям X и Z для плавного движения +- Автоматическое исключение интерактивных объектов и хит-зон + +**Интеграция:** +- Работает совместно с `PlayerManager` для ограничения движения +- Использует данные из `SceneManager` о коллайдерах интерьера +- Поддерживает различные типы геометрии (Box, Sphere, Custom) + ## Принципы модулизации 1. **Единая ответственность** - каждый модуль отвечает за одну область функциональности diff --git a/src/pages/EnhancedCollisionEditor.jsx b/src/pages/EnhancedCollisionEditor.jsx new file mode 100644 index 0000000..67aa5bd --- /dev/null +++ b/src/pages/EnhancedCollisionEditor.jsx @@ -0,0 +1,1380 @@ +import React, { useEffect, useMemo, useRef, useState } from 'react'; +import * as THREE from 'three'; +import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'; +import { TransformControls } from 'three/examples/jsm/controls/TransformControls'; +import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'; + +export default function EnhancedCollisionEditor() { + const mountRef = useRef(null); + const sceneRef = useRef(); + const cameraRef = useRef(); + const rendererRef = useRef(); + const orbitRef = useRef(); + const transformRef = useRef(); + const backgroundGroupRef = useRef(new THREE.Group()); + const gltfLoaderRef = useRef(new GLTFLoader()); + + const [shapeType, setShapeType] = useState('box'); + const [mode, setMode] = useState('translate'); + const [selected, setSelected] = useState(null); + const [cursorXZ, setCursorXZ] = useState({ x: 0, z: 0 }); + const [cities, setCities] = useState([]); + const [cityId, setCityId] = useState(1); + const [lockUniformXZ, setLockUniformXZ] = useState(true); + const collidersRef = useRef([]); + + // Новые состояния для цветов и прозрачности + const [selectedColor, setSelectedColor] = useState({ r: 1, g: 0, b: 0 }); + const [selectedOpacity, setSelectedOpacity] = useState(0.3); + const [showColorPicker, setShowColorPicker] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [isSaving, setIsSaving] = useState(false); + + // Состояния для управления камерой + const [cameraSpeed, setCameraSpeed] = useState(5); + const [keysPressed, setKeysPressed] = useState({}); + const cameraMoveRef = useRef({ forward: false, backward: false, left: false, right: false, up: false, down: false }); + + // Состояние для расстояния создания коллайдера + const [colliderCreationDistance, setColliderCreationDistance] = useState(10); + + // Состояние для предварительного просмотра позиции коллайдера + const [showPreview, setShowPreview] = useState(false); + const previewRef = useRef(null); + + // Состояния для управления параметрами выбранного коллайдера + const [colliderPosition, setColliderPosition] = useState({ x: 0, y: 0, z: 0 }); + const [colliderRotation, setColliderRotation] = useState({ x: 0, y: 0, z: 0 }); + const [colliderScale, setColliderScale] = useState({ x: 1, y: 1, z: 1 }); + + // Состояние для списка интерьеров + const [interiors, setInteriors] = useState([]); + + // Состояние для режима TransformControls + const [transformMode, setTransformMode] = useState('translate'); // 'translate', 'rotate', 'scale' + + const colliderMaterial = useMemo(() => new THREE.MeshBasicMaterial({ + color: new THREE.Color(selectedColor.r, selectedColor.g, selectedColor.b), + transparent: true, + opacity: selectedOpacity, + depthWrite: false + }), [selectedColor, selectedOpacity]); + + const colliderEdgeMaterial = useMemo(() => new THREE.LineBasicMaterial({ + color: new THREE.Color(selectedColor.r, selectedColor.g, selectedColor.b) + }), [selectedColor]); + + useEffect(() => { + const scene = new THREE.Scene(); + scene.background = new THREE.Color(0x9aa7b1); + sceneRef.current = scene; + + const camera = new THREE.PerspectiveCamera(60, mountRef.current.clientWidth / mountRef.current.clientHeight, 0.1, 2000); + camera.position.set(20, 20, 20); + camera.lookAt(0, 0, 0); + cameraRef.current = camera; + + const renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setSize(mountRef.current.clientWidth, mountRef.current.clientHeight); + mountRef.current.appendChild(renderer.domElement); + rendererRef.current = renderer; + + const hemi = new THREE.HemisphereLight(0xffffff, 0x444444, 0.9); + hemi.position.set(0, 50, 0); + scene.add(hemi); + + const grid = new THREE.GridHelper(1000, 100); + scene.add(grid); + + const orbit = new OrbitControls(camera, renderer.domElement); + orbit.enableDamping = true; + orbit.dampingFactor = 0.05; + orbit.enablePan = true; + orbit.enableZoom = true; + orbit.enableRotate = true; + orbit.panSpeed = 1.0; + orbit.zoomSpeed = 1.0; + orbit.rotateSpeed = 1.0; + orbit.minDistance = 5; + orbit.maxDistance = 500; + orbit.maxPolarAngle = Math.PI; + orbitRef.current = orbit; + + const transform = new TransformControls(camera, renderer.domElement); + transform.addEventListener('dragging-changed', (event) => { + orbit.enabled = !event.value; + }); + transform.addEventListener('objectChange', () => { + if (selected) { + updateColliderData(selected); + } + }); + scene.add(transform); + transformRef.current = transform; + + scene.add(backgroundGroupRef.current); + + // Функция для движения камеры с клавиатуры + const moveCamera = () => { + if (!cameraRef.current || !orbitRef.current) return; + + const camera = cameraRef.current; + const orbit = orbitRef.current; + const move = cameraMoveRef.current; + + if (move.forward || move.backward || move.left || move.right || move.up || move.down) { + const direction = new THREE.Vector3(); + const right = new THREE.Vector3(); + + // Получаем направление камеры + camera.getWorldDirection(direction); + right.crossVectors(direction, camera.up).normalize(); + + const moveVector = new THREE.Vector3(); + + if (move.forward) { + moveVector.add(direction.multiplyScalar(cameraSpeed * 0.1)); + } + if (move.backward) { + moveVector.add(direction.multiplyScalar(-cameraSpeed * 0.1)); + } + if (move.left) { + moveVector.add(right.multiplyScalar(-cameraSpeed * 0.1)); + } + if (move.right) { + moveVector.add(right.multiplyScalar(cameraSpeed * 0.1)); + } + if (move.up) { + moveVector.add(camera.up.multiplyScalar(cameraSpeed * 0.1)); + } + if (move.down) { + moveVector.add(camera.up.multiplyScalar(-cameraSpeed * 0.1)); + } + + // Применяем движение к камере и target OrbitControls + camera.position.add(moveVector); + orbit.target.add(moveVector); + orbit.update(); + } + }; + + const animate = () => { + requestAnimationFrame(animate); + moveCamera(); + orbit.update(); + + // Обновляем предварительный просмотр при движении камеры + if (showPreview) { + updatePreview(); + } + + renderer.render(scene, camera); + }; + animate(); + + return () => { + if (mountRef.current && renderer.domElement) { + mountRef.current.removeChild(renderer.domElement); + } + }; + }, []); + + // Загрузка городов и интерьеров + useEffect(() => { + const token = localStorage.getItem('token'); + + // Загружаем города + fetch('/api/cities', { headers: { Authorization: `Bearer ${token}` } }) + .then(r => r.json()) + .then(data => { + setCities(data); + if (data.length > 0) { + setCityId(data[0].id); + } + }) + .catch(() => {}); + + // Загружаем интерьеры + fetch('/api/interiors', { headers: { Authorization: `Bearer ${token}` } }) + .then(r => r.json()) + .then(data => { + setInteriors(data); + }) + .catch(() => {}); + }, []); + + // Загрузка объектов города + useEffect(() => { + if (!cityId || !sceneRef.current) return; + const token = localStorage.getItem('token'); + const bg = backgroundGroupRef.current; + + // Очищаем предыдущие объекты + while (bg.children.length) { + const ch = bg.children.pop(); + ch.traverse(n => { + if (n.isMesh) { + n.geometry?.dispose?.(); + if (n.material) { + if (Array.isArray(n.material)) n.material.forEach(m => m.dispose?.()); + else n.material.dispose?.(); + } + } + }); + } + + fetch(`/api/cities/${cityId}/objects`, { headers: { Authorization: `Bearer ${token}` } }) + .then(r => r.json()) + .then(async data => { + for (const obj of data) { + try { + const gltf = await gltfLoaderRef.current.loadAsync(obj.model_url); + const m = gltf.scene; + m.position.set(obj.pos_x, obj.pos_y, obj.pos_z); + m.rotation.set(obj.rot_x, obj.rot_y, obj.rot_z); + m.scale.set(obj.scale_x || 1, obj.scale_y || 1, obj.scale_z || 1); + m.traverse(child => { + if (child.isMesh && child.material) { + if (Array.isArray(child.material)) { + child.material.forEach(mat => { + if (mat) mat.transparent = true; + if (mat) mat.opacity = 0.9; + }); + } else { + child.material.transparent = true; + child.material.opacity = 0.9; + } + child.raycast = () => {}; + } + }); + bg.add(m); + } catch (e) { + console.error('Ошибка загрузки объекта:', e); + } + } + }) + .catch(() => {}); + }, [cityId]); + + // Обработчики клавиатуры для управления камерой + useEffect(() => { + const handleKeyDown = (event) => { + const key = event.key.toLowerCase(); + const move = cameraMoveRef.current; + + switch (key) { + case 'w': + case 'arrowup': + move.forward = true; + break; + case 's': + case 'arrowdown': + move.backward = true; + break; + case 'a': + case 'arrowleft': + move.left = true; + break; + case 'd': + case 'arrowright': + move.right = true; + break; + case 'q': + case 'pageup': + move.up = true; + break; + case 'e': + case 'pagedown': + move.down = true; + break; + case 'r': + // Сброс позиции камеры + if (cameraRef.current && orbitRef.current) { + cameraRef.current.position.set(20, 20, 20); + orbitRef.current.target.set(0, 0, 0); + orbitRef.current.update(); + } + break; + } + + setKeysPressed(prev => ({ ...prev, [key]: true })); + }; + + const handleKeyUp = (event) => { + const key = event.key.toLowerCase(); + const move = cameraMoveRef.current; + + switch (key) { + case 'w': + case 'arrowup': + move.forward = false; + break; + case 's': + case 'arrowdown': + move.backward = false; + break; + case 'a': + case 'arrowleft': + move.left = false; + break; + case 'd': + case 'arrowright': + move.right = false; + break; + case 'q': + case 'pageup': + move.up = false; + break; + case 'e': + case 'pagedown': + move.down = false; + break; + } + + setKeysPressed(prev => ({ ...prev, [key]: false })); + }; + + // Добавляем обработчики событий + window.addEventListener('keydown', handleKeyDown); + window.addEventListener('keyup', handleKeyUp); + + return () => { + window.removeEventListener('keydown', handleKeyDown); + window.removeEventListener('keyup', handleKeyUp); + }; + }, []); + + // Автоматическая загрузка коллайдеров при смене города + useEffect(() => { + if (cityId) { + loadCollidersFromJSON(); + } + }, [cityId]); + + // Загрузка коллизий из JSON через API + const loadCollidersFromJSON = async () => { + setIsLoading(true); + try { + const token = localStorage.getItem('token'); + const response = await fetch(`/api/colliders/city/${cityId}`, { + headers: { Authorization: `Bearer ${token}` } + }); + + if (!response.ok) { + console.log('JSON файл не найден, создаем новый'); + collidersRef.current = []; + return; + } + + const data = await response.json(); + collidersRef.current.forEach(c => sceneRef.current.remove(c.mesh)); + collidersRef.current = []; + + const list = Array.isArray(data?.colliders) ? data.colliders : []; + list.forEach(c => { + let geom; + if (c.type === 'circle') geom = new THREE.CylinderGeometry(1.5, 1.5, 2, 32); + else if (c.type === 'capsule') geom = new THREE.CapsuleGeometry(1, 2, 4, 12); + else geom = new THREE.BoxGeometry(2, 2, 2); + + const mesh = new THREE.Mesh(geom, colliderMaterial.clone()); + const edges = new THREE.EdgesGeometry(mesh.geometry); + const line = new THREE.LineSegments(edges, colliderEdgeMaterial.clone()); + mesh.add(line); + + mesh.position.set(c.position?.x || 0, c.position?.y || 0, c.position?.z || 0); + mesh.rotation.set(c.rotation?.x || 0, c.rotation?.y || 0, c.rotation?.z || 0); + mesh.scale.set(c.scale?.x || 1, c.scale?.y || 1, c.scale?.z || 1); + + // Применяем цвет и прозрачность из JSON + if (c.color) { + const color = new THREE.Color(c.color.r, c.color.g, c.color.b); + mesh.material.color = color; + line.material.color = color; + } + if (c.opacity !== undefined) { + mesh.material.opacity = c.opacity; + } + + mesh.userData = { + type: c.type || 'box', + color: c.color || { r: 1, g: 0, b: 0 }, + opacity: c.opacity || 0.3 + }; + + sceneRef.current.add(mesh); + collidersRef.current.push({ mesh, data: c }); + }); + + console.log(`Загружено ${list.length} коллайдеров`); + } catch (error) { + console.error('Ошибка загрузки коллайдеров:', error); + } finally { + setIsLoading(false); + } + }; + + // Сохранение коллизий в JSON через API + const saveCollidersToJSON = async () => { + setIsSaving(true); + try { + const collidersData = collidersRef.current.map(c => { + const mesh = c.mesh; + return { + type: mesh.userData.type || 'box', + position: { + x: mesh.position.x, + y: mesh.position.y, + z: mesh.position.z + }, + rotation: { + x: mesh.rotation.x, + y: mesh.rotation.y, + z: mesh.rotation.z + }, + scale: { + x: mesh.scale.x, + y: mesh.scale.y, + z: mesh.scale.z + }, + color: mesh.userData.color || { r: 1, g: 0, b: 0 }, + opacity: mesh.userData.opacity || 0.3 + }; + }); + + const jsonData = { colliders: collidersData }; + + // Отправляем данные на сервер для сохранения + const token = localStorage.getItem('token'); + const response = await fetch(`/api/colliders/city/${cityId}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify(jsonData) + }); + + if (response.ok) { + const result = await response.json(); + console.log('Коллайдеры сохранены успешно:', result.message); + alert('Коллайдеры сохранены!'); + } else { + const error = await response.json(); + console.error('Ошибка сохранения коллайдеров:', error); + alert('Ошибка сохранения коллайдеров: ' + (error.error || 'Неизвестная ошибка')); + } + } catch (error) { + console.error('Ошибка сохранения:', error); + alert('Ошибка сохранения: ' + error.message); + } finally { + setIsSaving(false); + } + }; + + // Создание нового коллайдера перед камерой + const createCollider = () => { + let geom; + if (shapeType === 'circle') geom = new THREE.CylinderGeometry(1.5, 1.5, 2, 32); + else if (shapeType === 'capsule') geom = new THREE.CapsuleGeometry(1, 2, 4, 12); + else geom = new THREE.BoxGeometry(2, 2, 2); + + const mesh = new THREE.Mesh(geom, colliderMaterial.clone()); + const edges = new THREE.EdgesGeometry(mesh.geometry); + const line = new THREE.LineSegments(edges, colliderEdgeMaterial.clone()); + mesh.add(line); + + // Вычисляем позицию перед камерой + const camera = cameraRef.current; + const direction = new THREE.Vector3(); + camera.getWorldDirection(direction); + + // Позиция перед камерой на настраиваемом расстоянии + const distance = colliderCreationDistance; + const position = new THREE.Vector3(); + position.copy(camera.position); + position.add(direction.multiplyScalar(distance)); + + // Используем высоту камеры вместо принудительной установки на 0 + // Устанавливаем Y координату на уровне камеры или немного ниже + position.y = camera.position.y - 1; + + mesh.position.copy(position); + mesh.userData = { + type: shapeType, + color: { ...selectedColor }, + opacity: selectedOpacity + }; + + sceneRef.current.add(mesh); + collidersRef.current.push({ mesh, data: null }); + setSelected(mesh); + transformRef.current.attach(mesh); + + console.log(`✅ Создан коллайдер типа "${shapeType}" в позиции:`, position); + console.log(`📊 Всего коллайдеров: ${collidersRef.current.length}`); + }; + + // Функция для обновления предварительного просмотра позиции коллайдера + const updatePreview = () => { + if (!showPreview || !cameraRef.current || !sceneRef.current) return; + + // Удаляем предыдущий предварительный просмотр + if (previewRef.current) { + sceneRef.current.remove(previewRef.current); + } + + // Создаем новый предварительный просмотр + let geom; + if (shapeType === 'circle') geom = new THREE.CylinderGeometry(1.5, 1.5, 2, 32); + else if (shapeType === 'capsule') geom = new THREE.CapsuleGeometry(1, 2, 4, 12); + else geom = new THREE.BoxGeometry(2, 2, 2); + + const previewMaterial = new THREE.MeshBasicMaterial({ + color: new THREE.Color(selectedColor.r, selectedColor.g, selectedColor.b), + transparent: true, + opacity: selectedOpacity * 0.5, + wireframe: true + }); + + const previewMesh = new THREE.Mesh(geom, previewMaterial); + + // Вычисляем позицию перед камерой + const camera = cameraRef.current; + const direction = new THREE.Vector3(); + camera.getWorldDirection(direction); + + const distance = colliderCreationDistance; + const position = new THREE.Vector3(); + position.copy(camera.position); + position.add(direction.multiplyScalar(distance)); + position.y = Math.max(0, position.y - 2); + + previewMesh.position.copy(position); + previewRef.current = previewMesh; + sceneRef.current.add(previewMesh); + }; + + // Обновляем предварительный просмотр при изменении параметров + useEffect(() => { + updatePreview(); + }, [showPreview, shapeType, selectedColor, selectedOpacity, colliderCreationDistance]); + + // Обновление данных коллайдера + const updateColliderData = (mesh) => { + const collider = collidersRef.current.find(c => c.mesh === mesh); + if (collider) { + collider.data = { + type: mesh.userData.type, + position: { x: mesh.position.x, y: mesh.position.y, z: mesh.position.z }, + rotation: { x: mesh.rotation.x, y: mesh.rotation.y, z: mesh.rotation.z }, + scale: { x: mesh.scale.x, y: mesh.scale.y, z: mesh.scale.z }, + color: mesh.userData.color, + opacity: mesh.userData.opacity + }; + } + }; + + // Функция для обновления параметров выбранного коллайдера + const updateSelectedColliderParameters = () => { + if (!selected) return; + + // Обновляем позицию + selected.position.set(colliderPosition.x, colliderPosition.y, colliderPosition.z); + + // Обновляем поворот + selected.rotation.set(colliderRotation.x, colliderRotation.y, colliderRotation.z); + + // Обновляем масштаб + selected.scale.set(colliderScale.x, colliderScale.y, colliderScale.z); + + // Обновляем данные + updateColliderData(selected); + + // Обновляем TransformControls + if (transformRef.current) { + transformRef.current.updateMatrixWorld(); + } + + console.log('Параметры коллайдера обновлены:', { position: colliderPosition, rotation: colliderRotation, scale: colliderScale }); + }; + + // Функция для дублирования выбранного коллайдера + const duplicateSelectedCollider = () => { + if (!selected) return; + + // Создаем копию геометрии + let geom; + if (selected.userData.type === 'circle') geom = new THREE.CylinderGeometry(1.5, 1.5, 2, 32); + else if (selected.userData.type === 'capsule') geom = new THREE.CapsuleGeometry(1, 2, 4, 12); + else geom = new THREE.BoxGeometry(2, 2, 2); + + // Создаем новый материал с правильными параметрами + const newMaterial = new THREE.MeshBasicMaterial({ + color: selected.material.color.clone(), + transparent: true, + opacity: selected.material.opacity, + wireframe: false + }); + + const mesh = new THREE.Mesh(geom, newMaterial); + const edges = new THREE.EdgesGeometry(mesh.geometry); + const line = new THREE.LineSegments(edges, new THREE.LineBasicMaterial({ color: 0x000000 })); + mesh.add(line); + + // Копируем параметры с небольшим смещением + mesh.position.copy(selected.position); + mesh.position.add(new THREE.Vector3(2, 0, 2)); // Смещаем на 2 единицы + + mesh.rotation.copy(selected.rotation); + mesh.scale.copy(selected.scale); + + // Обновляем параметры в UI для нового коллайдера + setColliderPosition({ x: mesh.position.x, y: mesh.position.y, z: mesh.position.z }); + setColliderRotation({ x: mesh.rotation.x, y: mesh.rotation.y, z: mesh.rotation.z }); + setColliderScale({ x: mesh.scale.x, y: mesh.scale.y, z: mesh.scale.z }); + + mesh.userData = { + type: selected.userData.type, + color: { ...selected.userData.color }, + opacity: selected.userData.opacity + }; + + sceneRef.current.add(mesh); + collidersRef.current.push({ mesh, data: null }); + setSelected(mesh); + transformRef.current.attach(mesh); + + console.log('✅ Коллайдер дублирован'); + console.log(`📊 Всего коллайдеров: ${collidersRef.current.length}`); + console.log('📐 Параметры трансформации скопированы:', { + position: { x: mesh.position.x, y: mesh.position.y, z: mesh.position.z }, + rotation: { x: mesh.rotation.x, y: mesh.rotation.y, z: mesh.rotation.z }, + scale: { x: mesh.scale.x, y: mesh.scale.y, z: mesh.scale.z } + }); + }; + + // Функция для телепортации камеры к интерьеру + const teleportToInterior = (interiorId) => { + const interior = interiors.find(i => i.id === interiorId); + if (!interior || !cameraRef.current || !orbitRef.current) return; + + const camera = cameraRef.current; + const orbit = orbitRef.current; + + // Телепортируем камеру к интерьеру + camera.position.set(interior.pos_x, interior.pos_y + 10, interior.pos_z); + orbit.target.set(interior.pos_x, interior.pos_y, interior.pos_z); + orbit.update(); + + console.log(`Телепорт к интерьеру ${interiorId}:`, { x: interior.pos_x, y: interior.pos_y, z: interior.pos_z }); + }; + + // Функция для переключения режима TransformControls + const switchTransformMode = (mode) => { + setTransformMode(mode); + if (transformRef.current) { + transformRef.current.setMode(mode); + } + console.log(`Режим TransformControls изменен на: ${mode}`); + }; + + // Функция для отладки коллайдеров + const debugColliders = () => { + console.log('🔍 Отладка коллайдеров:'); + console.log('📊 Всего коллайдеров:', collidersRef.current.length); + console.log('🎯 Выбранный коллайдер:', selected); + + collidersRef.current.forEach((collider, index) => { + console.log(`📦 Коллайдер ${index}:`, { + mesh: collider.mesh, + data: collider.data, + position: collider.mesh.position, + userData: collider.mesh.userData + }); + }); + }; + + // Обновление цвета выбранного коллайдера + const updateSelectedColliderColor = () => { + if (selected) { + const color = new THREE.Color(selectedColor.r, selectedColor.g, selectedColor.b); + selected.material.color = color; + selected.children[0].material.color = color; // Обновляем цвет линий + selected.userData.color = { ...selectedColor }; + selected.userData.opacity = selectedOpacity; + selected.material.opacity = selectedOpacity; + updateColliderData(selected); + } + }; + + // Удаление выбранного коллайдера + const deleteSelected = () => { + if (!selected) { + console.log('❌ Нет выбранного коллайдера для удаления'); + return; + } + + console.log('🗑️ Удаляем коллайдер:', selected); + console.log('📊 Всего коллайдеров до удаления:', collidersRef.current.length); + + // Удаляем из сцены + sceneRef.current.remove(selected); + + // Удаляем из массива коллайдеров + const beforeLength = collidersRef.current.length; + collidersRef.current = collidersRef.current.filter(c => c.mesh !== selected); + const afterLength = collidersRef.current.length; + + console.log(`📊 Коллайдеров до: ${beforeLength}, после: ${afterLength}`); + + // Отключаем TransformControls + transformRef.current.detach(); + setSelected(null); + + console.log('✅ Коллайдер успешно удален'); + }; + + // Обработка клика по объекту + const handleClick = (event) => { + const mouse = new THREE.Vector2(); + mouse.x = (event.clientX / window.innerWidth) * 2 - 1; + mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; + + const raycaster = new THREE.Raycaster(); + raycaster.setFromCamera(mouse, cameraRef.current); + + const intersects = raycaster.intersectObjects(collidersRef.current.map(c => c.mesh)); + + if (intersects.length > 0) { + const clickedMesh = intersects[0].object; + setSelected(clickedMesh); + transformRef.current.attach(clickedMesh); + + console.log('🎯 Выбран коллайдер:', clickedMesh); + console.log('📊 Всего коллайдеров в массиве:', collidersRef.current.length); + + // Обновляем цветовую палитру + setSelectedColor(clickedMesh.userData.color || { r: 1, g: 0, b: 0 }); + setSelectedOpacity(clickedMesh.userData.opacity || 0.3); + + // Обновляем параметры коллайдера + setColliderPosition({ x: clickedMesh.position.x, y: clickedMesh.position.y, z: clickedMesh.position.z }); + setColliderRotation({ x: clickedMesh.rotation.x, y: clickedMesh.rotation.y, z: clickedMesh.rotation.z }); + setColliderScale({ x: clickedMesh.scale.x, y: clickedMesh.scale.y, z: clickedMesh.scale.z }); + } else { + setSelected(null); + transformRef.current.detach(); + console.log('❌ Клик мимо коллайдера'); + } + }; + + // Обработка движения мыши для позиционирования + const handleMouseMove = (event) => { + const mouse = new THREE.Vector2(); + mouse.x = (event.clientX / window.innerWidth) * 2 - 1; + mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; + + const raycaster = new THREE.Raycaster(); + raycaster.setFromCamera(mouse, cameraRef.current); + + const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0); + const intersectPoint = new THREE.Vector3(); + raycaster.ray.intersectPlane(plane, intersectPoint); + + setCursorXZ({ x: intersectPoint.x, z: intersectPoint.z }); + }; + + return ( +
+ {/* Панель управления */} +
+

Редактор коллизий

+ + {/* Выбор города */} +
+ + +
+ + {/* Кнопки загрузки и сохранения */} +
+ + + +
+ + {/* Настройки коллайдера */} +
+

Создание коллайдера

+ +
+ + +
+ +
+ + setColliderCreationDistance(parseInt(e.target.value))} + style={{ width: '100%', marginTop: '5px' }} + /> +
+ +
+ +
+ + +
+ + {/* Настройки цвета и прозрачности */} + {selected && ( +
+

Настройки выбранного коллайдера

+ + {/* Управление параметрами */} +
+

Параметры трансформации

+ + {/* Позиция */} +
+ +
+ setColliderPosition({...colliderPosition, x: parseFloat(e.target.value) || 0})} + style={{ width: '60px', padding: '2px', fontSize: '11px' }} + placeholder="X" + /> + setColliderPosition({...colliderPosition, y: parseFloat(e.target.value) || 0})} + style={{ width: '60px', padding: '2px', fontSize: '11px' }} + placeholder="Y" + /> + setColliderPosition({...colliderPosition, z: parseFloat(e.target.value) || 0})} + style={{ width: '60px', padding: '2px', fontSize: '11px' }} + placeholder="Z" + /> +
+
+ + {/* Поворот */} +
+ +
+ setColliderRotation({...colliderRotation, x: parseFloat(e.target.value) || 0})} + style={{ width: '60px', padding: '2px', fontSize: '11px' }} + placeholder="X" + /> + setColliderRotation({...colliderRotation, y: parseFloat(e.target.value) || 0})} + style={{ width: '60px', padding: '2px', fontSize: '11px' }} + placeholder="Y" + /> + setColliderRotation({...colliderRotation, z: parseFloat(e.target.value) || 0})} + style={{ width: '60px', padding: '2px', fontSize: '11px' }} + placeholder="Z" + /> +
+
+ + {/* Масштаб */} +
+ +
+ setColliderScale({...colliderScale, x: parseFloat(e.target.value) || 0.1})} + style={{ width: '60px', padding: '2px', fontSize: '11px' }} + placeholder="X" + /> + setColliderScale({...colliderScale, y: parseFloat(e.target.value) || 0.1})} + style={{ width: '60px', padding: '2px', fontSize: '11px' }} + placeholder="Y" + /> + setColliderScale({...colliderScale, z: parseFloat(e.target.value) || 0.1})} + style={{ width: '60px', padding: '2px', fontSize: '11px' }} + placeholder="Z" + /> +
+
+ + +
+ + {/* Переключение режимов TransformControls */} +
+

Режим трансформации

+ +
+ + + + + +
+
+ +
+ +
+
setShowColorPicker(!showColorPicker)} + /> + + RGB({Math.round(selectedColor.r * 255)}, {Math.round(selectedColor.g * 255)}, {Math.round(selectedColor.b * 255)}) + +
+ + {showColorPicker && ( +
+
+ + setSelectedColor({...selectedColor, r: parseFloat(e.target.value)})} + style={{ width: '100%' }} + /> +
+
+ + setSelectedColor({...selectedColor, g: parseFloat(e.target.value)})} + style={{ width: '100%' }} + /> +
+
+ + setSelectedColor({...selectedColor, b: parseFloat(e.target.value)})} + style={{ width: '100%' }} + /> +
+
+ )} +
+ +
+ + setSelectedOpacity(parseFloat(e.target.value))} + style={{ width: '100%', marginTop: '5px' }} + /> +
+ + + + + + + + +
+ )} + + {/* Управление камерой */} +
+

Управление камерой

+ +
+ + setCameraSpeed(parseInt(e.target.value))} + style={{ width: '100%', marginTop: '5px' }} + /> +
+ +
+ + + + + +
+ +
+

Клавиши управления:

+

W / ↑ - Вперед

+

S / ↓ - Назад

+

A / ← - Влево

+

D / → - Вправо

+

Q / PageUp - Вверх

+

E / PageDown - Вниз

+

R - Сброс позиции

+

Мышь - Поворот камеры

+

Колесо мыши - Приближение

+
+
+ + {/* Телепорт к интерьерам */} +
+

Телепорт к интерьерам

+ + {interiors.length > 0 ? ( +
+ {interiors.map(interior => ( + + ))} +
+ ) : ( +

+ Интерьеры не найдены +

+ )} +
+ + {/* Информация */} +
+

Коллайдеров: {collidersRef.current.length}

+

Выбран: {selected ? 'Да' : 'Нет'}

+

Позиция курсора: ({cursorXZ.x.toFixed(2)}, {cursorXZ.z.toFixed(2)})

+ {cameraRef.current && ( +

Позиция камеры: ({cameraRef.current.position.x.toFixed(2)}, {cameraRef.current.position.y.toFixed(2)}, {cameraRef.current.position.z.toFixed(2)})

+ )} +
+
+ + {/* 3D сцена */} +
+
+ ); +} diff --git a/src/test-collision.js b/src/test-collision.js new file mode 100644 index 0000000..4bcb3cc --- /dev/null +++ b/src/test-collision.js @@ -0,0 +1,70 @@ +import { GameCore } from './modules/GameCore.js'; + +/** + * Тестовый класс для демонстрации системы коллизий в интерьерах + */ +class CollisionTest { + constructor() { + this.gameCore = null; + this.container = null; + } + + /** + * Инициализация теста + */ + async init() { + // Создаем контейнер для игры + this.container = document.createElement('div'); + this.container.style.width = '100vw'; + this.container.style.height = '100vh'; + this.container.style.position = 'fixed'; + this.container.style.top = '0'; + this.container.style.left = '0'; + document.body.appendChild(this.container); + + // Инициализируем игровое ядро + this.gameCore = new GameCore(this.container); + + console.log('Тест системы коллизий инициализирован'); + console.log('Управление:'); + console.log('- WASD или стрелки для движения'); + console.log('- Мышь для поворота камеры в интерьере'); + console.log('- Клик по объектам города для входа в интерьер'); + } + + /** + * Запуск теста + */ + start() { + console.log('Тест запущен'); + } + + /** + * Остановка теста + */ + stop() { + if (this.gameCore) { + this.gameCore.dispose(); + } + if (this.container && this.container.parentNode) { + this.container.parentNode.removeChild(this.container); + } + console.log('Тест остановлен'); + } +} + +// Автоматический запуск теста при загрузке страницы +if (typeof window !== 'undefined') { + window.addEventListener('load', async () => { + const test = new CollisionTest(); + await test.init(); + test.start(); + + // Обработка закрытия страницы + window.addEventListener('beforeunload', () => { + test.stop(); + }); + }); +} + +export { CollisionTest }; diff --git a/test-advanced-collision-editor.js b/test-advanced-collision-editor.js new file mode 100644 index 0000000..82cc3b6 --- /dev/null +++ b/test-advanced-collision-editor.js @@ -0,0 +1,131 @@ +// Тест расширенного функционала редактора коллизий +// Файл: test-advanced-collision-editor.js + +console.log('🚀 Тестирование расширенного функционала редактора коллизий'); +console.log(''); + +console.log('✨ Новые возможности:'); +console.log(''); + +console.log('1. 📐 Управление параметрами коллайдера:'); +console.log(' - Позиция (X, Y, Z) - точное позиционирование'); +console.log(' - Поворот (X, Y, Z) - в радианах'); +console.log(' - Масштаб (X, Y, Z) - изменение размера'); +console.log(' - Кнопка "Применить параметры" для обновления'); +console.log(''); + +console.log('2. 🔄 Дублирование коллайдеров:'); +console.log(' - Кнопка "Дублировать коллайдер"'); +console.log(' - Копирует все параметры (позиция, поворот, масштаб, цвет, прозрачность)'); +console.log(' - Создает копию со смещением на 2 единицы'); +console.log(' - Автоматически выбирает новый коллайдер'); +console.log(''); + +console.log('3. 🗑️ Удаление коллайдеров:'); +console.log(' - Кнопка "Удалить коллайдер"'); +console.log(' - Удаляет выбранный коллайдер из сцены'); +console.log(' - Очищает TransformControls'); +console.log(' - Обновляет список коллайдеров'); +console.log(''); + +console.log('4. 🚀 Телепорт к интерьерам:'); +console.log(' - Секция "Телепорт к интерьерам"'); +console.log(' - Список всех доступных интерьеров'); +console.log(' - Показывает координаты каждого интерьера'); +console.log(' - Мгновенная телепортация камеры'); +console.log(' - Автоматическое наведение на интерьер'); +console.log(''); + +console.log('🎮 Как использовать:'); +console.log(''); + +console.log('📐 Управление параметрами:'); +console.log('1. Выберите коллайдер кликом'); +console.log('2. В секции "Параметры трансформации" введите новые значения'); +console.log('3. Нажмите "Применить параметры"'); +console.log('4. Коллайдер обновится в реальном времени'); +console.log(''); + +console.log('🔄 Дублирование:'); +console.log('1. Выберите коллайдер для копирования'); +console.log('2. Нажмите "Дублировать коллайдер"'); +console.log('3. Новый коллайдер появится рядом с оригиналом'); +console.log('4. Новый коллайдер автоматически выберется'); +console.log(''); + +console.log('🗑️ Удаление:'); +console.log('1. Выберите коллайдер для удаления'); +console.log('2. Нажмите "Удалить коллайдер"'); +console.log('3. Коллайдер исчезнет из сцены'); +console.log(''); + +console.log('🚀 Телепорт:'); +console.log('1. В секции "Телепорт к интерьерам" выберите интерьер'); +console.log('2. Камера мгновенно переместится к интерьеру'); +console.log('3. Камера автоматически наведется на интерьер'); +console.log(''); + +console.log('🔧 Технические детали:'); +console.log(''); + +console.log('📐 Параметры трансформации:'); +console.log('- Позиция: точные координаты в 3D пространстве'); +console.log('- Поворот: углы в радианах (0 = 0°, π/2 = 90°, π = 180°)'); +console.log('- Масштаб: множители размера (1.0 = оригинальный размер)'); +console.log('- Валидация: минимальный масштаб 0.1'); +console.log('- Шаг: 0.1 для точной настройки'); +console.log(''); + +console.log('🔄 Дублирование:'); +console.log('- Копирует геометрию (box/circle/capsule)'); +console.log('- Копирует материал и цвет'); +console.log('- Копирует все трансформации'); +console.log('- Смещение: +2 по X и Z для избежания наложения'); +console.log('- Автоматическое обновление TransformControls'); +console.log(''); + +console.log('🗑️ Удаление:'); +console.log('- Удаление из Three.js сцены'); +console.log('- Удаление из массива collidersRef'); +console.log('- Отключение TransformControls'); +console.log('- Сброс выбранного объекта'); +console.log(''); + +console.log('🚀 Телепорт:'); +console.log('- Загрузка интерьеров через API /api/interiors'); +console.log('- Позиционирование камеры: (pos_x, pos_y + 10, pos_z)'); +console.log('- Наведение OrbitControls на интерьер'); +console.log('- Прокручиваемый список интерьеров'); +console.log('- Отображение координат для каждого интерьера'); +console.log(''); + +console.log('🎯 Преимущества:'); +console.log(''); + +console.log('✅ Точность:'); +console.log('- Числовые поля для точного ввода параметров'); +console.log('- Поддержка десятичных значений'); +console.log('- Валидация входных данных'); +console.log(''); + +console.log('✅ Удобство:'); +console.log('- Автоматическое обновление при выборе коллайдера'); +console.log('- Мгновенная обратная связь'); +console.log('- Интуитивный интерфейс'); +console.log(''); + +console.log('✅ Производительность:'); +console.log('- Эффективное дублирование объектов'); +console.log('- Оптимизированное удаление'); +console.log('- Быстрая телепортация'); +console.log(''); + +console.log('✅ Функциональность:'); +console.log('- Полный контроль над коллайдерами'); +console.log('- Быстрое перемещение по сцене'); +console.log('- Удобное создание похожих объектов'); +console.log(''); + +console.log('🚀 Готово к тестированию!'); +console.log('Откройте: http://localhost:4000/enhanced-collision-editor'); + diff --git a/test-camera-controls.js b/test-camera-controls.js new file mode 100644 index 0000000..25324a1 --- /dev/null +++ b/test-camera-controls.js @@ -0,0 +1,73 @@ +// Тест улучшенного управления камерой в редакторе коллизий +// Файл: test-camera-controls.js + +console.log('🎮 Тестирование улучшенного управления камерой'); +console.log(''); + +console.log('📋 Доступные функции:'); +console.log('1. Управление клавиатурой:'); +console.log(' - W / ↑ - Движение вперед'); +console.log(' - S / ↓ - Движение назад'); +console.log(' - A / ← - Движение влево'); +console.log(' - D / → - Движение вправо'); +console.log(' - Q / PageUp - Движение вверх'); +console.log(' - E / PageDown - Движение вниз'); +console.log(' - R - Сброс позиции камеры'); +console.log(''); + +console.log('2. Управление мышью:'); +console.log(' - Левая кнопка + движение - Поворот камеры'); +console.log(' - Правая кнопка + движение - Панорамирование'); +console.log(' - Колесо мыши - Приближение/отдаление'); +console.log(''); + +console.log('3. Настройки в интерфейсе:'); +console.log(' - Слайдер скорости движения (1-20)'); +console.log(' - Кнопка "Сброс камеры"'); +console.log(' - Кнопка "Вид сверху"'); +console.log(' - Кнопка "Вид сбоку"'); +console.log(''); + +console.log('4. Улучшенные OrbitControls:'); +console.log(' - Плавное движение с демпфированием'); +console.log(' - Настраиваемая скорость панорамирования'); +console.log(' - Настраиваемая скорость приближения'); +console.log(' - Настраиваемая скорость поворота'); +console.log(' - Ограничения расстояния (5-500 единиц)'); +console.log(''); + +console.log('🚀 Как протестировать:'); +console.log('1. Откройте редактор: http://localhost:4000/enhanced-collision-editor'); +console.log('2. Попробуйте управление клавиатурой (WASD + стрелки)'); +console.log('3. Используйте Q/E для движения вверх/вниз'); +console.log('4. Нажмите R для сброса позиции'); +console.log('5. Попробуйте кнопки быстрого перемещения'); +console.log('6. Измените скорость движения слайдером'); +console.log(''); + +console.log('✨ Особенности:'); +console.log('- Движение камеры синхронизировано с OrbitControls'); +console.log('- Поддержка множественных клавиш одновременно'); +console.log('- Плавное движение без рывков'); +console.log('- Информация о позиции камеры в реальном времени'); +console.log('- Совместимость с существующим функционалом редактора'); +console.log(''); + +console.log('🎯 Рекомендации по использованию:'); +console.log('- Используйте WASD для точного позиционирования'); +console.log('- Стрелки клавиатуры для альтернативного управления'); +console.log('- Q/E для вертикального движения'); +console.log('- R для быстрого возврата к исходной позиции'); +console.log('- Кнопки видов для быстрого переключения ракурсов'); +console.log('- Настройте скорость под свои предпочтения'); +console.log(''); + +console.log('🔧 Технические детали:'); +console.log('- Используется requestAnimationFrame для плавности'); +console.log('- Обработчики keydown/keyup для отзывчивости'); +console.log('- Векторная математика для корректного движения'); +console.log('- Синхронизация с target OrbitControls'); +console.log('- Автоматическая очистка обработчиков событий'); +console.log(''); + +console.log('✅ Готово к тестированию!'); diff --git a/test-collider-creation.js b/test-collider-creation.js new file mode 100644 index 0000000..284e51e --- /dev/null +++ b/test-collider-creation.js @@ -0,0 +1,53 @@ +// Тест улучшенного создания коллайдеров в редакторе +// Файл: test-collider-creation.js + +console.log('🎯 Тестирование улучшенного создания коллайдеров'); +console.log(''); + +console.log('✨ Что было исправлено:'); +console.log('1. Коллайдеры теперь создаются ПЕРЕД камерой, а не в начале координат'); +console.log('2. Добавлена настройка расстояния создания (5-50 единиц)'); +console.log('3. Добавлен предварительный просмотр позиции коллайдера'); +console.log('4. Предварительный просмотр обновляется в реальном времени'); +console.log(''); + +console.log('🎮 Как использовать:'); +console.log('1. Откройте редактор: http://localhost:4000/enhanced-collision-editor'); +console.log('2. Настройте расстояние создания коллайдера слайдером'); +console.log('3. Включите "Показать предварительный просмотр"'); +console.log('4. Перемещайте камеру - предварительный просмотр следует за ней'); +console.log('5. Нажмите "Создать коллайдер" - он появится перед камерой'); +console.log(''); + +console.log('🔧 Технические детали:'); +console.log('- Используется camera.getWorldDirection() для определения направления'); +console.log('- Позиция вычисляется: camera.position + direction * distance'); +console.log('- Y координата устанавливается на уровне земли (0) или выше'); +console.log('- Предварительный просмотр отображается как wireframe с половинной прозрачностью'); +console.log('- Предварительный просмотр автоматически удаляется после создания коллайдера'); +console.log(''); + +console.log('⚙️ Настройки:'); +console.log('- Расстояние: 5-50 единиц (по умолчанию 10)'); +console.log('- Предварительный просмотр: включен/выключен'); +console.log('- Тип коллайдера: коробка/цилиндр/капсула'); +console.log('- Цвет и прозрачность применяются к предварительному просмотру'); +console.log(''); + +console.log('🎯 Преимущества:'); +console.log('- Точное позиционирование коллайдеров'); +console.log('- Визуальная обратная связь перед созданием'); +console.log('- Настраиваемое расстояние для разных ситуаций'); +console.log('- Интуитивное управление'); +console.log('- Экономия времени на позиционировании'); +console.log(''); + +console.log('🚀 Рекомендации:'); +console.log('- Используйте предварительный просмотр для точного размещения'); +console.log('- Настройте расстояние в зависимости от размера объектов'); +console.log('- Перемещайте камеру для оптимального угла обзора'); +console.log('- Используйте разные типы коллайдеров для разных объектов'); +console.log(''); + +console.log('✅ Готово к тестированию!'); + diff --git a/test-collider-deletion-debug.js b/test-collider-deletion-debug.js new file mode 100644 index 0000000..6291975 --- /dev/null +++ b/test-collider-deletion-debug.js @@ -0,0 +1,141 @@ +// Тест отладки проблемы с удалением коллайдеров +// Файл: test-collider-deletion-debug.js + +console.log('🔍 Отладка проблемы с удалением коллайдеров'); +console.log(''); + +console.log('❓ Проблема:'); +console.log('Созданные и дублированные коллайдеры не удаляются, а старые удаляются'); +console.log(''); + +console.log('🔧 Добавленная отладка:'); +console.log(''); + +console.log('1. 📊 Отладочная информация в функции удаления:'); +console.log(' - Логирование выбранного коллайдера'); +console.log(' - Подсчет коллайдеров до и после удаления'); +console.log(' - Проверка успешности удаления'); +console.log(''); + +console.log('2. 📊 Отладочная информация в функциях создания:'); +console.log(' - Логирование создания нового коллайдера'); +console.log(' - Подсчет общего количества коллайдеров'); +console.log(' - Проверка добавления в массив'); +console.log(''); + +console.log('3. 📊 Отладочная информация в функции дублирования:'); +console.log(' - Логирование дублирования коллайдера'); +console.log(' - Подсчет общего количества коллайдеров'); +console.log(' - Проверка добавления в массив'); +console.log(''); + +console.log('4. 🎯 Отладочная информация в функции выбора:'); +console.log(' - Логирование выбранного коллайдера'); +console.log(' - Подсчет коллайдеров в массиве'); +console.log(' - Проверка корректности выбора'); +console.log(''); + +console.log('5. 🔍 Функция отладки коллайдеров:'); +console.log(' - Кнопка "🔍 Отладка коллайдеров"'); +console.log(' - Показывает все коллайдеры в массиве'); +console.log(' - Отображает их свойства и userData'); +console.log(''); + +console.log('🧪 Как тестировать:'); +console.log(''); + +console.log('1. 📦 Создание коллайдера:'); +console.log(' - Нажмите "Создать коллайдер"'); +console.log(' - Проверьте консоль: должно появиться "✅ Создан коллайдер"'); +console.log(' - Проверьте счетчик: "📊 Всего коллайдеров: X"'); +console.log(''); + +console.log('2. 🔄 Дублирование коллайдера:'); +console.log(' - Выберите коллайдер'); +console.log(' - Нажмите "Дублировать коллайдер"'); +console.log(' - Проверьте консоль: должно появиться "✅ Коллайдер дублирован"'); +console.log(' - Проверьте счетчик: количество должно увеличиться'); +console.log(''); + +console.log('3. 🎯 Выбор коллайдера:'); +console.log(' - Кликните по коллайдеру'); +console.log(' - Проверьте консоль: должно появиться "🎯 Выбран коллайдер"'); +console.log(' - Проверьте счетчик: "📊 Всего коллайдеров в массиве: X"'); +console.log(''); + +console.log('4. 🗑️ Удаление коллайдера:'); +console.log(' - Выберите коллайдер'); +console.log(' - Нажмите "Удалить коллайдер"'); +console.log(' - Проверьте консоль:'); +console.log(' * "🗑️ Удаляем коллайдер: [объект]"'); +console.log(' * "📊 Всего коллайдеров до удаления: X"'); +console.log(' * "📊 Коллайдеров до: X, после: Y"'); +console.log(' * "✅ Коллайдер успешно удален"'); +console.log(''); + +console.log('5. 🔍 Отладка коллайдеров:'); +console.log(' - Нажмите кнопку "🔍 Отладка коллайдеров"'); +console.log(' - Проверьте консоль:'); +console.log(' * "🔍 Отладка коллайдеров:"'); +console.log(' * "📊 Всего коллайдеров: X"'); +console.log(' * "🎯 Выбранный коллайдер: [объект или null]"'); +console.log(' * "📦 Коллайдер 0: {mesh, data, position, userData}"'); +console.log(''); + +console.log('🔍 Возможные причины проблемы:'); +console.log(''); + +console.log('1. 🔗 Проблема с ссылками:'); +console.log(' - Коллайдер может быть добавлен в сцену, но не в массив'); +console.log(' - Или наоборот: в массив, но не в сцену'); +console.log(' - Проверьте логи создания/дублирования'); +console.log(''); + +console.log('2. 🎯 Проблема с выбором:'); +console.log(' - Raycaster может не находить новые коллайдеры'); +console.log(' - Проверьте логи выбора коллайдера'); +console.log(''); + +console.log('3. 🗑️ Проблема с удалением:'); +console.log(' - Фильтрация массива может работать неправильно'); +console.log(' - Проверьте логи удаления'); +console.log(''); + +console.log('4. 🔄 Проблема с состоянием:'); +console.log(' - React состояние может не обновляться'); +console.log(' - Проверьте, обновляется ли selected'); +console.log(''); + +console.log('📋 Чек-лист для диагностики:'); +console.log(''); + +console.log('✅ Создание:'); +console.log('- [ ] В консоли появляется "✅ Создан коллайдер"'); +console.log('- [ ] Счетчик коллайдеров увеличивается'); +console.log('- [ ] Коллайдер виден в сцене'); +console.log('- [ ] Коллайдер можно выбрать'); +console.log(''); + +console.log('✅ Дублирование:'); +console.log('- [ ] В консоли появляется "✅ Коллайдер дублирован"'); +console.log('- [ ] Счетчик коллайдеров увеличивается'); +console.log('- [ ] Новый коллайдер виден в сцене'); +console.log('- [ ] Новый коллайдер можно выбрать'); +console.log(''); + +console.log('✅ Выбор:'); +console.log('- [ ] В консоли появляется "🎯 Выбран коллайдер"'); +console.log('- [ ] TransformControls активируется'); +console.log('- [ ] Параметры обновляются в UI'); +console.log(''); + +console.log('✅ Удаление:'); +console.log('- [ ] В консоли появляется "🗑️ Удаляем коллайдер"'); +console.log('- [ ] Счетчик коллайдеров уменьшается'); +console.log('- [ ] Коллайдер исчезает из сцены'); +console.log('- [ ] TransformControls отключается'); +console.log(''); + +console.log('🚀 Готово к тестированию!'); +console.log('Откройте: http://localhost:4000/enhanced-collision-editor'); +console.log('Откройте консоль браузера для просмотра логов'); diff --git a/test-colliders.js b/test-colliders.js new file mode 100644 index 0000000..d737ecd --- /dev/null +++ b/test-colliders.js @@ -0,0 +1,97 @@ +// Тестовый скрипт для проверки размеров коллайдеров +// Запустите этот код в консоли браузера для тестирования + +console.log('🧪 Тестирование системы коллайдеров'); + +// Проверяем текущую конфигурацию +console.log('Текущая конфигурация:', window.colliderConfig); + +// Тестируем разные коэффициенты +const testMultipliers = [1.0, 2.0, 3.0, 4.0, 5.0]; + +console.log('📊 Тестируем разные коэффициенты:'); +testMultipliers.forEach(multiplier => { + console.log(`\n--- Тест с коэффициентом ${multiplier} ---`); + window.updateColliderSize(multiplier); + + // Ждем немного для обновления + setTimeout(() => { + console.log('Размеры коллайдеров обновлены'); + window.testCollisions(); + }, 100); +}); + +// Функция для быстрого тестирования +window.quickTest = () => { + console.log('🚀 Быстрый тест коллайдеров и объектов интерьера'); + + // Включаем режим отладки + window.toggleColliderDebug(); + + // Тестируем цвета коллайдеров + setTimeout(() => { + console.log('Тестируем синий цвет коллайдеров...'); + window.setColliderColor(0, 0, 1); // Синий + }, 1000); + + setTimeout(() => { + console.log('Тестируем зеленый цвет коллайдеров...'); + window.setColliderColor(0, 1, 0); // Зеленый + }, 2000); + + // Тестируем цвета объектов интерьера + setTimeout(() => { + console.log('Тестируем красный цвет объектов интерьера...'); + window.setInteriorObjectColor(1, 0, 0); // Красный + }, 3000); + + setTimeout(() => { + console.log('Тестируем желтый цвет объектов интерьера...'); + window.setInteriorObjectColor(1, 1, 0); // Желтый + }, 4000); + + // Тестируем прозрачность + setTimeout(() => { + console.log('Тестируем полупрозрачность объектов интерьера...'); + window.setInteriorObjectOpacity(0.5); + }, 5000); + + setTimeout(() => { + console.log('Тестируем полную прозрачность объектов интерьера...'); + window.setInteriorObjectOpacity(0.1); + }, 6000); + + // Тестируем случайные цвета коллайдеров + setTimeout(() => { + console.log('Тестируем случайные цвета коллайдеров...'); + window.randomizeColliderColors(); + }, 7000); + + // Возвращаем стандартные настройки + setTimeout(() => { + console.log('Возвращаем стандартные настройки...'); + window.setColliderColor(1, 0, 0); // Красный коллайдер + window.setInteriorObjectColor(1, 1, 1); // Белый объект + window.setInteriorObjectOpacity(1.0); // Полная непрозрачность + }, 8000); +}; + +console.log('✅ Тестовые функции загружены!'); +console.log('Используйте window.quickTest() для быстрого тестирования'); +console.log('Используйте window.updateColliderSize(коэффициент) для изменения размера'); +console.log('Используйте window.toggleColliderDebug() для включения/выключения визуализации'); +console.log(''); +console.log('🔧 Функции диагностики:'); +console.log('window.debugInteriorObjects() - диагностика объектов интерьера'); +console.log('window.setInteriorObjectColor(r,g,b) - цвет объектов интерьера'); +console.log('window.setAllObjectsColor(r,g,b) - цвет ВСЕХ объектов в сцене'); +console.log(''); +console.log('🎯 Целевые функции (только объекты из JSON):'); +console.log('window.setColliderObjectsColor(r,g,b) - цвет только объектов из JSON коллайдеров'); +console.log('window.applyJsonColorsToObjects() - применить цвета и прозрачность из JSON'); +console.log(''); +console.log('🎨 Примеры использования:'); +console.log('window.applyJsonColorsToObjects() - применить настройки из JSON'); +console.log('window.setColliderObjectsColor(1, 0, 0) - красный цвет объектов из JSON'); +console.log('window.setColliderObjectsColor(0, 1, 0) - зеленый цвет объектов из JSON'); +console.log('window.setAllObjectsColor(0, 0, 1) - синий цвет всех объектов (для сравнения)'); diff --git a/test-collision-editor-fixes.js b/test-collision-editor-fixes.js new file mode 100644 index 0000000..b800b12 --- /dev/null +++ b/test-collision-editor-fixes.js @@ -0,0 +1,129 @@ +// Тест исправлений редактора коллизий +// Файл: test-collision-editor-fixes.js + +console.log('🔧 Тестирование исправлений редактора коллизий'); +console.log(''); + +console.log('✅ Исправленные проблемы:'); +console.log(''); + +console.log('1. 🔄 Дублирование коллайдера:'); +console.log(' ПРОБЛЕМА: У нового коллайдера была видна только рамка, внутри пустота'); +console.log(' РЕШЕНИЕ: Исправлено создание материала при дублировании'); +console.log(' - Создается новый MeshBasicMaterial с правильными параметрами'); +console.log(' - Копируется цвет и прозрачность из оригинального коллайдера'); +console.log(' - Создается новый LineBasicMaterial для рамки'); +console.log(' - Материал правильно применяется к геометрии'); +console.log(''); + +console.log('2. 📐 Применение параметров трансформации:'); +console.log(' ПРОБЛЕМА: При нажатии "Применить параметры" ничего не происходило'); +console.log(' РЕШЕНИЕ: Добавлено обновление TransformControls'); +console.log(' - Вызывается transformRef.current.updateMatrixWorld()'); +console.log(' - TransformControls корректно отображает изменения'); +console.log(' - Параметры применяются в реальном времени'); +console.log(''); + +console.log('3. 🎛️ Переключение режимов TransformControls:'); +console.log(' НОВАЯ ФУНКЦИЯ: Добавлено переключение между осями'); +console.log(' - Кнопки "Перемещение", "Поворот", "Масштаб"'); +console.log(' - Визуальная индикация активного режима'); +console.log(' - Функция switchTransformMode() для управления'); +console.log(' - Автоматическое обновление TransformControls'); +console.log(''); + +console.log('4. 📏 Высота создания коллайдера:'); +console.log(' ПРОБЛЕМА: Коллайдер создавался на высоте 0, а не на высоте камеры'); +console.log(' РЕШЕНИЕ: Используется высота камеры для создания'); +console.log(' - position.y = camera.position.y - 1'); +console.log(' - Коллайдер создается на уровне камеры или немного ниже'); +console.log(' - Работает корректно в отрицательных координатах'); +console.log(''); + +console.log('🎮 Как использовать исправления:'); +console.log(''); + +console.log('🔄 Дублирование:'); +console.log('1. Выберите коллайдер'); +console.log('2. Нажмите "Дублировать коллайдер"'); +console.log('3. Новый коллайдер появится с правильным материалом'); +console.log('4. Цвет и прозрачность будут скопированы'); +console.log(''); + +console.log('📐 Применение параметров:'); +console.log('1. Выберите коллайдер'); +console.log('2. Измените значения в полях позиции/поворота/масштаба'); +console.log('3. Нажмите "Применить параметры"'); +console.log('4. Коллайдер обновится и TransformControls покажет изменения'); +console.log(''); + +console.log('🎛️ Режимы трансформации:'); +console.log('1. Выберите коллайдер'); +console.log('2. Нажмите одну из кнопок: "Перемещение", "Поворот", "Масштаб"'); +console.log('3. Активная кнопка подсветится зеленым'); +console.log('4. TransformControls переключится в соответствующий режим'); +console.log('5. Теперь можно перетаскивать оси для трансформации'); +console.log(''); + +console.log('📏 Создание на высоте камеры:'); +console.log('1. Переместите камеру в нужное место'); +console.log('2. Нажмите "Создать коллайдер"'); +console.log('3. Коллайдер появится перед камерой на её высоте'); +console.log('4. Работает даже в отрицательных координатах Y'); +console.log(''); + +console.log('🔧 Технические детали исправлений:'); +console.log(''); + +console.log('🔄 Дублирование материала:'); +console.log('- Создается новый MeshBasicMaterial вместо клонирования'); +console.log('- Правильно копируется цвет: selected.material.color.clone()'); +console.log('- Правильно копируется прозрачность: selected.material.opacity'); +console.log('- Создается новый LineBasicMaterial для рамки'); +console.log('- Материал применяется к новой геометрии'); +console.log(''); + +console.log('📐 Обновление TransformControls:'); +console.log('- Добавлен вызов updateMatrixWorld() после изменения параметров'); +console.log('- TransformControls корректно отображает новые значения'); +console.log('- Оси обновляются в реальном времени'); +console.log('- Поддерживается все три режима трансформации'); +console.log(''); + +console.log('🎛️ Переключение режимов:'); +console.log('- Состояние transformMode для отслеживания текущего режима'); +console.log('- Функция switchTransformMode() для переключения'); +console.log('- Вызов transformRef.current.setMode(mode)'); +console.log('- Визуальная индикация активного режима'); +console.log(''); + +console.log('📏 Высота создания:'); +console.log('- Используется camera.position.y вместо принудительного 0'); +console.log('- position.y = camera.position.y - 1 для небольшого смещения'); +console.log('- Работает в любых координатах (положительных и отрицательных)'); +console.log('- Коллайдер создается на уровне камеры'); +console.log(''); + +console.log('🎯 Преимущества исправлений:'); +console.log(''); + +console.log('✅ Надежность:'); +console.log('- Дублирование работает корректно'); +console.log('- Параметры применяются без ошибок'); +console.log('- Высота создания предсказуема'); +console.log(''); + +console.log('✅ Удобство:'); +console.log('- Визуальное переключение режимов'); +console.log('- Интуитивные кнопки управления'); +console.log('- Мгновенная обратная связь'); +console.log(''); + +console.log('✅ Функциональность:'); +console.log('- Полный контроль над трансформацией'); +console.log('- Корректное дублирование объектов'); +console.log('- Работа в любых координатах'); +console.log(''); + +console.log('🚀 Все исправления готовы к тестированию!'); +console.log('Откройте: http://localhost:4000/enhanced-collision-editor'); diff --git a/test-collision.html b/test-collision.html new file mode 100644 index 0000000..4f9ded4 --- /dev/null +++ b/test-collision.html @@ -0,0 +1,405 @@ + + + + + + Тест системы коллизий интерьеров + + + +
+ +
+
Статус: Загрузка...
+
Коллайдеры: 0
+
Позиция игрока: 0, 0, 0
+
В интерьере: false
+
+ +
+

Управление:

+

WASD - движение

+

Мышь - поворот камеры (в интерьере)

+

Клик по объекту - вход в интерьер

+

Escape - выход из интерьера

+
+

Тест коллизий:

+

В интерьере игрок не должен проходить сквозь стены и объекты

+
+ + + + diff --git a/test-duplicate-transform-fix.js b/test-duplicate-transform-fix.js new file mode 100644 index 0000000..f605372 --- /dev/null +++ b/test-duplicate-transform-fix.js @@ -0,0 +1,135 @@ +// Тест исправления дублирования параметров трансформации +// Файл: test-duplicate-transform-fix.js + +console.log('🔧 Исправление дублирования параметров трансформации'); +console.log(''); + +console.log('❓ Проблема:'); +console.log('При дублировании коллайдера параметры трансформации не передавались новому объекту'); +console.log(''); + +console.log('✅ Исправление:'); +console.log(''); + +console.log('1. 📐 Копирование параметров трансформации:'); +console.log(' - Позиция: mesh.position.copy(selected.position)'); +console.log(' - Поворот: mesh.rotation.copy(selected.rotation)'); +console.log(' - Масштаб: mesh.scale.copy(selected.scale)'); +console.log(' - Смещение: +2 по X и Z для избежания наложения'); +console.log(''); + +console.log('2. 🎛️ Обновление UI параметров:'); +console.log(' - setColliderPosition() с новыми координатами'); +console.log(' - setColliderRotation() с новыми углами'); +console.log(' - setColliderScale() с новыми размерами'); +console.log(' - UI автоматически отображает параметры нового коллайдера'); +console.log(''); + +console.log('3. 📊 Улучшенная отладка:'); +console.log(' - Логирование скопированных параметров трансформации'); +console.log(' - Отображение позиции, поворота и масштаба'); +console.log(' - Проверка корректности копирования'); +console.log(''); + +console.log('🎮 Как тестировать исправление:'); +console.log(''); + +console.log('1. 📦 Создайте коллайдер:'); +console.log(' - Нажмите "Создать коллайдер"'); +console.log(' - Коллайдер появится перед камерой'); +console.log(''); + +console.log('2. 🔧 Измените параметры:'); +console.log(' - Выберите коллайдер'); +console.log(' - Измените позицию, поворот или масштаб'); +console.log(' - Нажмите "Применить параметры"'); +console.log(' - Коллайдер обновится'); +console.log(''); + +console.log('3. 🔄 Дублируйте коллайдер:'); +console.log(' - Убедитесь, что коллайдер выбран'); +console.log(' - Нажмите "Дублировать коллайдер"'); +console.log(' - Проверьте консоль:'); +console.log(' * "✅ Коллайдер дублирован"'); +console.log(' * "📐 Параметры трансформации скопированы:"'); +console.log(' * Позиция, поворот, масштаб нового коллайдера'); +console.log(''); + +console.log('4. ✅ Проверьте результат:'); +console.log(' - Новый коллайдер должен иметь те же параметры'); +console.log(' - Плюс смещение на 2 единицы по X и Z'); +console.log(' - UI должен показать параметры нового коллайдера'); +console.log(' - TransformControls должен быть прикреплен к новому коллайдеру'); +console.log(''); + +console.log('🔍 Технические детали:'); +console.log(''); + +console.log('📐 Копирование трансформации:'); +console.log('- mesh.position.copy(selected.position) - копирует позицию'); +console.log('- mesh.position.add(new THREE.Vector3(2, 0, 2)) - добавляет смещение'); +console.log('- mesh.rotation.copy(selected.rotation) - копирует поворот'); +console.log('- mesh.scale.copy(selected.scale) - копирует масштаб'); +console.log(''); + +console.log('🎛️ Обновление UI:'); +console.log('- setColliderPosition() - обновляет поля позиции в UI'); +console.log('- setColliderRotation() - обновляет поля поворота в UI'); +console.log('- setColliderScale() - обновляет поля масштаба в UI'); +console.log('- UI автоматически синхронизируется с новым коллайдером'); +console.log(''); + +console.log('📊 Отладочная информация:'); +console.log('- Логирование всех скопированных параметров'); +console.log('- Отображение точных значений позиции, поворота, масштаба'); +console.log('- Проверка корректности копирования'); +console.log(''); + +console.log('🎯 Преимущества исправления:'); +console.log(''); + +console.log('✅ Полнота копирования:'); +console.log('- Все параметры трансформации копируются'); +console.log('- Новый коллайдер идентичен оригиналу'); +console.log('- Только позиция смещается для избежания наложения'); +console.log(''); + +console.log('✅ Удобство использования:'); +console.log('- UI показывает параметры нового коллайдера'); +console.log('- Можно сразу редактировать параметры'); +console.log('- TransformControls готов к работе'); +console.log(''); + +console.log('✅ Предсказуемость:'); +console.log('- Поведение дублирования предсказуемо'); +console.log('- Все параметры сохраняются'); +console.log('- Смещение всегда одинаковое'); +console.log(''); + +console.log('🧪 Тестовые сценарии:'); +console.log(''); + +console.log('1. 🔄 Дублирование с поворотом:'); +console.log(' - Создайте коллайдер'); +console.log(' - Поверните его (например, на 45°)'); +console.log(' - Дублируйте'); +console.log(' - Новый коллайдер должен быть повернут на тот же угол'); +console.log(''); + +console.log('2. 🔄 Дублирование с масштабом:'); +console.log(' - Создайте коллайдер'); +console.log(' - Увеличьте масштаб (например, в 2 раза)'); +console.log(' - Дублируйте'); +console.log(' - Новый коллайдер должен иметь тот же масштаб'); +console.log(''); + +console.log('3. 🔄 Дублирование с позицией:'); +console.log(' - Создайте коллайдер'); +console.log(' - Переместите его в другое место'); +console.log(' - Дублируйте'); +console.log(' - Новый коллайдер должен быть рядом (смещение +2, +2)'); +console.log(''); + +console.log('🚀 Исправление готово к тестированию!'); +console.log('Откройте: http://localhost:4000/enhanced-collision-editor'); +console.log('Проверьте дублирование коллайдеров с различными параметрами'); diff --git a/test-interior-api.html b/test-interior-api.html new file mode 100644 index 0000000..3087559 --- /dev/null +++ b/test-interior-api.html @@ -0,0 +1,297 @@ + + + + + + Тест API интерьера + + + +
+

Тест API интерьера

+ +
+

1. Проверка токена

+ +
+
+ +
+

2. Тест API интерьера

+ + +
+
+ +
+

3. Тест загрузки GLB

+ + +
+
+ +
+

4. Полный тест интерьера

+ +
+
+
+ + + +