add map on bike
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useEffect, useRef, useState, useMemo } from "react";
|
||||
import * as THREE from "three";
|
||||
import { useFrame, useThree } from "@react-three/fiber";
|
||||
import { EbikeGPSMap } from "@/components/ebike/EbikeGPSMap";
|
||||
import { InteractableObject } from "@/components/three/interaction/InteractableObject";
|
||||
import { useLoggedGLTF } from "@/hooks/three/useLoggedGLTF";
|
||||
import { useClonedObject } from "@/hooks/three/useClonedObject";
|
||||
@@ -39,8 +40,31 @@ export function Ebike({ position }: EbikeProps): React.JSX.Element {
|
||||
});
|
||||
const model = useClonedObject(scene);
|
||||
const movementMode = useGameStore((state) => state.player.movementMode);
|
||||
const mainState = useGameStore((state) => state.mainState);
|
||||
const camera = useThree((state) => state.camera);
|
||||
|
||||
// Map active mainState to target repair zone coordinate
|
||||
const destPos = useMemo(() => {
|
||||
switch (mainState) {
|
||||
case "bike":
|
||||
return { x: 8, y: 0, z: -6 };
|
||||
case "pylone":
|
||||
return { x: 64, y: 0, z: -66 };
|
||||
case "ferme":
|
||||
return { x: -24, y: 0, z: 42 };
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}, [mainState]);
|
||||
|
||||
// Throttled GPS start position to optimize pathfinding A* algorithm execution
|
||||
const [gpsStartPos, setGpsStartPos] = useState<{ x: number; y: number; z: number }>({
|
||||
x: position[0],
|
||||
y: position[1],
|
||||
z: position[2],
|
||||
});
|
||||
const lastGpsUpdatePos = useRef<THREE.Vector3>(new THREE.Vector3(...position));
|
||||
|
||||
const restingPosition = useRef<Vector3Tuple>([
|
||||
position[0],
|
||||
position[1] - PLAYER_EYE_HEIGHT,
|
||||
@@ -90,6 +114,13 @@ export function Ebike({ position }: EbikeProps): React.JSX.Element {
|
||||
12 * delta
|
||||
);
|
||||
}
|
||||
|
||||
// Throttled GPS start position update to prevent performance loss
|
||||
const currentPos = groupRef.current.position;
|
||||
if (currentPos.distanceTo(lastGpsUpdatePos.current) > 2.0) {
|
||||
lastGpsUpdatePos.current.copy(currentPos);
|
||||
setGpsStartPos({ x: currentPos.x, y: currentPos.y, z: currentPos.z });
|
||||
}
|
||||
} else {
|
||||
groupRef.current.position.set(...restingPosition.current);
|
||||
groupRef.current.rotation.set(0, restingRotation.current, 0);
|
||||
@@ -204,7 +235,25 @@ export function Ebike({ position }: EbikeProps): React.JSX.Element {
|
||||
<meshBasicMaterial colorWrite={false} depthWrite={false} />
|
||||
</mesh>
|
||||
</InteractableObject>
|
||||
|
||||
{/* Dynamic 3D GPS Dashboard Screen */}
|
||||
<group position={[0, 7, 0]} rotation={[0, 90, 0]}>
|
||||
<EbikeGPSMap
|
||||
width={0.8}
|
||||
height={0.8}
|
||||
startPos={gpsStartPos}
|
||||
destPos={destPos}
|
||||
mapImageUrl="/map_background.png"
|
||||
worldBounds={{
|
||||
minX: -166,
|
||||
maxX: 163,
|
||||
minZ: -142,
|
||||
maxZ: 138,
|
||||
}}
|
||||
/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
{debugRef.current.showCameraPoints && (
|
||||
<>
|
||||
<mesh position={camPointPos}>
|
||||
|
||||
@@ -8,12 +8,8 @@ export interface EbikeGPSMapProps {
|
||||
* 3D world position of the player/bike (GPS start point)
|
||||
* If omitted, snaps to [0,0,0]
|
||||
*/
|
||||
startPos?: { x: number; y: number; z: number };
|
||||
|
||||
/**
|
||||
* 3D world position of the destination/target (GPS end point)
|
||||
*/
|
||||
destPos?: { x: number; y: number; z: number };
|
||||
startPos?: { x: number; y: number; z: number } | undefined;
|
||||
destPos?: { x: number; y: number; z: number } | undefined;
|
||||
|
||||
/**
|
||||
* Optional custom URL to the map background texture.
|
||||
@@ -41,6 +37,11 @@ export interface EbikeGPSMapProps {
|
||||
* Height of the 3D plane mesh (default: 1)
|
||||
*/
|
||||
height?: number;
|
||||
|
||||
/**
|
||||
* Optional world position for the GPS screen (defaults to origin)
|
||||
*/
|
||||
position?: [number, number, number];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -56,6 +57,7 @@ export const EbikeGPSMap: React.FC<EbikeGPSMapProps> = ({
|
||||
worldBounds,
|
||||
width = 1,
|
||||
height = 1,
|
||||
position = [0, 0, 0],
|
||||
}) => {
|
||||
const [waypoints, setWaypoints] = useState<Waypoint[]>([]);
|
||||
const [mapImage, setMapImage] = useState<HTMLImageElement | HTMLCanvasElement | null>(null);
|
||||
@@ -235,10 +237,10 @@ export const EbikeGPSMap: React.FC<EbikeGPSMapProps> = ({
|
||||
if (activePath.length > 1) {
|
||||
// Pass 1: Wide transparent orange bloom
|
||||
ctx.beginPath();
|
||||
let pt = worldToCanvas(activePath[0].x, activePath[0].z, size);
|
||||
let pt = worldToCanvas(activePath[0]!.x, activePath[0]!.z, size);
|
||||
ctx.moveTo(pt.x, pt.y);
|
||||
for (let i = 1; i < activePath.length; i++) {
|
||||
pt = worldToCanvas(activePath[i].x, activePath[i].z, size);
|
||||
pt = worldToCanvas(activePath[i]!.x, activePath[i]!.z, size);
|
||||
ctx.lineTo(pt.x, pt.y);
|
||||
}
|
||||
ctx.strokeStyle = 'rgba(249, 115, 22, 0.2)'; // Faint bright orange
|
||||
@@ -251,10 +253,10 @@ export const EbikeGPSMap: React.FC<EbikeGPSMapProps> = ({
|
||||
|
||||
// Pass 2: Saturated glow core
|
||||
ctx.beginPath();
|
||||
pt = worldToCanvas(activePath[0].x, activePath[0].z, size);
|
||||
pt = worldToCanvas(activePath[0]!.x, activePath[0]!.z, size);
|
||||
ctx.moveTo(pt.x, pt.y);
|
||||
for (let i = 1; i < activePath.length; i++) {
|
||||
pt = worldToCanvas(activePath[i].x, activePath[i].z, size);
|
||||
pt = worldToCanvas(activePath[i]!.x, activePath[i]!.z, size);
|
||||
ctx.lineTo(pt.x, pt.y);
|
||||
}
|
||||
ctx.strokeStyle = '#f97316'; // Vibrant orange
|
||||
@@ -265,10 +267,10 @@ export const EbikeGPSMap: React.FC<EbikeGPSMapProps> = ({
|
||||
|
||||
// Pass 3: High-intensity white core
|
||||
ctx.beginPath();
|
||||
pt = worldToCanvas(activePath[0].x, activePath[0].z, size);
|
||||
pt = worldToCanvas(activePath[0]!.x, activePath[0]!.z, size);
|
||||
ctx.moveTo(pt.x, pt.y);
|
||||
for (let i = 1; i < activePath.length; i++) {
|
||||
pt = worldToCanvas(activePath[i].x, activePath[i].z, size);
|
||||
pt = worldToCanvas(activePath[i]!.x, activePath[i]!.z, size);
|
||||
ctx.lineTo(pt.x, pt.y);
|
||||
}
|
||||
ctx.strokeStyle = '#fff7ed'; // Cream white
|
||||
@@ -280,8 +282,8 @@ export const EbikeGPSMap: React.FC<EbikeGPSMapProps> = ({
|
||||
const segments: { start: { x: number; y: number }; end: { x: number; y: number }; len: number }[] = [];
|
||||
let totalLen = 0;
|
||||
for (let i = 0; i < activePath.length - 1; i++) {
|
||||
const p1 = worldToCanvas(activePath[i].x, activePath[i].z, size);
|
||||
const p2 = worldToCanvas(activePath[i + 1].x, activePath[i + 1].z, size);
|
||||
const p1 = worldToCanvas(activePath[i]!.x, activePath[i]!.z, size);
|
||||
const p2 = worldToCanvas(activePath[i + 1]!.x, activePath[i + 1]!.z, size);
|
||||
const len = Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));
|
||||
segments.push({ start: p1, end: p2, len });
|
||||
totalLen += len;
|
||||
@@ -290,7 +292,7 @@ export const EbikeGPSMap: React.FC<EbikeGPSMapProps> = ({
|
||||
if (totalLen > 0) {
|
||||
const targetLen = totalLen * animTimeRef.current;
|
||||
let currentLen = 0;
|
||||
let dotPt = segments[0].start;
|
||||
let dotPt = segments[0]!.start;
|
||||
|
||||
for (const seg of segments) {
|
||||
if (currentLen + seg.len >= targetLen) {
|
||||
@@ -378,9 +380,9 @@ export const EbikeGPSMap: React.FC<EbikeGPSMapProps> = ({
|
||||
}, [waypoints, startPos, destPos, bounds, mapImage]);
|
||||
|
||||
return (
|
||||
<mesh castShadow receiveShadow>
|
||||
<mesh castShadow receiveShadow position={position as any}>
|
||||
<planeGeometry args={[width, height]} />
|
||||
<meshBasicMaterial toneMapped={false} transparent={true} opacity={1} depthWrite={false}>
|
||||
<meshBasicMaterial toneMapped={false} transparent={true} opacity={1} depthWrite={false} side={THREE.DoubleSide}>
|
||||
<canvasTexture
|
||||
ref={textureRef}
|
||||
attach="map"
|
||||
|
||||
Reference in New Issue
Block a user