From 970253801a4e6d364cce5534e2d9a3cb7cccb58f Mon Sep 17 00:00:00 2001 From: math-pixel <59537610+math-pixel@users.noreply.github.com> Date: Fri, 22 May 2026 18:28:05 +0200 Subject: [PATCH] add map on bike --- src/components/ebike/Ebike.tsx | 51 +++++++++++++++++++++++++++- src/components/ebike/EbikeGPSMap.tsx | 36 ++++++++++---------- src/pages/backgroundmap/page.tsx | 2 +- src/pages/waypoint/page.tsx | 5 +-- src/world/World.tsx | 1 + 5 files changed, 74 insertions(+), 21 deletions(-) diff --git a/src/components/ebike/Ebike.tsx b/src/components/ebike/Ebike.tsx index bbbca53..5c3edeb 100644 --- a/src/components/ebike/Ebike.tsx +++ b/src/components/ebike/Ebike.tsx @@ -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(new THREE.Vector3(...position)); + const restingPosition = useRef([ 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 { + + {/* Dynamic 3D GPS Dashboard Screen */} + + + + {debugRef.current.showCameraPoints && ( <> diff --git a/src/components/ebike/EbikeGPSMap.tsx b/src/components/ebike/EbikeGPSMap.tsx index 441bb92..f904989 100644 --- a/src/components/ebike/EbikeGPSMap.tsx +++ b/src/components/ebike/EbikeGPSMap.tsx @@ -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 = ({ worldBounds, width = 1, height = 1, + position = [0, 0, 0], }) => { const [waypoints, setWaypoints] = useState([]); const [mapImage, setMapImage] = useState(null); @@ -235,10 +237,10 @@ export const EbikeGPSMap: React.FC = ({ 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 = ({ // 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 = ({ // 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 = ({ 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 = ({ 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 = ({ }, [waypoints, startPos, destPos, bounds, mapImage]); return ( - + - + + boundsTextRef: React.RefObject }) { const { camera, gl, scene } = useThree(); const controlsRef = useRef(null); diff --git a/src/pages/waypoint/page.tsx b/src/pages/waypoint/page.tsx index 11ee4dd..769a53a 100644 --- a/src/pages/waypoint/page.tsx +++ b/src/pages/waypoint/page.tsx @@ -63,6 +63,7 @@ const EditorScene: React.FC = ({ const { raycaster, pointer, camera } = useThree(); const groupRef = useRef(null); const rubberLineRef = useRef(null); + const rubberLineInstance = React.useMemo(() => new THREE.Line(), []); // Mirror reactive props inside Refs to guarantee useFrame loop never closes over stale state const hoveredNodeIdRef = useRef(null); @@ -151,7 +152,7 @@ const EditorScene: React.FC = ({ /> {/* 2. Drag Rubber Band Preview Line (WebGL optimized) */} - + = ({ transparent opacity={0.9} /> - + {/* 3. Render Established Connections */}