Frontend:: added rays, naming, upd canvas, upd calc logic end etc

This commit is contained in:
moxitech 2024-11-06 20:21:22 +07:00
parent 685c4c637c
commit dcdb360612
6 changed files with 300 additions and 81 deletions

View File

@ -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}
> >
&#x2715; &#x2715;

View 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;

View File

@ -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>

View File

@ -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>
)
}

View File

@ -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,94 +102,151 @@ const ThreeJsInstance = () => {
}; };
return ( return (
<> <div className='mt-20'>
<Canvas style={{ height: '800px', width: '900px' }} shadows> <div style={{ height: '550px' }} />
<ambientLight intensity={0.5} /> <div className='border border-blue-500 bg-blue-500 flex justify-center items-start'>
<directionalLight position={[5, 10, 5]} intensity={1} castShadow /> <Canvas
<pointLight position={[10, 10, 10]} intensity={0.8} /> style={{ height: '550px', width: '1100px' }}
<spotLight position={[-10, 15, 10]} angle={0.3} intensity={0.7} castShadow /> shadows
<OrbitControls ref={orbitControlsRef} /> >
<MapModel /> <ambientLight intensity={0.5} />
{baseStations.map((baseStation) => ( <directionalLight position={[5, 10, 5]} intensity={1} castShadow />
<BaseStationModel <pointLight position={[10, 10, 10]} intensity={0.8} />
key={baseStation.id} <spotLight position={[-10, 15, 10]} angle={0.3} intensity={0.7} castShadow />
onClick={() => handleObjectClick('baseStation', baseStation.id)} <OrbitControls ref={orbitControlsRef} />
isSelected={selectedObject?.type === 'baseStation' && selectedObject.id === baseStation.id} <MapModel />
position={baseStation.position} {baseStations.map((baseStation) => (
/> <BaseStationModel
))} key={baseStation.id}
{drones.map((drone) => ( onClick={() => handleObjectClick('baseStation', baseStation.id)}
<DroneModel isSelected={selectedObject?.type === 'baseStation' && selectedObject.id === baseStation.id}
key={drone.id} position={baseStation.position}
position={drone.position} />
onClick={() => handleObjectClick('drone', drone.id)} ))}
isSelected={selectedObject?.type === 'drone' && selectedObject.id === drone.id} {drones.map((drone) => (
/> <DroneModel
))} key={drone.id}
{drones.flatMap((drone) => ( position={drone.position}
baseStations.map((baseStation) => { droneName={drone.name}
const distance = Math.sqrt( onClick={() => handleObjectClick('drone', drone.id)}
Math.pow(drone.position[0] - baseStation.position[0], 2) + isSelected={selectedObject?.type === 'drone' && selectedObject.id === drone.id}
Math.pow(drone.position[1] - baseStation.position[1], 2) + />
Math.pow(drone.position[2] - baseStation.position[2], 2) ))}
); {drones.flatMap((drone) => (
baseStations.map((baseStation) => {
if (distance <= drone.signalRadius && distance <= baseStation.signalRadius) { const distance = Math.sqrt(
return ( Math.pow(drone.position[0] - baseStation.position[0], 2) +
<Line Math.pow(drone.position[1] - baseStation.position[1], 2) +
key={`link-${drone.id}-${baseStation.id}`} Math.pow(drone.position[2] - baseStation.position[2], 2)
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;
})
))}
</Canvas>
<button onClick={() => addDrone()} className="p-2 m-2 bg-green-500 text-white rounded">Добавить дрон</button> if (distance <= drone.signalRadius && distance <= baseStation.signalRadius) {
<button onClick={() => addBaseStation()} className="p-2 m-2 bg-blue-500 text-white rounded">Добавить базовую станцию</button> return (
<button onClick={resetCamera} className="p-2 m-2 bg-red-500 text-white rounded">Сбросить камеру</button> <Line
<button onClick={() => selectNextObject('prev')} className="p-2 m-2 bg-yellow-500 text-white rounded">&lt;</button> key={`link-${drone.id}-${baseStation.id}`}
<button onClick={() => selectNextObject('next')} className="p-2 m-2 bg-yellow-500 text-white rounded">&gt;</button> points={[drone.position, baseStation.position]}
{selectedObject && ( color="yellow"
<Modal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)}> lineWidth={Math.min(drone.frequency, baseStation.frequency) / 200} // Adjust line width based on frequency
<div className="p-4 m-4 bg-gray-100 border rounded"> dashed={false}
{selectedObject.type === 'drone' && ( />
<FormComponent );
entity={drones.find((d) => d.id === selectedObject.id)!} }
onUpdate={handleUpdate} return null;
/> })
)} ))}
{selectedObject.type === 'baseStation' && ( {baseStations.map((baseStation) => (
<FormComponent baseStation.signalRadius > 0 && (
entity={baseStations.find((b) => b.id === selectedObject.id)!} <mesh key={`signal-${baseStation.id}`} position={baseStation.position}>
onUpdate={handleUpdate} <sphereGeometry args={[baseStation.signalRadius, 32, 32]} />
/> <meshBasicMaterial color="red" opacity={0.3} transparent />
)} </mesh>
</div> )
</Modal> ))}
)} {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">&lt; Предыдущий</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">&gt; Следующий</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 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>
)} )}

View File

@ -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">