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}
|
||||
>
|
||||
<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
|
||||
>
|
||||
<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}
|
||||
>
|
||||
✕
|
||||
|
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}>
|
||||
<a
|
||||
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}
|
||||
<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";
|
||||
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 { Drone, BaseStation } from './Models';
|
||||
import { PlayCircleIcon } from '@heroicons/react/24/outline';
|
||||
import FormComponent from '../ObjectProps/FormComponent';
|
||||
import Modal from '../Modal/Modal';
|
||||
|
||||
@ -12,7 +13,24 @@ const ThreeJsInstance = () => {
|
||||
const [selectedObject, setSelectedObject] = useState<{ type: 'drone' | 'baseStation'; id: number } | null>(null);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
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) => {
|
||||
setDrones((prev) => [
|
||||
...prev,
|
||||
@ -32,7 +50,7 @@ const ThreeJsInstance = () => {
|
||||
baseStation || {
|
||||
id: prev.length,
|
||||
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
|
||||
signalRadius: Math.random() * 5 + 5, // Example signal radius between 5-10
|
||||
antennaDirection: [0, 1, 0],
|
||||
@ -84,94 +102,151 @@ const ThreeJsInstance = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Canvas style={{ height: '800px', width: '900px' }} shadows>
|
||||
<ambientLight intensity={0.5} />
|
||||
<directionalLight position={[5, 10, 5]} intensity={1} castShadow />
|
||||
<pointLight position={[10, 10, 10]} intensity={0.8} />
|
||||
<spotLight position={[-10, 15, 10]} angle={0.3} intensity={0.7} castShadow />
|
||||
<OrbitControls ref={orbitControlsRef} />
|
||||
<MapModel />
|
||||
{baseStations.map((baseStation) => (
|
||||
<BaseStationModel
|
||||
key={baseStation.id}
|
||||
onClick={() => handleObjectClick('baseStation', baseStation.id)}
|
||||
isSelected={selectedObject?.type === 'baseStation' && selectedObject.id === baseStation.id}
|
||||
position={baseStation.position}
|
||||
/>
|
||||
))}
|
||||
{drones.map((drone) => (
|
||||
<DroneModel
|
||||
key={drone.id}
|
||||
position={drone.position}
|
||||
onClick={() => handleObjectClick('drone', drone.id)}
|
||||
isSelected={selectedObject?.type === 'drone' && selectedObject.id === drone.id}
|
||||
/>
|
||||
))}
|
||||
{drones.flatMap((drone) => (
|
||||
baseStations.map((baseStation) => {
|
||||
const distance = Math.sqrt(
|
||||
Math.pow(drone.position[0] - baseStation.position[0], 2) +
|
||||
Math.pow(drone.position[1] - baseStation.position[1], 2) +
|
||||
Math.pow(drone.position[2] - baseStation.position[2], 2)
|
||||
);
|
||||
|
||||
if (distance <= drone.signalRadius && distance <= baseStation.signalRadius) {
|
||||
return (
|
||||
<Line
|
||||
key={`link-${drone.id}-${baseStation.id}`}
|
||||
points={[drone.position, baseStation.position]}
|
||||
color="yellow"
|
||||
lineWidth={Math.min(drone.frequency, baseStation.frequency) / 200} // Adjust line width based on frequency
|
||||
dashed={false}
|
||||
/>
|
||||
<div className='mt-20'>
|
||||
<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} />
|
||||
<directionalLight position={[5, 10, 5]} intensity={1} castShadow />
|
||||
<pointLight position={[10, 10, 10]} intensity={0.8} />
|
||||
<spotLight position={[-10, 15, 10]} angle={0.3} intensity={0.7} castShadow />
|
||||
<OrbitControls ref={orbitControlsRef} />
|
||||
<MapModel />
|
||||
{baseStations.map((baseStation) => (
|
||||
<BaseStationModel
|
||||
key={baseStation.id}
|
||||
onClick={() => handleObjectClick('baseStation', baseStation.id)}
|
||||
isSelected={selectedObject?.type === 'baseStation' && selectedObject.id === baseStation.id}
|
||||
position={baseStation.position}
|
||||
/>
|
||||
))}
|
||||
{drones.map((drone) => (
|
||||
<DroneModel
|
||||
key={drone.id}
|
||||
position={drone.position}
|
||||
droneName={drone.name}
|
||||
onClick={() => handleObjectClick('drone', drone.id)}
|
||||
isSelected={selectedObject?.type === 'drone' && selectedObject.id === drone.id}
|
||||
/>
|
||||
))}
|
||||
{drones.flatMap((drone) => (
|
||||
baseStations.map((baseStation) => {
|
||||
const distance = Math.sqrt(
|
||||
Math.pow(drone.position[0] - baseStation.position[0], 2) +
|
||||
Math.pow(drone.position[1] - baseStation.position[1], 2) +
|
||||
Math.pow(drone.position[2] - baseStation.position[2], 2)
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})
|
||||
))}
|
||||
</Canvas>
|
||||
|
||||
<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={resetCamera} className="p-2 m-2 bg-red-500 text-white rounded">Сбросить камеру</button>
|
||||
<button onClick={() => selectNextObject('prev')} 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">></button>
|
||||
{selectedObject && (
|
||||
<Modal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)}>
|
||||
<div className="p-4 m-4 bg-gray-100 border rounded">
|
||||
{selectedObject.type === 'drone' && (
|
||||
<FormComponent
|
||||
entity={drones.find((d) => d.id === selectedObject.id)!}
|
||||
onUpdate={handleUpdate}
|
||||
/>
|
||||
)}
|
||||
{selectedObject.type === 'baseStation' && (
|
||||
<FormComponent
|
||||
entity={baseStations.find((b) => b.id === selectedObject.id)!}
|
||||
onUpdate={handleUpdate}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
if (distance <= drone.signalRadius && distance <= baseStation.signalRadius) {
|
||||
return (
|
||||
<Line
|
||||
key={`link-${drone.id}-${baseStation.id}`}
|
||||
points={[drone.position, baseStation.position]}
|
||||
color="yellow"
|
||||
lineWidth={Math.min(drone.frequency, baseStation.frequency) / 200} // Adjust line width based on frequency
|
||||
dashed={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
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>
|
||||
</div>
|
||||
|
||||
<>
|
||||
<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={() => 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 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 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 && (
|
||||
<Modal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)}>
|
||||
<div className="p-2 m-4 bg-gray-100 border rounded">
|
||||
{selectedObject.type === 'drone' && (
|
||||
<FormComponent
|
||||
entity={drones.find((d) => d.id === selectedObject.id)!}
|
||||
onUpdate={handleUpdate}
|
||||
/>
|
||||
)}
|
||||
{selectedObject.type === 'baseStation' && (
|
||||
<FormComponent
|
||||
entity={baseStations.find((b) => b.id === selectedObject.id)!}
|
||||
onUpdate={handleUpdate}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</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 { 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');
|
||||
return (
|
||||
<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 && (
|
||||
<mesh position={position}>
|
||||
<sphereGeometry args={[0.5, 16, 16]} />
|
||||
<sphereGeometry args={[1, 16, 16]} />
|
||||
<meshBasicMaterial color="red" wireframe />
|
||||
</mesh>
|
||||
)}
|
||||
|
@ -1,13 +1,17 @@
|
||||
"use client";
|
||||
import ThreeJsInstance from '@/app/components/Threejs/ThreeJsInstance';
|
||||
import React from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
|
||||
const Simulations: React.FC = () => {
|
||||
const [activeSimalationWindow, setActiveSimalationWindow] = useState(false);
|
||||
|
||||
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)]">
|
||||
<main id="modal-root">
|
||||
<ThreeJsInstance/>
|
||||
{activeSimalationWindow ? <div>
|
||||
</div> : <ThreeJsInstance/>}
|
||||
</main>
|
||||
|
||||
<footer className="row-start-3 flex gap-6 flex-wrap items-center justify-center">
|
||||
|
Loading…
Reference in New Issue
Block a user