feat: Initialize Turtle Control Center with React and WebSocket server
- Added index.html for the main entry point of the client application. - Created package.json for client dependencies and scripts. - Implemented App component with view controls and layout. - Added CSS styles for the application and components. - Developed ControlPanel component for turtle management and commands. - Created Map3D component for 3D visualization of turtles. - Established Zustand store for state management of turtles and WebSocket connection. - Set up server with Express and WebSocket for handling turtle updates and commands. - Added REST API endpoints for turtle status updates and command handling. - Implemented start.sh script for setting up and running the application.
This commit is contained in:
203
client/src/components/Map3D.jsx
Normal file
203
client/src/components/Map3D.jsx
Normal file
@@ -0,0 +1,203 @@
|
||||
import React, { useRef, useMemo } from 'react';
|
||||
import { Canvas, useFrame } from '@react-three/fiber';
|
||||
import { OrbitControls, Grid, Text, Line } from '@react-three/drei';
|
||||
import * as THREE from 'three';
|
||||
import { useTurtleStore } from '../store/turtleStore';
|
||||
|
||||
// Turtle marker component
|
||||
function TurtleMarker({ turtle, isSelected, onClick }) {
|
||||
const meshRef = useRef();
|
||||
const { position, mode } = turtle;
|
||||
|
||||
useFrame((state) => {
|
||||
if (meshRef.current && isSelected) {
|
||||
meshRef.current.rotation.y += 0.02;
|
||||
}
|
||||
});
|
||||
|
||||
if (!position) return null;
|
||||
|
||||
const color = mode === 'mining' ? '#4ade80' :
|
||||
mode === 'exploring' ? '#60a5fa' :
|
||||
mode === 'returning' ? '#f59e0b' :
|
||||
'#9ca3af';
|
||||
|
||||
return (
|
||||
<group position={[position.x, position.y, position.z]} onClick={onClick}>
|
||||
{/* Turtle body */}
|
||||
<mesh ref={meshRef}>
|
||||
<boxGeometry args={[0.8, 0.6, 0.8]} />
|
||||
<meshStandardMaterial
|
||||
color={color}
|
||||
emissive={color}
|
||||
emissiveIntensity={isSelected ? 0.5 : 0.2}
|
||||
/>
|
||||
</mesh>
|
||||
|
||||
{/* Selection indicator */}
|
||||
{isSelected && (
|
||||
<>
|
||||
<mesh position={[0, 1, 0]}>
|
||||
<coneGeometry args={[0.3, 0.5, 4]} />
|
||||
<meshStandardMaterial color="#ffffff" emissive="#ffffff" emissiveIntensity={0.8} />
|
||||
</mesh>
|
||||
|
||||
{/* Selection ring */}
|
||||
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, -0.4, 0]}>
|
||||
<ringGeometry args={[1, 1.2, 32]} />
|
||||
<meshBasicMaterial color="#ffffff" side={THREE.DoubleSide} transparent opacity={0.5} />
|
||||
</mesh>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Turtle ID label */}
|
||||
<Text
|
||||
position={[0, 1.5, 0]}
|
||||
fontSize={0.4}
|
||||
color="white"
|
||||
anchorX="center"
|
||||
anchorY="middle"
|
||||
>
|
||||
T-{turtle.turtleID}
|
||||
</Text>
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
// Path trail component
|
||||
function PathTrail({ turtle }) {
|
||||
const { position, homePosition } = turtle;
|
||||
|
||||
if (!position || !homePosition) return null;
|
||||
|
||||
const points = [
|
||||
new THREE.Vector3(position.x, position.y, position.z),
|
||||
new THREE.Vector3(homePosition.x, homePosition.y, homePosition.z)
|
||||
];
|
||||
|
||||
return (
|
||||
<Line
|
||||
points={points}
|
||||
color="#60a5fa"
|
||||
lineWidth={2}
|
||||
dashed
|
||||
dashScale={2}
|
||||
transparent
|
||||
opacity={0.5}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Home marker component
|
||||
function HomeMarker({ position }) {
|
||||
if (!position) return null;
|
||||
|
||||
return (
|
||||
<group position={[position.x, position.y, position.z]}>
|
||||
<mesh>
|
||||
<cylinderGeometry args={[1, 0.5, 0.5, 6]} />
|
||||
<meshStandardMaterial color="#10b981" emissive="#10b981" emissiveIntensity={0.3} />
|
||||
</mesh>
|
||||
<Text
|
||||
position={[0, 1.2, 0]}
|
||||
fontSize={0.5}
|
||||
color="#10b981"
|
||||
anchorX="center"
|
||||
anchorY="middle"
|
||||
>
|
||||
HOME
|
||||
</Text>
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
// Main scene component
|
||||
function Scene() {
|
||||
const turtles = useTurtleStore((state) => state.getTurtleArray());
|
||||
const selectedTurtleId = useTurtleStore((state) => state.selectedTurtleId);
|
||||
const selectTurtle = useTurtleStore((state) => state.selectTurtle);
|
||||
|
||||
// Calculate center point for camera focus
|
||||
const centerPoint = useMemo(() => {
|
||||
if (turtles.length === 0) return [0, 0, 0];
|
||||
|
||||
let sumX = 0, sumY = 0, sumZ = 0;
|
||||
let count = 0;
|
||||
|
||||
turtles.forEach(turtle => {
|
||||
if (turtle.position) {
|
||||
sumX += turtle.position.x;
|
||||
sumY += turtle.position.y;
|
||||
sumZ += turtle.position.z;
|
||||
count++;
|
||||
}
|
||||
});
|
||||
|
||||
if (count === 0) return [0, 0, 0];
|
||||
return [sumX / count, sumY / count, sumZ / count];
|
||||
}, [turtles]);
|
||||
|
||||
// Get home position from first turtle
|
||||
const homePosition = turtles.find(t => t.homePosition)?.homePosition;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ambientLight intensity={0.5} />
|
||||
<pointLight position={[10, 10, 10]} intensity={1} />
|
||||
<pointLight position={[-10, -10, -10]} intensity={0.5} />
|
||||
|
||||
{/* Grid */}
|
||||
<Grid
|
||||
args={[100, 100]}
|
||||
cellSize={1}
|
||||
cellThickness={0.5}
|
||||
cellColor="#1e293b"
|
||||
sectionSize={5}
|
||||
sectionThickness={1}
|
||||
sectionColor="#334155"
|
||||
fadeDistance={50}
|
||||
fadeStrength={1}
|
||||
followCamera={false}
|
||||
infiniteGrid
|
||||
/>
|
||||
|
||||
{/* Home marker */}
|
||||
{homePosition && <HomeMarker position={homePosition} />}
|
||||
|
||||
{/* Turtles and paths */}
|
||||
{turtles.map((turtle) => (
|
||||
<React.Fragment key={turtle.turtleID}>
|
||||
<PathTrail turtle={turtle} />
|
||||
<TurtleMarker
|
||||
turtle={turtle}
|
||||
isSelected={selectedTurtleId === turtle.turtleID}
|
||||
onClick={() => selectTurtle(turtle.turtleID)}
|
||||
/>
|
||||
</React.Fragment>
|
||||
))}
|
||||
|
||||
{/* Camera controls */}
|
||||
<OrbitControls
|
||||
target={centerPoint}
|
||||
enableDamping
|
||||
dampingFactor={0.05}
|
||||
minDistance={5}
|
||||
maxDistance={100}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// Main Map3D component
|
||||
export default function Map3D() {
|
||||
return (
|
||||
<div style={{ width: '100%', height: '100%', background: '#0a0e1a' }}>
|
||||
<Canvas
|
||||
camera={{ position: [15, 15, 15], fov: 60 }}
|
||||
style={{ background: '#0a0e1a' }}
|
||||
>
|
||||
<Scene />
|
||||
</Canvas>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user