Files
rltn/src/pages/EnhancedCollisionEditor.jsx

1653 lines
63 KiB
React
Raw Normal View History

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 (
<div style={{ display: 'flex', height: '100vh' }}>
{/* Панель управления */}
<div style={{
width: '300px',
backgroundColor: '#2c3e50',
color: 'white',
padding: '20px',
overflowY: 'auto'
}}>
<h2>Редактор коллизий</h2>
{/* Выбор города */}
<div style={{ marginBottom: '20px' }}>
<label>Город:</label>
<select
value={cityId}
onChange={(e) => setCityId(parseInt(e.target.value))}
style={{ width: '100%', padding: '5px', marginTop: '5px' }}
>
{cities.map(city => (
<option key={city.id} value={city.id}>{city.name}</option>
))}
</select>
</div>
{/* Кнопки загрузки и сохранения */}
<div style={{ marginBottom: '20px' }}>
<button
onClick={loadCollidersFromJSON}
disabled={isLoading}
style={{
width: '100%',
padding: '10px',
marginBottom: '10px',
backgroundColor: '#3498db',
color: 'white',
border: 'none',
borderRadius: '5px',
cursor: isLoading ? 'not-allowed' : 'pointer'
}}
>
{isLoading ? 'Загрузка...' : 'Загрузить из JSON'}
</button>
<button
onClick={saveCollidersToJSON}
disabled={isSaving}
style={{
width: '100%',
padding: '10px',
backgroundColor: '#27ae60',
color: 'white',
border: 'none',
borderRadius: '5px',
cursor: isSaving ? 'not-allowed' : 'pointer'
}}
>
{isSaving ? 'Сохранение...' : 'Сохранить в JSON'}
</button>
</div>
{/* Настройки коллайдера */}
<div style={{ marginBottom: '20px' }}>
<h3>Создание коллайдера</h3>
<div style={{ marginBottom: '10px' }}>
<label>Тип:</label>
<select
value={shapeType}
onChange={(e) => setShapeType(e.target.value)}
style={{ width: '100%', padding: '5px', marginTop: '5px' }}
>
<option value="box">Коробка</option>
<option value="circle">Цилиндр</option>
<option value="capsule">Капсула</option>
</select>
</div>
<div style={{ marginBottom: '10px' }}>
<label>Расстояние от камеры: {colliderCreationDistance}</label>
<input
type="range"
min="5"
max="50"
value={colliderCreationDistance}
onChange={(e) => setColliderCreationDistance(parseInt(e.target.value))}
style={{ width: '100%', marginTop: '5px' }}
/>
</div>
<div style={{ marginBottom: '10px' }}>
<label style={{ display: 'flex', alignItems: 'center' }}>
<input
type="checkbox"
checked={showPreview}
onChange={(e) => setShowPreview(e.target.checked)}
style={{ marginRight: '8px' }}
/>
Показать предварительный просмотр
</label>
</div>
<button
onClick={createCollider}
style={{
width: '100%',
padding: '10px',
backgroundColor: '#e74c3c',
color: 'white',
border: 'none',
borderRadius: '5px',
cursor: 'pointer'
}}
>
Создать коллайдер
</button>
</div>
{/* Настройки цвета и прозрачности */}
{selected && (
<div style={{ marginBottom: '20px' }}>
<h3>Настройки выбранного коллайдера</h3>
{/* Управление параметрами */}
<div style={{ marginBottom: '15px', padding: '10px', backgroundColor: '#34495e', borderRadius: '5px' }}>
<h4 style={{ margin: '0 0 10px 0', fontSize: '14px' }}>Параметры трансформации</h4>
{/* Позиция */}
<div style={{ marginBottom: '8px' }}>
<label style={{ fontSize: '12px' }}>Позиция:</label>
<div style={{ display: 'flex', gap: '5px', marginTop: '3px' }}>
<input
type="number"
step="0.1"
value={colliderPosition.x.toFixed(2)}
onChange={(e) => setColliderPosition({...colliderPosition, x: parseFloat(e.target.value) || 0})}
style={{ width: '60px', padding: '2px', fontSize: '11px' }}
placeholder="X"
/>
<input
type="number"
step="0.1"
value={colliderPosition.y.toFixed(2)}
onChange={(e) => setColliderPosition({...colliderPosition, y: parseFloat(e.target.value) || 0})}
style={{ width: '60px', padding: '2px', fontSize: '11px' }}
placeholder="Y"
/>
<input
type="number"
step="0.1"
value={colliderPosition.z.toFixed(2)}
onChange={(e) => setColliderPosition({...colliderPosition, z: parseFloat(e.target.value) || 0})}
style={{ width: '60px', padding: '2px', fontSize: '11px' }}
placeholder="Z"
/>
</div>
</div>
{/* Поворот */}
<div style={{ marginBottom: '8px' }}>
<label style={{ fontSize: '12px' }}>Поворот (радианы):</label>
<div style={{ display: 'flex', gap: '5px', marginTop: '3px' }}>
<input
type="number"
step="0.1"
value={colliderRotation.x.toFixed(2)}
onChange={(e) => setColliderRotation({...colliderRotation, x: parseFloat(e.target.value) || 0})}
style={{ width: '60px', padding: '2px', fontSize: '11px' }}
placeholder="X"
/>
<input
type="number"
step="0.1"
value={colliderRotation.y.toFixed(2)}
onChange={(e) => setColliderRotation({...colliderRotation, y: parseFloat(e.target.value) || 0})}
style={{ width: '60px', padding: '2px', fontSize: '11px' }}
placeholder="Y"
/>
<input
type="number"
step="0.1"
value={colliderRotation.z.toFixed(2)}
onChange={(e) => setColliderRotation({...colliderRotation, z: parseFloat(e.target.value) || 0})}
style={{ width: '60px', padding: '2px', fontSize: '11px' }}
placeholder="Z"
/>
</div>
</div>
{/* Масштаб */}
<div style={{ marginBottom: '8px' }}>
<label style={{ fontSize: '12px' }}>Масштаб:</label>
<div style={{ display: 'flex', gap: '5px', marginTop: '3px' }}>
<input
type="number"
step="0.1"
min="0.1"
value={colliderScale.x.toFixed(2)}
onChange={(e) => setColliderScale({...colliderScale, x: parseFloat(e.target.value) || 0.1})}
style={{ width: '60px', padding: '2px', fontSize: '11px' }}
placeholder="X"
/>
<input
type="number"
step="0.1"
min="0.1"
value={colliderScale.y.toFixed(2)}
onChange={(e) => setColliderScale({...colliderScale, y: parseFloat(e.target.value) || 0.1})}
style={{ width: '60px', padding: '2px', fontSize: '11px' }}
placeholder="Y"
/>
<input
type="number"
step="0.1"
min="0.1"
value={colliderScale.z.toFixed(2)}
onChange={(e) => setColliderScale({...colliderScale, z: parseFloat(e.target.value) || 0.1})}
style={{ width: '60px', padding: '2px', fontSize: '11px' }}
placeholder="Z"
/>
</div>
</div>
<button
onClick={updateSelectedColliderParameters}
style={{
width: '100%',
padding: '5px',
backgroundColor: '#3498db',
color: 'white',
border: 'none',
borderRadius: '3px',
cursor: 'pointer',
fontSize: '12px',
marginBottom: '8px'
}}
>
Применить параметры
</button>
</div>
{/* Переключение режимов TransformControls */}
<div style={{ marginBottom: '15px', padding: '10px', backgroundColor: '#2c3e50', borderRadius: '5px' }}>
<h4 style={{ margin: '0 0 10px 0', fontSize: '14px' }}>Режим трансформации</h4>
<div style={{ display: 'flex', gap: '5px' }}>
<button
onClick={() => switchTransformMode('translate')}
style={{
flex: 1,
padding: '8px',
backgroundColor: transformMode === 'translate' ? '#27ae60' : '#34495e',
color: 'white',
border: 'none',
borderRadius: '3px',
cursor: 'pointer',
fontSize: '11px'
}}
>
Перемещение
</button>
<button
onClick={() => switchTransformMode('rotate')}
style={{
flex: 1,
padding: '8px',
backgroundColor: transformMode === 'rotate' ? '#27ae60' : '#34495e',
color: 'white',
border: 'none',
borderRadius: '3px',
cursor: 'pointer',
fontSize: '11px'
}}
>
Поворот
</button>
<button
onClick={() => switchTransformMode('scale')}
style={{
flex: 1,
padding: '8px',
backgroundColor: transformMode === 'scale' ? '#27ae60' : '#34495e',
color: 'white',
border: 'none',
borderRadius: '3px',
cursor: 'pointer',
fontSize: '11px'
}}
>
Масштаб
</button>
</div>
{/* Переключение рамок */}
<div style={{ marginBottom: '8px' }}>
<button
onClick={toggleWireframes}
style={{
width: '100%',
padding: '8px',
backgroundColor: showWireframes ? '#27ae60' : '#e74c3c',
color: 'white',
border: 'none',
borderRadius: '3px',
cursor: 'pointer',
fontSize: '11px'
}}
>
{showWireframes ? '🔲 Скрыть рамки' : '🔲 Показать рамки'}
</button>
</div>
</div>
<div style={{ marginBottom: '10px' }}>
<label>Цвет:</label>
<div style={{ display: 'flex', alignItems: 'center', marginTop: '5px' }}>
<div
style={{
width: '30px',
height: '30px',
backgroundColor: `rgb(${selectedColor.r * 255}, ${selectedColor.g * 255}, ${selectedColor.b * 255})`,
border: '2px solid white',
borderRadius: '5px',
cursor: 'pointer'
}}
onClick={() => setShowColorPicker(!showColorPicker)}
/>
<span style={{ marginLeft: '10px' }}>
RGB({Math.round(selectedColor.r * 255)}, {Math.round(selectedColor.g * 255)}, {Math.round(selectedColor.b * 255)})
</span>
</div>
{showColorPicker && (
<div style={{ marginTop: '10px' }}>
<div style={{ marginBottom: '5px' }}>
<label>Красный: {Math.round(selectedColor.r * 255)}</label>
<input
type="range"
min="0"
max="1"
step="0.01"
value={selectedColor.r}
onChange={(e) => setSelectedColor({...selectedColor, r: parseFloat(e.target.value)})}
style={{ width: '100%' }}
/>
</div>
<div style={{ marginBottom: '5px' }}>
<label>Зеленый: {Math.round(selectedColor.g * 255)}</label>
<input
type="range"
min="0"
max="1"
step="0.01"
value={selectedColor.g}
onChange={(e) => setSelectedColor({...selectedColor, g: parseFloat(e.target.value)})}
style={{ width: '100%' }}
/>
</div>
<div style={{ marginBottom: '5px' }}>
<label>Синий: {Math.round(selectedColor.b * 255)}</label>
<input
type="range"
min="0"
max="1"
step="0.01"
value={selectedColor.b}
onChange={(e) => setSelectedColor({...selectedColor, b: parseFloat(e.target.value)})}
style={{ width: '100%' }}
/>
</div>
</div>
)}
</div>
<div style={{ marginBottom: '10px' }}>
<label>Прозрачность: {Math.round(selectedOpacity * 100)}%</label>
<input
type="range"
min="0"
max="1"
step="0.01"
value={selectedOpacity}
onChange={(e) => setSelectedOpacity(parseFloat(e.target.value))}
style={{ width: '100%', marginTop: '5px' }}
/>
</div>
<button
onClick={updateSelectedColliderColor}
style={{
width: '100%',
padding: '10px',
backgroundColor: '#9b59b6',
color: 'white',
border: 'none',
borderRadius: '5px',
cursor: 'pointer',
marginBottom: '10px'
}}
>
Применить цвет
</button>
<button
onClick={duplicateSelectedCollider}
style={{
width: '100%',
padding: '10px',
backgroundColor: '#f39c12',
color: 'white',
border: 'none',
borderRadius: '5px',
cursor: 'pointer',
marginBottom: '10px'
}}
>
Дублировать коллайдер
</button>
<button
onClick={deleteSelected}
style={{
width: '100%',
padding: '10px',
backgroundColor: '#e74c3c',
color: 'white',
border: 'none',
borderRadius: '5px',
cursor: 'pointer',
marginBottom: '10px'
}}
>
Удалить коллайдер
</button>
<button
onClick={debugColliders}
style={{
width: '100%',
padding: '8px',
backgroundColor: '#9b59b6',
color: 'white',
border: 'none',
borderRadius: '5px',
cursor: 'pointer',
fontSize: '12px'
}}
>
🔍 Отладка коллайдеров
</button>
</div>
)}
{/* Управление камерой */}
<div style={{ marginBottom: '20px' }}>
<h3>Управление камерой</h3>
<div style={{ marginBottom: '10px' }}>
<label>Скорость движения: {cameraSpeed}</label>
<input
type="range"
min="1"
max="20"
value={cameraSpeed}
onChange={(e) => setCameraSpeed(parseInt(e.target.value))}
style={{ width: '100%', marginTop: '5px' }}
/>
</div>
<div style={{ marginTop: '10px' }}>
<button
onClick={() => {
if (cameraRef.current && orbitRef.current) {
cameraRef.current.position.set(20, 20, 20);
orbitRef.current.target.set(0, 0, 0);
orbitRef.current.update();
}
}}
style={{
width: '100%',
padding: '8px',
backgroundColor: '#3498db',
color: 'white',
border: 'none',
borderRadius: '5px',
cursor: 'pointer',
marginBottom: '5px'
}}
>
Сброс камеры (R)
</button>
<button
onClick={() => {
if (cameraRef.current && orbitRef.current) {
cameraRef.current.position.set(0, 50, 0);
orbitRef.current.target.set(0, 0, 0);
orbitRef.current.update();
}
}}
style={{
width: '100%',
padding: '8px',
backgroundColor: '#9b59b6',
color: 'white',
border: 'none',
borderRadius: '5px',
cursor: 'pointer',
marginBottom: '5px'
}}
>
Вид сверху
</button>
<button
onClick={() => {
if (cameraRef.current && orbitRef.current) {
cameraRef.current.position.set(50, 10, 0);
orbitRef.current.target.set(0, 0, 0);
orbitRef.current.update();
}
}}
style={{
width: '100%',
padding: '8px',
backgroundColor: '#e67e22',
color: 'white',
border: 'none',
borderRadius: '5px',
cursor: 'pointer'
}}
>
Вид сбоку
</button>
</div>
<div style={{ fontSize: '11px', color: '#bdc3c7', marginTop: '10px' }}>
<p><strong>Клавиши управления:</strong></p>
<p>W / - Вперед</p>
<p>S / - Назад</p>
<p>A / - Влево</p>
<p>D / - Вправо</p>
<p>Q / PageUp - Вверх</p>
<p>E / PageDown - Вниз</p>
<p>R - Сброс позиции</p>
<p>Мышь - Поворот камеры</p>
<p>Колесо мыши - Приближение</p>
</div>
</div>
{/* Телепорт к интерьерам */}
<div style={{ marginBottom: '20px' }}>
<h3>Телепорт к интерьерам</h3>
{interiors.length > 0 ? (
<div style={{ maxHeight: '150px', overflowY: 'auto' }}>
{interiors.map(interior => (
<button
key={interior.id}
onClick={() => teleportToInterior(interior.id)}
style={{
width: '100%',
padding: '8px',
backgroundColor: '#16a085',
color: 'white',
border: 'none',
borderRadius: '5px',
cursor: 'pointer',
marginBottom: '5px',
fontSize: '12px',
textAlign: 'left'
}}
>
Интерьер #{interior.id}
<br />
<span style={{ fontSize: '10px', opacity: 0.8 }}>
({interior.pos_x?.toFixed(1)}, {interior.pos_y?.toFixed(1)}, {interior.pos_z?.toFixed(1)})
</span>
</button>
))}
</div>
) : (
<p style={{ fontSize: '12px', color: '#bdc3c7' }}>
Интерьеры не найдены
</p>
)}
</div>
{/* Управление сохранением */}
<div style={{ marginBottom: '20px' }}>
<h3>Управление сохранением</h3>
<div style={{ marginBottom: '10px' }}>
<label style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<input
type="checkbox"
checked={autoSave}
onChange={(e) => setAutoSave(e.target.checked)}
style={{ transform: 'scale(1.2)' }}
/>
<span style={{ fontSize: '14px' }}>Автоматическое сохранение</span>
</label>
</div>
{lastSaved && (
<div style={{ fontSize: '12px', color: '#27ae60', marginBottom: '10px' }}>
💾 Последнее сохранение: {lastSaved.toLocaleTimeString()}
</div>
)}
<div style={{ display: 'flex', gap: '10px' }}>
<button
onClick={saveCollidersToJSON}
disabled={isSaving}
style={{
flex: 1,
padding: '8px',
backgroundColor: isSaving ? '#95a5a6' : '#27ae60',
color: 'white',
border: 'none',
borderRadius: '5px',
cursor: isSaving ? 'not-allowed' : 'pointer',
fontSize: '12px'
}}
>
{isSaving ? 'Сохранение...' : '💾 Сохранить сейчас'}
</button>
<button
onClick={loadCollidersFromJSON}
disabled={isLoading}
style={{
flex: 1,
padding: '8px',
backgroundColor: isLoading ? '#95a5a6' : '#3498db',
color: 'white',
border: 'none',
borderRadius: '5px',
cursor: isLoading ? 'not-allowed' : 'pointer',
fontSize: '12px'
}}
>
{isLoading ? 'Загрузка...' : '🔄 Перезагрузить'}
</button>
<button
onClick={reloadColliders}
disabled={isLoading}
style={{
flex: 1,
padding: '8px',
backgroundColor: isLoading ? '#95a5a6' : '#e67e22',
color: 'white',
border: 'none',
borderRadius: '5px',
cursor: isLoading ? 'not-allowed' : 'pointer',
fontSize: '12px'
}}
>
{isLoading ? 'Загрузка...' : '🔄 Синхронизировать'}
</button>
</div>
</div>
{/* Информация */}
<div style={{ marginTop: '20px', fontSize: '12px', color: '#bdc3c7' }}>
<p>Коллайдеров: {collidersRef.current.length}</p>
<p>Выбран: {selected ? 'Да' : 'Нет'}</p>
<p>Позиция курсора: ({cursorXZ.x.toFixed(2)}, {cursorXZ.z.toFixed(2)})</p>
{cameraRef.current && (
<p>Позиция камеры: ({cameraRef.current.position.x.toFixed(2)}, {cameraRef.current.position.y.toFixed(2)}, {cameraRef.current.position.z.toFixed(2)})</p>
)}
</div>
</div>
{/* 3D сцена */}
<div
ref={mountRef}
style={{ flex: 1 }}
onClick={handleClick}
onMouseMove={handleMouseMove}
/>
</div>
);
}