Base station object & cookies & refactoring

This commit is contained in:
moxitech 2024-10-08 04:05:32 +07:00
parent 7357f02ac6
commit 6b739c3967
17 changed files with 615 additions and 148 deletions

View File

@ -12,6 +12,7 @@
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"bootstrap-icons": "^1.11.3",
"js-cookie": "^3.0.5",
"qrcode.react": "^4.0.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
@ -13124,6 +13125,14 @@
"jiti": "bin/jiti.js"
}
},
"node_modules/js-cookie": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
"integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
"engines": {
"node": ">=14"
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",

View File

@ -7,6 +7,7 @@
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"bootstrap-icons": "^1.11.3",
"js-cookie": "^3.0.5",
"qrcode.react": "^4.0.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

View File

@ -1,4 +1,5 @@
import Cookies from 'js-cookie';
export const LocalSimulationJson = () => {
@ -8,3 +9,18 @@ export const LocalSimulationJson = () => {
"TODO": "JSON RETURN"
}
}
// Функция для установки куки
export const setCustomCookie = (key, value, options = {}) => {
Cookies.set(key, value, { ...options });
};
// Функция для получения куки
export const getCustomCookie = (key) => {
return Cookies.get(key);
};
// Функция для удаления куки
export const removeCustomCookie = (key) => {
Cookies.remove(key);
};

View File

@ -1,6 +1,6 @@
.sidebar {
width: 200px;
height: 100vh;
height: 120vh;
background-color: #2c3e50;
color: white;
padding: 20px;

View File

@ -1,14 +1,14 @@
import React, { useEffect, useRef, useState } from 'react';
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
import { FontLoader } from 'three/examples/jsm/loaders/FontLoader';
import { TextGeometry } from 'three/examples/jsm/geometries/TextGeometry';
import Drone from './model/Drone';
import BaseStation from './model/BaseStation';
import HeightPoint from './model/HeightPoint';
import { RandomHeightPoint } from './helpers/generateRandomHeightPoints'
import ModalWindow from './components/Modal/Modal';
import './Dashboard.css';
// import { NavBar } from './components/Navbar/Navbar';
const Dashboard = () => {
const mountRef = useRef(null); // Указатель монтирования
const sceneRef = useRef(null); // Указатель на сцену
@ -23,10 +23,59 @@ const Dashboard = () => {
const [selectedBaseStation, setSelectedBaseStation] = useState(null); // Состояние для выбранной базовой станции
const [mapSettings, setMapSettings] = useState({maxHeight: 20, coordX: 0, coordY: 0, }); // Настройки карты
const [isModalOpen, setIsModalOpen] = useState(false);
const [objectType, setObjectType] = useState(1); // 1 = Drone, 2 = Base Station
const [formObjectData, setFormObjectData] = useState({
name: "",
type: 1,
coordinates_x: 0,
coordinates_y: 0,
coordinates_z: 0,
droneField1: "",
droneField2: "",
baseStationField1: "",
baseStationField2: ""
});
const handleTypeChange = (type) => {
setObjectType(type);
setFormData({ ...formData, type });
};
// Одно состояние для управления всеми модальными окнами
const [modals, setModals] = useState({
isModalOpen: false,
isModalAddOpen: false,
isModalDeleteOpen: false,
isModalSearchOpen: false,
isModalMapOpen: false,
});
// Функция открытия модального окна в зависимости от переданной строки
const openModal = (modalName) => {
setModals((prevModals) => ({
...prevModals,
[modalName]: true,
}));
};
// Функция закрытия всех модальных окон
const closeAllModals = () => {
setModals({
isModalOpen: false,
isModalAddOpen: false,
isModalDeleteOpen: false,
isModalSearchOpen: false,
});
};
const openModal = () => setIsModalOpen(true);
const closeModal = () => setIsModalOpen(false);
const [contextMenu, setContextMenu] = useState({ visible: false, x: 0, y: 0 }); // контекстное меню сцены
@ -139,6 +188,9 @@ const Dashboard = () => {
drones.forEach(droneData => {
scene.add(droneData.getObject());
});
baseStation.forEach(b => {
scene.add(b.getObject());
});
// Обработка нажатия ПКМ
renderer.domElement.addEventListener('contextmenu', (event) => {
@ -168,39 +220,62 @@ const Dashboard = () => {
sceneRef.current.add(drone.getObject());
}
};
// Добавление дрона в сцену
const handleAddBaseStation = () => {
if (sceneRef.current) {
const current = Date.now();
const base = new BaseStation(current / 1000);
base.setPosition(Math.random() * 20 - 10, 20, Math.random() * 20 - 10);
setBaseStation((prev) => [...prev, base]);
console.log(baseStation.map(b => b.getObject().children[0]));
sceneRef.current.add(base.getObject());
}
};
// Обработчик ввода на форму высот
const handleInputChange = (event) => {
const { name, value } = event.target;
setFormData({
...formData,
[name]: value,
});
};
// Обработчик ввода на форму объектов
const handleAddInputChange = (event) => {
const { name, value } = event.target;
setFormObjectData({
...formData,
[name]: value,
});
};
// Обработчик ввода на форму
// const handleInputChange = (event) => {
// const { name, value } = event.target;
// setFormData({
// ...formData,
// [name]: value,
// });
// };
// Добавление элемента карты высот (высоты)
// const handleAddHeight = (event) => {
// event.preventDefault();
// const { x, y, height } = formData;
// const newPoint = new HeightPoint(parseInt(x), parseInt(y), parseFloat(height));
// setHeightData((prevData) => [...prevData, newPoint]);
// setFormData({ x: '', y: '', height: '' });
// };
const handleAddHeight = (event) => {
event.preventDefault();
const { x, y, height } = formData;
const newPoint = new HeightPoint(parseInt(x), parseInt(y), parseFloat(height));
setHeightData((prevData) => [...prevData, newPoint]);
setFormData({ x: '', y: '', height: '' });
};
// Изменение настроек
// const handleSettingsChange = (event) => {
// const { name, value } = event.target;
// setMapSettings({
// ...mapSettings,
// [name]: value,
// });
// };
const handleSettingsChange = (event) => {
const { name, value } = event.target;
setMapSettings({
...mapSettings,
[name]: value,
});
};
// Открытие контекстного меню
const handleContextMenuClick = (action) => {
if (action === 'add') {
if (action === 'add_base'){
handleAddBaseStation();
}
else if (action === 'add') {
handleAddDrone();
console.log('Добавить объект');
} else if (action === 'save') {
console.log('Сохранить промежуточный результат');
}
@ -219,52 +294,237 @@ const Dashboard = () => {
<h4><b>Drone</b> Network Simulator - окно разработки</h4>
</div>
{/* <NavBar/> TEST */}
{/* <button onClick={openModal}>Открыть модальное окно</button>
<ModalWindow isOpen={isModalOpen} onClose={closeModal}>
<h2>Кастомное содержимое</h2>
<input type="text" placeholder="Введите текст" />
<button onClick={() => alert('Действие!')}>Добавить</button>
</ModalWindow> */}
{/* END NAVBAR TEST */}
<ModalWindow isOpen={isModalOpen} onClose={closeModal}>
<h2>Добавление объекта</h2>
<input type="text" placeholder="Введите название" />
<button onClick={() => alert('Добавлено')}>Добавить</button>
<ModalWindow isOpen={modals.isModalAddOpen} onClose={closeAllModals}>
<div className="modal d-block" tabIndex="-1">
<div className="modal-dialog">
<div className="modal-content">
<div className="modal-header">
<h5 className="modal-title">Добавление объекта</h5>
<button type="button" className="btn-close" onClick={closeAllModals}></button>
</div>
<div className="modal-body">
<div className="mb-3">
<label>Тип объекта:</label>
<div>
<div className="form-check form-check-inline">
<input
className="form-check-input"
type="radio"
name="objectType"
id="drone"
checked={objectType === 1}
onChange={() => handleTypeChange(1)}
/>
<label className="form-check-label" htmlFor="drone">Дрон</label>
</div>
<div className="form-check form-check-inline">
<input
className="form-check-input"
type="radio"
name="objectType"
id="baseStation"
checked={objectType === 2}
onChange={() => handleTypeChange(2)}
/>
<label className="form-check-label" htmlFor="baseStation">Базовая станция</label>
</div>
</div>
</div>
<div className="mb-3">
<label>Название объекта:</label>
<input
type="text"
className="form-control"
placeholder="Введите название"
name='name'
value={formObjectData.name}
onChange={handleAddInputChange}
/>
</div>
<div className="mb-3">
<label>Координаты:</label>
<div className="input-group mb-2">
<span className="input-group-text">X</span>
<input
type="number"
className="form-control"
value={formObjectData.coordinates_x}
name='coordinates_x'
onChange={handleAddInputChange}
/>
</div>
<div className="input-group mb-2">
<span className="input-group-text">Y</span>
<input
type="number"
className="form-control"
name='coordinates_y'
value={formObjectData.coordinates_y}
onChange={handleAddInputChange}
/>
</div>
<div className="input-group mb-2">
<span className="input-group-text">Z</span>
<input
type="number"
className="form-control"
name='coordinates_z'
value={formObjectData.coordinates_z}
onChange={handleAddInputChange}
/>
</div>
</div>
{objectType === 1 && (
<>
<div className="mb-3">
<label>Поле 1 для дрона:</label>
<input
type="text"
className="form-control"
name='droneField1'
value={formObjectData.droneField1}
onChange={handleAddInputChange}
/>
</div>
<div className="mb-3">
<label>Поле 2 для дрона:</label>
<input
type="text"
className="form-control"
name='droneField2'
value={formObjectData.droneField2}
onChange={handleAddInputChange}
/>
</div>
</>
)}
{objectType === 2 && (
<>
<div className="mb-3">
<label>Поле 1 для базовой станции:</label>
<input
type="text"
className="form-control"
name='baseStationField1'
value={formObjectData.baseStationField1}
onChange={handleAddInputChange}
/>
</div>
<div className="mb-3">
<label>Поле 2 для базовой станции:</label>
<input
type="text"
className="form-control"
name='baseStationField2'
value={formObjectData.baseStationField2}
onChange={handleAddInputChange}
/>
</div>
</>
)}
</div>
<div className="modal-footer">
<button type="button" className="btn btn-secondary" onClick={closeAllModals}>Закрыть</button>
<button type="button" className="btn btn-primary" onClick={() => { alert(`Добавлено: ${JSON.stringify(formData)}`); }}>Добавить</button>
</div>
</div>
</div>
</div>
</ModalWindow>
<ModalWindow isOpen={isModalOpen} onClose={closeModal}>
<ModalWindow isOpen={modals.isModalSearchOpen} onClose={closeAllModals}>
<h2>Поиск</h2>
<input type="text" placeholder="Введите имя" />
<button onClick={() => alert('Добавлено')}>Выбор</button>
</ModalWindow>
<ModalWindow isOpen={isModalOpen} onClose={closeModal}>
<ModalWindow isOpen={modals.isModalDeleteOpen} onClose={closeAllModals}>
<h2>Удаление</h2>
<h2>Вы точно хотите удалить объект X?</h2>
<button onClick={closeModal}>Отмена</button>
<button onClick={closeAllModals}>Отмена</button>
<button onClick={() => alert('Удалено')}>Удалить</button>
</ModalWindow>
<ModalWindow isOpen={isModalOpen} onClose={closeModal}>
<ModalWindow isOpen={modals.isModalOpen} onClose={closeAllModals}>
<h2>Загрузка JSON</h2>
<p>Выберите или переташите файл для загрузки</p>
<input type='file'/>
<button onClick={closeModal}>Отмена</button>
<button onClick={closeAllModals}>Отмена</button>
<button onClick={() => alert('Загружено')}>Загрузить</button>
</ModalWindow>
<ModalWindow isOpen={isModalOpen} onClose={closeModal}>
{/* Форма для добавления новых высот */}
<ModalWindow isOpen={modals.isModalMapOpen} onClose={closeAllModals}>
<h2>Настройки карты</h2>
<button onClick={closeModal}>Отмена</button>
<div className="columns">
<div className='column'>
<form onSubmit={handleAddHeight} style={{ marginTop: '20px' }}>
<div>
<label>Координата X:</label>
<input
className="input is-primary"
type="number"
name="x"
value={formData.x}
onChange={handleInputChange}
required
/>
</div>
<div>
<label>Координата Y:</label>
<input
className="input is-primary"
type="number"
name="y"
value={formData.y}
onChange={handleInputChange}
required
/>
</div>
<div>
<label>Высота:</label>
<input
className="input is-primary"
type="number"
name="height"
value={formData.height}
onChange={handleInputChange}
required
/>
</div>
<button className='button' type="submit">Добавить точку высоты</button>
</form>
</div>
{/* Настройки карты */}
<div className='column'>
<div style={{ marginTop: '20px' }}>
<h2>Настройки карты</h2>
<div>
<label>Максимальная высота:</label>
<input
className="input"
type="number"
name="maxHeight"
value={mapSettings.maxHeight}
onChange={handleSettingsChange}
/>
</div>
</div>
</div>
</div>
<button onClick={closeAllModals}>Отмена</button>
<button onClick={() => alert('Обновлена карта!')}>Готово</button>
</ModalWindow>
{/* Окно программы */}
<div className='coler-border'>
<div>
<p>X: {mouseState.x} Y: {mouseState.y} Z: {mouseState.z}</p>
<p> X: {mouseState.x} Y: {mouseState.y} Z: {mouseState.z}</p>
</div>
<div ref={mountRef} style={{
minWidth: '100%',
@ -274,15 +534,15 @@ const Dashboard = () => {
<div class="btn-toolbar" role="toolbar" aria-label="Toolbar with button groups">
<div class="btn-group me-2" role="group" aria-label="First group">
<button type="button" onClick={openModal} class="btn btn-success">
<button type="button" onClick={() => openModal('isModalAddOpen')} class="btn btn-success">
<i class="bi-plus"/>Добавить
</button>
<button type="button" onClick={openModal} class="btn btn-success">
<button type="button" onClick={() => openModal('isModalDeleteOpen')} class="btn btn-success">
<i class="bi-trash"/>Удалить
</button>
<button type="button" onClick={openModal} class="btn btn-success">
<button type="button" onClick={() => openModal('isModalSearchOpen')} class="btn btn-success">
<i class="bi-search"/>Выбрать
</button>
@ -301,13 +561,13 @@ const Dashboard = () => {
<i class="bi-filetype-json"/>Выгрузить
</button>
<button type="button" class="btn btn-secondary">
<button type="button" onClick={() => openModal('isModalMapOpen')} class="btn btn-secondary">
<i class="bi-map"/>Настройки карты
</button>
{/* <button type="button" class="btn btn-secondary">6</button> */}
</div>
<div class="btn-group" role="group" aria-label="Third group">
<div class="btn-group me-2" role="group" aria-label="Third group">
<button type="button" class="btn btn-info">
<i class="bi-play"/>
</button>
@ -348,68 +608,12 @@ const Dashboard = () => {
<div className='context'>
<div class="dot"></div>
<li className={"btn"} onClick={() => handleContextMenuClick('add')}>Добавить объект</li>
<li className={"btn"} onClick={() => handleContextMenuClick('add_base')}>Добавить базовую станцию</li>
<li className={"btn"} onClick={() => handleContextMenuClick('save')}>Сохранить промежуточный результат</li>
<li className={"btn"} onClick={() => handleContextMenuClick('cancel')}>Отмена</li>
</div>
</ul>
)}
<div className="columns">
{/* Форма для добавления новых высот */}
{/* <div className='column'>
<form onSubmit={handleAddHeight} style={{ marginTop: '20px' }}>
<div>
<label>Координата X:</label>
<input
className="input is-primary"
type="number"
name="x"
value={formData.x}
onChange={handleInputChange}
required
/>
</div>
<div>
<label>Координата Y:</label>
<input
className="input is-primary"
type="number"
name="y"
value={formData.y}
onChange={handleInputChange}
required
/>
</div>
<div>
<label>Высота:</label>
<input
className="input is-primary"
type="number"
name="height"
value={formData.height}
onChange={handleInputChange}
required
/>
</div>
<button className='button' type="submit">Добавить точку высоты</button>
</form>
</div> */}
{/* Настройки карты */}
{/* <div className='column'>
<div style={{ marginTop: '20px' }}>
<h2>Настройки карты</h2>
<div>
<label>Максимальная высота:</label>
<input
className="input"
type="number"
name="maxHeight"
value={mapSettings.maxHeight}
onChange={handleSettingsChange}
/>
</div>
</div>
</div> */}
</div>
</div>
);
};

View File

@ -5,6 +5,9 @@ import '../Devices.css';
const Devices = () => {
return (
<div>
<p>Разработчик: @moxitech</p>
<img style={{ width: 200, height: 200 }} src='/recources/moxitech.webp'></img>
<hr/>
<button className='button is-danger'>Удалить неиспользуемые шаблоны</button>
<hr/>
<button className='button is-danger'>Удалить неактивных пользователей</button>

View File

@ -1,16 +1,26 @@
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import { setCustomCookie, getCustomCookie } from '../Services/Local'
import '../Login.scss';
const Login = ({ onLogin }) => {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
useEffect(() => {
let cook = getCustomCookie('userToken')
if (cook) {
onLogin();
}
}, [])
const handleSubmit = (e) => {
e.preventDefault();
// Simple login/password check
if (username === 'admin' && password === 'admin') {
// TODO :: CHANGE
setCustomCookie('userToken', '12345', { expires: 7 }); // Куки с истечением через 7 дней
onLogin(); // Call function on successful login
} else {
setError('Неверный логин или пароль');
}

View File

@ -1,49 +1,62 @@
import React, { useEffect, useState } from 'react';
import '../css/Custom/Main.css';
import { ServerRequest, giveMeServerWebsocketRequestAddress } from '../Services/Server';
import { setCustomCookie, getCustomCookie } from '../Services/Local'
const Main = () => {
const [rooms, setRooms] = useState(null)
const [rooms, setRooms] = useState(null);
const [isCreating, setIsCreating] = useState(false);
useEffect(() => {
connectSimulation()
}, [])
getSimulations();
setCustomCookie('userToken', '12345', { expires: 7 }); // Куки с истечением через 7 дней
}, []);
const createSimulation = () => {
setIsCreating(true);
giveMeServerWebsocketRequestAddress();
// TODO: Make POST request to create simulation here.
// POST $BASE_ADDR/createSimulation?userToken=?
}
// Simulating an async request for demo purposes
setTimeout(() => {
setIsCreating(false);
}, 2000);
};
const connectSimulation = () => {
ServerRequest("/simulations/active", "GET").then( x => {
console.log(x.data, x.status)
const getSimulations = () => {
ServerRequest("/simulations/active", "GET").then(x => {
console.log(x.data, x.status);
if (x.status === 200) {
setRooms(x.data.rooms_ids)
setRooms(x.data.rooms_ids);
} else {
setRooms(null)
setRooms(null);
}
})
}
});
};
return (
<div className='main__imager' style={{marginTop: "2%"}}>
<div className='main__imager' style={{ marginTop: "2%" }}>
<div className="row align-items-start">
<div className="col">
<h1 className="title">Создать новую симуляцию</h1>
<hr/>
<hr />
<button className="btn btn-primary" onClick={() => giveMeServerWebsocketRequestAddress()}>
<div class="spinner-border text-primary" role="status"/>
Create
<button
className="btn btn-primary"
onClick={createSimulation}
disabled={isCreating}
>
{isCreating && <div className="spinner-border text-light me-2" role="status" />}
Create
</button>
</div>
<div className="col">
<h1 className="title">Присоединиться к моделированию</h1>
<hr/>
<hr />
{rooms && rooms.length > 0 ? (
rooms.map(room => (
<div className='row' key={room}>
<div className='row' key={room.uuid}>
<div className='col'>
<h3>{room.name} | UUID : {room.uuid}</h3>
</div>
@ -57,11 +70,10 @@ const Main = () => {
) : (
<p>Пока никто не работает 😒</p>
)}
</div>
</div>
</div>
)
);
};
export default Main;

View File

@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React from 'react';
import '../UserAccount.scss';
const PrevCalc = () => {

View File

@ -0,0 +1,29 @@
import React from 'react';
const ContextMenu = ({ contextMenu, handleContextMenuClick }) => {
if (!contextMenu.visible) return null;
return (
<ul
style={{
position: 'absolute',
top: `${contextMenu.y}px`,
left: `${contextMenu.x}px`,
backgroundColor: 'transparent',
listStyle: 'none',
padding: '2px',
boxShadow: '0px 0px 5px rgba(0,0,0,0.3)',
zIndex: 1000,
}}
>
<div className="context">
<div className="dot"></div>
<li className={"btn"} onClick={() => handleContextMenuClick('add')}>Добавить объект</li>
<li className={"btn"} onClick={() => handleContextMenuClick('save')}>Сохранить промежуточный результат</li>
<li className={"btn"} onClick={() => handleContextMenuClick('cancel')}>Отмена</li>
</div>
</ul>
);
};
export default ContextMenu;

View File

@ -20,7 +20,7 @@ const ModalWindow = ({ isOpen, onClose, children }) => {
return ReactDOM.createPortal(
<div className="modal-overlay" onClick={onClose}>
<div className="modal-window" onClick={(e) => e.stopPropagation()}>
<button className="modal-close-button" onClick={onClose}>×</button>
<button className="btn-close" onClick={onClose}></button>
<div className="modal-content">{children}</div>
</div>
</div>,

View File

@ -0,0 +1,40 @@
import React from 'react';
const Toolbar = ({ openModal }) => {
return (
<div className="btn-toolbar" role="toolbar" aria-label="Toolbar with button groups">
<div className="btn-group me-2" role="group">
<button type="button" onClick={() => openModal('isModalAddOpen')} className="btn btn-success">
<i className="bi-plus" />Добавить
</button>
<button type="button" onClick={() => openModal('isModalDeleteOpen')} className="btn btn-success">
<i className="bi-trash" />Удалить
</button>
<button type="button" onClick={() => openModal('isModalSearchOpen')} className="btn btn-success">
<i className="bi-search" />Выбрать
</button>
<button type="button" className="btn btn-success">
<i className="bi-save" />Сохранить
</button>
</div>
<div className="btn-group me-2" role="group">
<button type="button" className="btn btn-secondary">
<i className="bi-box-arrow-in-down" />Загрузить данные
</button>
<button type="button" className="btn btn-secondary">
<i className="bi-filetype-json" />Выгрузить
</button>
<button type="button" onClick={() => openModal('isModalMapOpen')} className="btn btn-secondary">
<i className="bi-map" />Настройки карты
</button>
</div>
<div className="btn-group me-2" role="group">
<button type="button" className="btn btn-info">
<i className="bi-play" />
</button>
</div>
</div>
);
};
export default Toolbar;

View File

@ -0,0 +1,113 @@
import React, { useEffect, useRef } from 'react';
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
import Drone from '../../model/Drone';
const ThreeJSScene = ({ heightData, mapSettings, drones, setDrones, setSelectedDrone }) => {
const mountRef = useRef(null);
const sceneRef = useRef(null);
useEffect(() => {
if (!mountRef.current) return;
let width = mountRef.current.clientWidth;
let height = mountRef.current.clientHeight;
const scene = new THREE.Scene();
sceneRef.current = scene;
const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 2000);
camera.position.set(0, 80, 120);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(width, height);
mountRef.current.appendChild(renderer.domElement);
const geometry = new THREE.PlaneGeometry(200, 200, 20, 20);
const vertices = geometry.attributes.position.array;
const colors = [];
const color = new THREE.Color();
const minHeight = Math.min(...heightData.map((point) => point.height));
const maxHeight = Math.max(mapSettings.maxHeight, ...heightData.map((point) => point.height));
for (let i = 0; i < vertices.length; i += 3) {
const x = Math.floor(i / 3) % 5;
const y = Math.floor(i / (3 * 5));
const heightPoint = heightData.find((point) => point.x === x && point.y === y);
const heightValue = heightPoint ? heightPoint.height : 0;
vertices[i + 2] = heightValue;
const normalizedHeight = (heightValue - minHeight) / (maxHeight - minHeight);
color.setHSL(0.7 * (1 - normalizedHeight), 1, 0.5);
colors.push(color.r, color.g, color.b);
}
geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3));
const material = new THREE.MeshBasicMaterial({
vertexColors: true,
side: THREE.DoubleSide,
wireframe: false,
});
const mesh = new THREE.Mesh(geometry, material);
mesh.rotation.x = -Math.PI / 2;
scene.add(mesh);
const axesHelper = new THREE.AxesHelper(10);
scene.add(axesHelper);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(0, 50, 50).normalize();
scene.add(light);
const handleClick = (event) => {
const mouse = new THREE.Vector2(
(event.clientX / window.innerWidth) * 2 - 1,
-(event.clientY / window.innerHeight) * 2 + 1
);
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(drones.map(drone => drone.getObject()), true);
if (intersects.length > 0) {
const selected = intersects[0].object;
const drone = drones.find(drone => drone.getObject().children[0] === selected);
if (drone) {
if (setSelectedDrone) {
setSelectedDrone(drone);
}
}
} else {
if (setSelectedDrone) {
setSelectedDrone(null);
}
}
};
const animate = () => {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
};
renderer.domElement.addEventListener('click', handleClick);
animate();
drones.forEach(droneData => {
scene.add(droneData.getObject());
});
return () => {
if (mountRef.current) {
mountRef.current.removeChild(renderer.domElement);
}
};
}, [drones, heightData, mapSettings]);
return <div ref={mountRef} style={{ minWidth: '100%', minHeight: '60vh', position: 'relative' }} />;
};
export default ThreeJSScene;

View File

@ -0,0 +1,30 @@
import * as THREE from 'three';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
class BaseStation {
constructor(name, scale = 10) {
this.object = new THREE.Object3D();
this.name = name;
// Инициализируем GLTFLoader
const loader = new GLTFLoader();
// Загружаем модель из файла GLTF
loader.load("/phone_tower.glb", (gltf) => {
this.object.add(gltf.scene); // Добавляем сцену из GLTF к объекту дрона
gltf.scene.scale.set(scale, scale, scale); // Применяем масштаб
}, undefined, (error) => {
console.error("Ошибка загрузки GLTF:", error);
});
}
setPosition(x, y, z) {
this.object.position.set(x, y, z);
}
getObject() {
return this.object;
}
}
export default BaseStation;