This commit is contained in:
math-pixel
2026-06-02 16:31:36 +02:00
parent 2c194cdd2e
commit 193fc8b4b6
6 changed files with 1135 additions and 256 deletions
+55 -20
View File
@@ -2,7 +2,7 @@ import { useEffect, useRef, useState, useMemo, useCallback } from "react";
import * as THREE from "three";
import { useFrame, useThree } from "@react-three/fiber";
import { EbikeGPSMap } from "@/components/ebike/EbikeGPSMap";
import { EbikeSpeedometer } from "@/components/ebike/EbikeSpeedometer";
import { EbikeSpeedmeter } from "@/components/ebike/EbikeSpeedmeter";
import { InteractableObject } from "@/components/three/interaction/InteractableObject";
import { useLoggedGLTF } from "@/hooks/three/useLoggedGLTF";
import { useClonedObject } from "@/hooks/three/useClonedObject";
@@ -123,11 +123,35 @@ export function Ebike({ position }: EbikeProps): React.JSX.Element {
}, [movementMode, parkedPosition]);
useEffect(() => {
if (model) {
const fork = model.getObjectByName("fourche");
if (fork) {
forkRef.current = fork;
if (!model) return;
// Full recursive search — case-insensitive so it survives export renames.
// Also tries the exact path Moto > * > Fourche as a fallback.
let forkNode: THREE.Object3D | null = null;
model.traverse((child) => {
if (child.name.toLowerCase() === "fourche") {
forkNode = child;
}
});
if (forkNode) {
forkRef.current = forkNode;
console.log("[Ebike] Fork found:", (forkNode as THREE.Object3D).name);
} else {
// Print the full hierarchy tree so you can read the exact node names.
const lines: string[] = [];
function printTree(obj: THREE.Object3D, indent: number): void {
lines.push(" ".repeat(indent * 2) + (obj.name || "(unnamed)"));
for (const child of obj.children) {
printTree(child, indent + 1);
}
}
printTree(model, 0);
console.warn(
'[Ebike] No node matching "fourche" (case-insensitive) found.\nFull hierarchy:\n' +
lines.join("\n"),
);
}
}, [model]);
@@ -156,9 +180,11 @@ export function Ebike({ position }: EbikeProps): React.JSX.Element {
useFrame((_, delta) => {
if (groupRef.current) {
if (movementMode === "ebike") {
// Sound plays whenever the bike is actually moving (speedFactor > 5 %),
// not only while the input key is held.
updateEbikeSounds({
mounted: true,
driving: window.ebikeDriveInputActive === true,
driving: (window.ebikeSpeedFactor ?? 0) > 0.05,
breakdown: window.ebikeBreakdownActive === true,
});
@@ -169,11 +195,11 @@ export function Ebike({ position }: EbikeProps): React.JSX.Element {
];
restingRotationRef.current = groupRef.current.rotation.y;
// Smoothly rotate the front fork ("fourche") up to 15 degrees in its own Z axis
// Smoothly rotate the front fork ("fourche") on its local Z axis
const steerFactor = window.ebikeSteerFactor ?? 0;
if (forkRef.current) {
// 15 degrees is 0.26 radians
const targetForkRotation = steerFactor * 0.26;
// 10 degrees = 0.175 radians
const targetForkRotation = steerFactor * 0.175;
forkRef.current.rotation.z = THREE.MathUtils.lerp(
forkRef.current.rotation.z,
targetForkRotation,
@@ -329,6 +355,9 @@ export function Ebike({ position }: EbikeProps): React.JSX.Element {
scale={EBIKE_WORLD_SCALE}
>
<primitive object={model} />
{/* radius 20 → ~7 unités monde (scale 0.35).
Sphère omnidirectionnelle pour que le raycast fonctionne
quelle que soit l'orientation de la caméra (montée ou à pied). */}
<InteractableObject
kind="trigger"
label={interactionLabel}
@@ -337,16 +366,25 @@ export function Ebike({ position }: EbikeProps): React.JSX.Element {
onPress={handleInteract}
>
<mesh>
<boxGeometry args={[8, 9, 2]} />
<meshBasicMaterial colorWrite={false} depthWrite={false} />
<sphereGeometry args={[8, 15, 12]} />
<meshBasicMaterial colorWrite={false} color={"red"} depthWrite={false} />
</mesh>
</InteractableObject>
{/* Dynamic 3D GPS Dashboard Screen */}
<group position={[0, 7, 0]} rotation={[0, 90, 0]}>
{/* GPS + Speedmeter same group so they are perfectly co-localised.
GPS: full circle (Fresnel mask), renderOrder 10 000
Speedmeter: upper-half arc overlay, renderOrder 10 001
rotation: Math.PI/2 radians = 90° (NOT the number 90 which = ~116.6°) */}
<group position={[2, 6, 0]} rotation={[0, -80, 0]}>
<EbikeSpeedmeter width={3} height={1.5} position={[0, 0.4, 0]} gaugeInnerR={0.33} gaugeOuterR={0.445}
gaugeWidth={2.5}
gaugeHeight={2.1}
gaugeOffsetX={0}
gaugeOffsetY={-0.19}
/>
<EbikeGPSMap
width={0.8}
height={0.8}
width={1.3}
height={1}
startPos={gpsStartPos}
destPos={destPos}
mapImageUrl="/assets/world/gps/map_background.png"
@@ -359,15 +397,12 @@ export function Ebike({ position }: EbikeProps): React.JSX.Element {
zoom={4}
/>
</group>
<group position={[0, 6.35, 0]} rotation={[0, 90, 0]}>
<EbikeSpeedometer />
</group>
</group>
) : null}
{showCameraPoints && !repairGameOwnsEbikeModel && (
<>
<mesh position={camPointPos}>
{/* <mesh position={camPointPos}>
<sphereGeometry args={[0.3, 16, 16]} />
<meshStandardMaterial
color="yellow"
@@ -382,7 +417,7 @@ export function Ebike({ position }: EbikeProps): React.JSX.Element {
emissive="cyan"
emissiveIntensity={0.5}
/>
</mesh>
</mesh> */}
</>
)}
</>