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 [autoSave, setAutoSave] = useState(true); const [lastSaved, setLastSaved] = useState(null); const autoSaveTimeoutRef = useRef(null); const [showWireframes, setShowWireframes] = useState(true); // Новое состояние для рамок const colliderMaterial = useMemo(() => { const material = new THREE.MeshBasicMaterial({ color: new THREE.Color(selectedColor.r, selectedColor.g, selectedColor.b), transparent: true, opacity: selectedOpacity, depthWrite: false }); // Если прозрачность 0, делаем материал невидимым if (selectedOpacity === 0) { material.visible = false; material.alphaTest = 0; } else { material.visible = true; material.alphaTest = 0.1; } return material; }, [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); // Обновляем параметры в UI при изменении через TransformControls setColliderPosition({ x: selected.position.x, y: selected.position.y, z: selected.position.z }); setColliderRotation({ x: selected.rotation.x, y: selected.rotation.y, z: selected.rotation.z }); setColliderScale({ x: selected.scale.x, y: selected.scale.y, z: selected.scale.z }); console.log('🔄 Параметры обновлены через TransformControls:', { position: selected.position, rotation: selected.rotation, scale: selected.scale }); // Запускаем автоматическое сохранение triggerAutoSave(); } }); 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'); // Сначала пробуем новый API с базой данных let response = await fetch(`/api/colliders/city/${cityId}`, { headers: { Authorization: `Bearer ${token}` } }); // Если новый API недоступен (500 ошибка), пробуем старый JSON API if (!response.ok && response.status === 500) { console.log('🔄 Новый API недоступен, пробуем старый JSON API...'); response = await fetch(`/api/colliders?cityId=${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()); line.visible = showWireframes; // Учитываем настройку видимости рамок 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; // Если прозрачность 0, делаем материал невидимым if (c.opacity === 0) { mesh.material.visible = false; mesh.material.transparent = true; mesh.material.alphaTest = 0; } else { mesh.material.visible = true; mesh.material.transparent = true; mesh.material.alphaTest = 0.1; } } mesh.userData = { type: c.type || 'box', color: c.color || { r: 1, g: 0, b: 0 }, opacity: c.opacity || 0.3, id: c.id // Добавляем ID из базы данных }; 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, id: mesh.userData.id // Добавляем ID для существующих коллайдеров }; }); const jsonData = { colliders: collidersData }; // Отправляем данные на сервер для сохранения const token = localStorage.getItem('token'); // Сначала пробуем новый API с базой данных let response = await fetch(`/api/colliders/city/${cityId}`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify(jsonData) }); // Если новый API недоступен (500 ошибка), пробуем старый JSON API if (!response.ok && response.status === 500) { console.log('🔄 Новый API недоступен, пробуем старый JSON API...'); response = await fetch('/api/colliders', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify({ ...jsonData, cityId: parseInt(cityId) }) }); } 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()); line.visible = showWireframes; // Учитываем настройку видимости рамок 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, id: null // Новый коллайдер пока не имеет ID }; 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}`); // Запускаем автоматическое сохранение triggerAutoSave(); }; // Функция для обновления предварительного просмотра позиции коллайдера 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 })); line.visible = showWireframes; // Учитываем настройку видимости рамок 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, id: null // Дублированный коллайдер пока не имеет ID }; 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 } }); // Запускаем автоматическое сохранение triggerAutoSave(); }; // Функция для телепортации камеры к интерьеру 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 reloadColliders = async () => { console.log('🔄 Принудительная перезагрузка коллайдеров из БД...'); await loadCollidersFromJSON(); console.log('✅ Коллайдеры перезагружены'); }; // Функция для переключения видимости рамок const toggleWireframes = () => { const newShowWireframes = !showWireframes; setShowWireframes(newShowWireframes); // Обновляем видимость рамок у всех коллайдеров collidersRef.current.forEach(c => { const mesh = c.mesh; mesh.children.forEach(child => { if (child.type === 'LineSegments') { child.visible = newShowWireframes; } }); }); console.log(`🔲 Рамки ${newShowWireframes ? 'включены' : 'отключены'}`); }; // Функция для отладки коллайдеров 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 triggerAutoSave = () => { if (!autoSave || !cityId) return; // Очищаем предыдущий таймер if (autoSaveTimeoutRef.current) { clearTimeout(autoSaveTimeoutRef.current); } // Устанавливаем новый таймер на 2 секунды autoSaveTimeoutRef.current = setTimeout(async () => { try { await saveCollidersToJSON(); setLastSaved(new Date()); console.log('💾 Автоматическое сохранение выполнено'); } catch (error) { console.error('❌ Ошибка автоматического сохранения:', error); } }, 2000); }; // Обновление цвета выбранного коллайдера 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; // Если прозрачность 0, делаем материал невидимым if (selectedOpacity === 0) { selected.material.visible = false; selected.material.transparent = true; selected.material.alphaTest = 0; } else { selected.material.visible = true; selected.material.transparent = true; selected.material.alphaTest = 0.1; } updateColliderData(selected); } }; // Удаление выбранного коллайдера const deleteSelected = async () => { if (!selected) { console.log('❌ Нет выбранного коллайдера для удаления'); return; } console.log('🗑️ Удаляем коллайдер:', selected); console.log('📊 Всего коллайдеров до удаления:', collidersRef.current.length); // Если у коллайдера есть ID, удаляем его из базы данных if (selected.userData.id) { try { const token = localStorage.getItem('token'); const response = await fetch(`/api/colliders/${selected.userData.id}`, { method: 'DELETE', headers: { Authorization: `Bearer ${token}` } }); if (response.ok) { console.log(`✅ Коллайдер ${selected.userData.id} удален из базы данных`); } else { console.error(`❌ Ошибка удаления коллайдера ${selected.userData.id} из БД:`, await response.text()); } } catch (error) { console.error('❌ Ошибка при удалении коллайдера из БД:', error); } } else { console.log('⚠️ У коллайдера нет ID, удаляем только из фронтенда'); } // Удаляем из сцены 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('✅ Коллайдер успешно удален'); // Запускаем автоматическое сохранение для синхронизации triggerAutoSave(); }; // Обработка клика по объекту 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 allColliderObjects = []; collidersRef.current.forEach(c => { allColliderObjects.push(c.mesh); // Основной меш allColliderObjects.push(...c.mesh.children); // Рамки (дочерние объекты) }); const intersects = raycaster.intersectObjects(allColliderObjects); if (intersects.length > 0) { let clickedObject = intersects[0].object; // Если кликнули по рамке, находим родительский меш if (clickedObject.type === 'LineSegments') { clickedObject = clickedObject.parent; console.log('🎯 Кликнули по рамке, выбираем родительский меш:', clickedObject); } // Проверяем, что это действительно коллайдер из нашего списка const collider = collidersRef.current.find(c => c.mesh === clickedObject); if (!collider) { console.log('❌ Кликнули по объекту, который не является коллайдером'); return; } setSelected(clickedObject); transformRef.current.attach(clickedObject); console.log('🎯 Выбран коллайдер:', clickedObject); console.log('📊 Всего коллайдеров в массиве:', collidersRef.current.length); // Обновляем цветовую палитру setSelectedColor(clickedObject.userData.color || { r: 1, g: 0, b: 0 }); setSelectedOpacity(clickedObject.userData.opacity || 0.3); // Обновляем параметры коллайдера setColliderPosition({ x: clickedObject.position.x, y: clickedObject.position.y, z: clickedObject.position.z }); setColliderRotation({ x: clickedObject.rotation.x, y: clickedObject.rotation.y, z: clickedObject.rotation.z }); setColliderScale({ x: clickedObject.scale.x, y: clickedObject.scale.y, z: clickedObject.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 (
Клавиши управления:
W / ↑ - Вперед
S / ↓ - Назад
A / ← - Влево
D / → - Вправо
Q / PageUp - Вверх
E / PageDown - Вниз
R - Сброс позиции
Мышь - Поворот камеры
Колесо мыши - Приближение
Интерьеры не найдены
)}Коллайдеров: {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)})
)}