Frontend:: added rays, naming, upd canvas, upd calc logic end etc
This commit is contained in:
parent
685c4c637c
commit
dcdb360612
@ -36,11 +36,11 @@ const Modal: React.FC<ModalProps> = ({ children, isOpen, onClose }) => {
|
|||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="bg-gray-700 p-8 rounded-lg shadow-lg relative"
|
className="bg-gray-700 p-1 rounded-lg shadow-lg relative"
|
||||||
onClick={(e) => e.stopPropagation()} // Prevent closing when clicking inside the modal
|
onClick={(e) => e.stopPropagation()} // Prevent closing when clicking inside the modal
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
className="absolute top-2 right-2 text-gray-500 hover:text-gray-800"
|
className="absolute w-10 h-10 top-2 right-2 text-gray-950 hover:text-gray-800"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
>
|
>
|
||||||
✕
|
✕
|
||||||
|
118
src/usn-frontend/src/app/components/Player/Player.tsx
Normal file
118
src/usn-frontend/src/app/components/Player/Player.tsx
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
interface Player {
|
||||||
|
Click: () => void; // клик по кнопке
|
||||||
|
TimeEnd: number; // Время окончания
|
||||||
|
TimeStep: number; // временной шаг
|
||||||
|
onTimeUpdate?: (currentTime: number) => void; // callback для возврата текущего времени
|
||||||
|
}
|
||||||
|
|
||||||
|
const InitPlayer: React.FC<Player> = ({ Click, TimeStep, TimeEnd, onTimeUpdate }) => {
|
||||||
|
const [isPlaying, setIsPlaying] = useState(false);
|
||||||
|
const [currentTime, setCurrentTime] = useState(0);
|
||||||
|
const [playbackSpeed, setPlaybackSpeed] = useState(1); // скорость воспроизведения
|
||||||
|
const [isDropdownOpen, setIsDropdownOpen] = useState(false); // состояние выпадающей панели
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let interval: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
|
if (isPlaying) {
|
||||||
|
interval = setInterval(() => {
|
||||||
|
setCurrentTime((prev) => {
|
||||||
|
const newTime = prev + TimeStep * playbackSpeed;
|
||||||
|
if (newTime >= TimeEnd) {
|
||||||
|
clearInterval(interval!);
|
||||||
|
onTimeUpdate && onTimeUpdate(TimeEnd);
|
||||||
|
return TimeEnd;
|
||||||
|
}
|
||||||
|
onTimeUpdate && onTimeUpdate(newTime);
|
||||||
|
return newTime;
|
||||||
|
});
|
||||||
|
}, TimeStep * 1000 / playbackSpeed);
|
||||||
|
} else {
|
||||||
|
if (interval) {
|
||||||
|
clearInterval(interval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (interval) {
|
||||||
|
clearInterval(interval);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [isPlaying, TimeStep, TimeEnd, playbackSpeed, onTimeUpdate]);
|
||||||
|
|
||||||
|
const handlePlayPause = () => {
|
||||||
|
setIsPlaying(!isPlaying);
|
||||||
|
Click(); // вызываем функцию при клике
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTimeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const newTime = Number(e.target.value);
|
||||||
|
setCurrentTime(newTime);
|
||||||
|
onTimeUpdate && onTimeUpdate(newTime); // вызываем callback при изменении времени
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSpeedChange = (speed: number) => {
|
||||||
|
setPlaybackSpeed(speed);
|
||||||
|
setIsDropdownOpen(false); // закрываем выпадающую панель после выбора скорости
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-slate-600 p-4 rounded-md w-full max-w-md mx-auto">
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
{/* Кнопка старт/стоп симуляции */}
|
||||||
|
<button
|
||||||
|
onClick={handlePlayPause}
|
||||||
|
className={`text-white p-2 rounded-md ${
|
||||||
|
isPlaying ? 'bg-red-600' : 'bg-blue-600'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{isPlaying ? 'Пауза' : 'Старт'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4">
|
||||||
|
{/* Полоска плеера симуляции */}
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max={TimeEnd}
|
||||||
|
value={currentTime}
|
||||||
|
step={TimeStep}
|
||||||
|
onChange={handleTimeChange}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
<div className="text-white mt-2">
|
||||||
|
Текущее время: {currentTime}s / {TimeEnd}s
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 relative">
|
||||||
|
{/* Панель изменения скорости воспроизведения */}
|
||||||
|
<button
|
||||||
|
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
|
||||||
|
className="bg-gray-700 text-white p-2 rounded-md"
|
||||||
|
>
|
||||||
|
Скорость: {playbackSpeed}x
|
||||||
|
</button>
|
||||||
|
{isDropdownOpen && (
|
||||||
|
<div className="absolute bg-gray-800 text-white mt-2 rounded-md w-32 shadow-lg">
|
||||||
|
<ul>
|
||||||
|
{[0.5, 1, 1.5, 2].map((speed) => (
|
||||||
|
<li
|
||||||
|
key={speed}
|
||||||
|
onClick={() => handleSpeedChange(speed)}
|
||||||
|
className="px-4 py-2 hover:bg-gray-600 cursor-pointer"
|
||||||
|
>
|
||||||
|
{speed}x
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InitPlayer;
|
||||||
|
|
@ -96,7 +96,7 @@ export default function NavBar() {
|
|||||||
<li key={item.name}>
|
<li key={item.name}>
|
||||||
<a
|
<a
|
||||||
href={item.to}
|
href={item.to}
|
||||||
className="flex items-center p-2 text-gray-900 rounded-lg dark:text-white hover:bg-gray-100 dark:hover:bg-gray-700 group"
|
className="transition duration-300 ease-in-out transform hover:scale-105 focus:scale-95 flex items-center p-2 text-gray-900 rounded-lg dark:text-white hover:bg-gray-100 dark:hover:bg-gray-700 group"
|
||||||
>
|
>
|
||||||
{item.icon}
|
{item.icon}
|
||||||
<span className="ms-3">{item.name}</span>
|
<span className="ms-3">{item.name}</span>
|
||||||
|
@ -0,0 +1,22 @@
|
|||||||
|
import React from "react";
|
||||||
|
import InitPlayer from "../Player/Player"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
interface ISimulation {
|
||||||
|
TimeEnd: number;
|
||||||
|
TimeStep: number;
|
||||||
|
ResultSourceOrUrl?: string;
|
||||||
|
OnLoading?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// SimulationWindow -Основное окно с симуляцией
|
||||||
|
// на вход подается словарь время:Массив объектов
|
||||||
|
// массив объектов рендериться согласно времени и параметрам объектов
|
||||||
|
const SimulationWindow: React.FC<ISimulation> = ({TimeEnd, TimeStep, ResultSourceOrUrl, OnLoading}) => {
|
||||||
|
return (
|
||||||
|
<InitPlayer Click={() => {console.log("STOP")}} TimeEnd={1000} TimeStep={1} onTimeUpdate={(e) => {console.log(e)}}>
|
||||||
|
|
||||||
|
</InitPlayer>
|
||||||
|
)
|
||||||
|
}
|
@ -1,8 +1,9 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { Canvas } from '@react-three/fiber';
|
import { Canvas } from '@react-three/fiber';
|
||||||
import { OrbitControls, useGLTF, Line } from '@react-three/drei';
|
import { OrbitControls, useGLTF, Line, Text } from '@react-three/drei';
|
||||||
import { useState, useEffect, useRef } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import { Drone, BaseStation } from './Models';
|
import { Drone, BaseStation } from './Models';
|
||||||
|
import { PlayCircleIcon } from '@heroicons/react/24/outline';
|
||||||
import FormComponent from '../ObjectProps/FormComponent';
|
import FormComponent from '../ObjectProps/FormComponent';
|
||||||
import Modal from '../Modal/Modal';
|
import Modal from '../Modal/Modal';
|
||||||
|
|
||||||
@ -12,7 +13,24 @@ const ThreeJsInstance = () => {
|
|||||||
const [selectedObject, setSelectedObject] = useState<{ type: 'drone' | 'baseStation'; id: number } | null>(null);
|
const [selectedObject, setSelectedObject] = useState<{ type: 'drone' | 'baseStation'; id: number } | null>(null);
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
const orbitControlsRef = useRef<any>(null); // Reference to OrbitControls
|
const orbitControlsRef = useRef<any>(null); // Reference to OrbitControls
|
||||||
|
const handleJsonUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = event.target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
try {
|
||||||
|
const content = e.target?.result;
|
||||||
|
if (typeof content === "string") {
|
||||||
|
const jsonData = JSON.parse(content);
|
||||||
|
console.log(jsonData);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Ошибка при чтении файла:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
};
|
||||||
const addDrone = (drone?: Drone) => {
|
const addDrone = (drone?: Drone) => {
|
||||||
setDrones((prev) => [
|
setDrones((prev) => [
|
||||||
...prev,
|
...prev,
|
||||||
@ -32,7 +50,7 @@ const ThreeJsInstance = () => {
|
|||||||
baseStation || {
|
baseStation || {
|
||||||
id: prev.length,
|
id: prev.length,
|
||||||
name: `Base Station ${prev.length + 1}`,
|
name: `Base Station ${prev.length + 1}`,
|
||||||
position: [Math.random() * 10, 20, Math.random() * 10],
|
position: [Math.random() * 10, -1, Math.random() * 10],
|
||||||
frequency: Math.random() * 100 + 400, // Example frequency range between 400-500
|
frequency: Math.random() * 100 + 400, // Example frequency range between 400-500
|
||||||
signalRadius: Math.random() * 5 + 5, // Example signal radius between 5-10
|
signalRadius: Math.random() * 5 + 5, // Example signal radius between 5-10
|
||||||
antennaDirection: [0, 1, 0],
|
antennaDirection: [0, 1, 0],
|
||||||
@ -84,8 +102,13 @@ const ThreeJsInstance = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className='mt-20'>
|
||||||
<Canvas style={{ height: '800px', width: '900px' }} shadows>
|
<div style={{ height: '550px' }} />
|
||||||
|
<div className='border border-blue-500 bg-blue-500 flex justify-center items-start'>
|
||||||
|
<Canvas
|
||||||
|
style={{ height: '550px', width: '1100px' }}
|
||||||
|
shadows
|
||||||
|
>
|
||||||
<ambientLight intensity={0.5} />
|
<ambientLight intensity={0.5} />
|
||||||
<directionalLight position={[5, 10, 5]} intensity={1} castShadow />
|
<directionalLight position={[5, 10, 5]} intensity={1} castShadow />
|
||||||
<pointLight position={[10, 10, 10]} intensity={0.8} />
|
<pointLight position={[10, 10, 10]} intensity={0.8} />
|
||||||
@ -104,6 +127,7 @@ const ThreeJsInstance = () => {
|
|||||||
<DroneModel
|
<DroneModel
|
||||||
key={drone.id}
|
key={drone.id}
|
||||||
position={drone.position}
|
position={drone.position}
|
||||||
|
droneName={drone.name}
|
||||||
onClick={() => handleObjectClick('drone', drone.id)}
|
onClick={() => handleObjectClick('drone', drone.id)}
|
||||||
isSelected={selectedObject?.type === 'drone' && selectedObject.id === drone.id}
|
isSelected={selectedObject?.type === 'drone' && selectedObject.id === drone.id}
|
||||||
/>
|
/>
|
||||||
@ -130,16 +154,39 @@ const ThreeJsInstance = () => {
|
|||||||
return null;
|
return null;
|
||||||
})
|
})
|
||||||
))}
|
))}
|
||||||
|
{baseStations.map((baseStation) => (
|
||||||
|
baseStation.signalRadius > 0 && (
|
||||||
|
<mesh key={`signal-${baseStation.id}`} position={baseStation.position}>
|
||||||
|
<sphereGeometry args={[baseStation.signalRadius, 32, 32]} />
|
||||||
|
<meshBasicMaterial color="red" opacity={0.3} transparent />
|
||||||
|
</mesh>
|
||||||
|
)
|
||||||
|
))}
|
||||||
|
{drones.map((drone) => (
|
||||||
|
drone.signalRadius > 0 && (
|
||||||
|
<mesh key={`signal-${drone.id}`} position={drone.position}>
|
||||||
|
<sphereGeometry args={[drone.signalRadius, 32, 32]} />
|
||||||
|
<meshBasicMaterial color="green" opacity={0.3} transparent />
|
||||||
|
</mesh>
|
||||||
|
)
|
||||||
|
))}
|
||||||
</Canvas>
|
</Canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button onClick={() => addDrone()} className="p-2 m-2 bg-green-500 text-white rounded">Добавить дрон</button>
|
<>
|
||||||
<button onClick={() => addBaseStation()} className="p-2 m-2 bg-blue-500 text-white rounded">Добавить базовую станцию</button>
|
<button onClick={() => addDrone()} className="p-2 m-2 bg-green-500 text-white rounded transition duration-300 ease-in-out transform hover:scale-105 focus:scale-95">Добавить дрон</button>
|
||||||
<button onClick={resetCamera} className="p-2 m-2 bg-red-500 text-white rounded">Сбросить камеру</button>
|
<button onClick={() => addBaseStation()} className="p-2 m-2 bg-blue-500 text-white rounded transition duration-300 ease-in-out transform hover:scale-105 focus:scale-95">Добавить базовую станцию</button>
|
||||||
<button onClick={() => selectNextObject('prev')} className="p-2 m-2 bg-yellow-500 text-white rounded"><</button>
|
<button onClick={() => selectNextObject('prev')} className="p-2 m-2 bg-yellow-500 text-white rounded transition duration-300 ease-in-out transform hover:scale-105 focus:scale-95">< Предыдущий</button>
|
||||||
<button onClick={() => selectNextObject('next')} className="p-2 m-2 bg-yellow-500 text-white rounded">></button>
|
<button onClick={() => selectNextObject('next')} className="p-2 m-2 bg-yellow-500 text-white rounded transition duration-300 ease-in-out transform hover:scale-105 focus:scale-95">> Следующий</button>
|
||||||
|
<button onClick={resetCamera} className="p-2 m-2 bg-red-500 text-white rounded transition duration-300 ease-in-out transform hover:scale-105 focus:scale-95">Сбросить камеру</button>
|
||||||
|
|
||||||
|
<button onClick={() => document.getElementById("fileInput")?.click()} className="p-2 m-2 bg-orange-400 text-white rounded transition duration-300 ease-in-out transform hover:scale-105 focus:scale-95">Загрузка из JSON</button>
|
||||||
|
<button onClick={() => {}} className="p-1 m-2 w-9 h-9 bg-emerald-700 text-white rounded transition duration-300 ease-in-out transform hover:scale-105 focus:scale-95">
|
||||||
|
<PlayCircleIcon/>
|
||||||
|
</button>
|
||||||
{selectedObject && (
|
{selectedObject && (
|
||||||
<Modal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)}>
|
<Modal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)}>
|
||||||
<div className="p-4 m-4 bg-gray-100 border rounded">
|
<div className="p-2 m-4 bg-gray-100 border rounded">
|
||||||
{selectedObject.type === 'drone' && (
|
{selectedObject.type === 'drone' && (
|
||||||
<FormComponent
|
<FormComponent
|
||||||
entity={drones.find((d) => d.id === selectedObject.id)!}
|
entity={drones.find((d) => d.id === selectedObject.id)!}
|
||||||
@ -155,23 +202,51 @@ const ThreeJsInstance = () => {
|
|||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
)}
|
)}
|
||||||
|
<input
|
||||||
|
id="fileInput"
|
||||||
|
type="file"
|
||||||
|
accept=".json"
|
||||||
|
style={{ display: "none" }}
|
||||||
|
onChange={handleJsonUpload}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
|
|
||||||
|
<div className="bg-gray-800 hover:bg-gray-700 focus:bg-gray-900 text-gray-200 mt-10 p-4 rounded-lg shadow-lg transition duration-300 ease-in-out transform hover:scale-105 focus:scale-95">
|
||||||
|
{/* TODO: EXTERNAL COMPONENT */}
|
||||||
|
<div className="bg-blue-600 text-white p-3 rounded-md mb-4">
|
||||||
|
Тестовая симуляция 1
|
||||||
|
</div>
|
||||||
|
<button className="bg-blue-500 hover:bg-blue-600 text-white py-2 px-4 rounded transition duration-300 ease-in-out focus:outline-none focus:ring-2 focus:ring-blue-400">
|
||||||
|
Запуск
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const MapModel = () => {
|
const MapModel = () => {
|
||||||
const { scene } = useGLTF('/map/map.glb');
|
const { scene } = useGLTF('/map/map.glb');
|
||||||
return <primitive object={scene} scale={[1.5, 1.5, 1.5]} />;
|
return <primitive object={scene} scale={[2, 2, 2]} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DroneModel = ({ position, onClick, isSelected }: { position: [number, number, number]; onClick: () => void; isSelected: boolean }) => {
|
const DroneModel = ({ position, droneName, onClick, isSelected }: { position: [number, number, number]; droneName?: string; onClick: () => void; isSelected: boolean }) => {
|
||||||
const { scene } = useGLTF('/objects/drone.glb');
|
const { scene } = useGLTF('/objects/drone.glb');
|
||||||
return (
|
return (
|
||||||
<group onClick={onClick}>
|
<group onClick={onClick}>
|
||||||
<primitive object={scene} position={[position[0] + 0.8, position[1], position[2] - 0.5]} scale={[0.2, 0.2, 0.2]} />
|
<primitive object={scene} position={[position[0] + 0.8, position[1], position[2] - 0.5]} scale={[0.1, 0.1, 0.1]} />
|
||||||
|
{/* Добавляем текстовую подпись над дроном */}
|
||||||
|
<Text
|
||||||
|
position={[position[0], position[1] + 10, position[2]]} // Позиционируем текст над дроном
|
||||||
|
fontSize={1}
|
||||||
|
color="white" // Цвет текста
|
||||||
|
anchorX="center" // Центровка текста по X
|
||||||
|
anchorY="middle" // Центровка текста по Y
|
||||||
|
>
|
||||||
|
{droneName}
|
||||||
|
</Text>
|
||||||
{isSelected && (
|
{isSelected && (
|
||||||
<mesh position={position}>
|
<mesh position={position}>
|
||||||
<sphereGeometry args={[0.5, 16, 16]} />
|
<sphereGeometry args={[1, 16, 16]} />
|
||||||
<meshBasicMaterial color="red" wireframe />
|
<meshBasicMaterial color="red" wireframe />
|
||||||
</mesh>
|
</mesh>
|
||||||
)}
|
)}
|
||||||
|
@ -1,13 +1,17 @@
|
|||||||
|
"use client";
|
||||||
import ThreeJsInstance from '@/app/components/Threejs/ThreeJsInstance';
|
import ThreeJsInstance from '@/app/components/Threejs/ThreeJsInstance';
|
||||||
import React from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
|
||||||
const Simulations: React.FC = () => {
|
const Simulations: React.FC = () => {
|
||||||
|
const [activeSimalationWindow, setActiveSimalationWindow] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
||||||
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
|
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
|
||||||
<main id="modal-root">
|
<main id="modal-root">
|
||||||
<ThreeJsInstance/>
|
{activeSimalationWindow ? <div>
|
||||||
|
</div> : <ThreeJsInstance/>}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer className="row-start-3 flex gap-6 flex-wrap items-center justify-center">
|
<footer className="row-start-3 flex gap-6 flex-wrap items-center justify-center">
|
||||||
|
Loading…
Reference in New Issue
Block a user