import React, { useEffect, useRef } from 'react' import { useFrame } from '@react-three/fiber' import * as THREE from 'three' import { useSceneStore } from '../store/useSceneStore' import { useTruckMovement } from '../hooks/useTruckMovement' import { animateWheels } from '../animations/wheelAnimation' import { easing } from 'maath' import { truckPath } from '../curves/truckPath' export default function TruckAnimation({ truckRef, wheelRefs }) { const scrollProgress = useSceneStore((state) => state.scrollProgress) const activeSection = useSceneStore((state) => state.activeSection) const setTruckProgress = useSceneStore((state) => state.setTruckProgress) const { truckProgress } = useTruckMovement(scrollProgress) const initialized = useRef(false) // Sync truck progress to the global store useEffect(() => { setTruckProgress(truckProgress) }, [truckProgress, setTruckProgress]) // Float trackers for 1D progress and direction detection const dampedProgressRef = useRef(0) const lastScrollProgressRef = useRef(0) const isReversingRef = useRef(false) // Tracker for smooth 180-degree yaw rotation (prevents glitches by pivoting Y rotation angle directly) const extraRotationRef = useRef(0) // Track wheel rotation accumulation const accumulatedRotationRef = useRef(0) const lastDampedProgressRef = useRef(0) useFrame((state, delta) => { if (!truckRef.current) return // r3f can emit delta === 0 (coincident frames, the first frame, or after a // long main-thread block while the 32MB scene parses). maath's easing.damp // divides by delta internally, so a 0 yields NaN/Infinity that poisons the // damper's stored velocity — and from then on truckPath.getPoint(NaN) throws // "Cannot read properties of undefined (reading 'x')". Clamp delta to a safe // positive range before any damping. const dt = Number.isFinite(delta) && delta > 0 ? Math.min(delta, 0.1) : 1 / 60 // Detect scroll direction changes from the actual page scroll progress const deltaScroll = scrollProgress - lastScrollProgressRef.current if (deltaScroll < -0.0001) { isReversingRef.current = true } else if (deltaScroll > 0.0001) { isReversingRef.current = false } lastScrollProgressRef.current = scrollProgress // Ensure correct parent-child structure and orientation for the truck (runs reactively on re-renders) const innerGroup = truckRef.current.children[0] if (innerGroup && truckRef.current.children.length > 1) { const siblings = [...truckRef.current.children].slice(1) siblings.forEach((sibling) => { innerGroup.attach(sibling) }) innerGroup.rotation.set(0, -Math.PI / 2, 0) // Disable frustum culling on all child meshes so the truck/shadow is always visible truckRef.current.traverse((child) => { if (child.isMesh) { child.frustumCulled = false child.castShadow = true child.receiveShadow = true } }) } // Run one-time state initialization for progress trackers if (!initialized.current) { dampedProgressRef.current = truckProgress dampedProgressRef.current_velocity = 0 lastDampedProgressRef.current = truckProgress lastScrollProgressRef.current = scrollProgress isReversingRef.current = false extraRotationRef.current = 0 extraRotationRef.current_velocity = 0 const position = truckPath.getPoint(dampedProgressRef.current) let lookAtTargetVector if (dampedProgressRef.current >= 0.99) { const tangent = truckPath.getTangent(1.0) const endPoint = truckPath.getPoint(1.0) lookAtTargetVector = new THREE.Vector3().copy(endPoint).addScaledVector(tangent, 1.0) } else { const ahead = Math.min(dampedProgressRef.current + 0.01, 1.0) lookAtTargetVector = truckPath.getPoint(ahead) } truckRef.current.position.copy(position) if (truckRef.current.position.distanceToSquared(lookAtTargetVector) > 0.0001) { truckRef.current.lookAt(lookAtTargetVector) } initialized.current = true } // Smoothly damp the 1D progress scalar along the curve path easing.damp(dampedProgressRef, 'current', truckProgress, 0.30, dt) // Defensive: keep the spline parameter a finite value in [0,1]. getPoint(NaN) // or an out-of-range t reads an undefined curve point and throws. if (!Number.isFinite(dampedProgressRef.current)) { dampedProgressRef.current = truckProgress if (dampedProgressRef.__damp) dampedProgressRef.__damp = {} // clear any poisoned velocity } dampedProgressRef.current = THREE.MathUtils.clamp(dampedProgressRef.current, 0, 1) // Evaluate the 3D position and orientation directly on the spline curve const position = truckPath.getPoint(dampedProgressRef.current) let lookAtTargetVector if (dampedProgressRef.current >= 0.99) { const tangent = truckPath.getTangent(1.0) const endPoint = truckPath.getPoint(1.0) lookAtTargetVector = new THREE.Vector3().copy(endPoint).addScaledVector(tangent, 1.0) } else { const ahead = Math.min(dampedProgressRef.current + 0.01, 1.0) lookAtTargetVector = truckPath.getPoint(ahead) } // Update position and base forward rotation directly (ensures 100% spline compliance, zero corner cutting) truckRef.current.position.copy(position) if (truckRef.current.position.distanceToSquared(lookAtTargetVector) > 0.0001) { truckRef.current.lookAt(lookAtTargetVector) } // Determine target extra rotation: // - 0 radians when moving forward // - Math.PI radians (180 degrees) when reversing // We disable U-turns at the extreme start and end of the path to keep the truck stable at warehouse/delivery spots let targetExtraRotation = 0 if (dampedProgressRef.current > 0.05 && dampedProgressRef.current < 0.95) { if (isReversingRef.current) { targetExtraRotation = Math.PI } } // Smoothly damp the extra rotation angle directly (prevents pitch/roll glitches or 3D target collapse) easing.damp(extraRotationRef, 'current', targetExtraRotation, 0.20, dt) // Defensive: reset extra rotation if it becomes NaN if (!Number.isFinite(extraRotationRef.current)) { extraRotationRef.current = targetExtraRotation extraRotationRef.current_velocity = 0 if (extraRotationRef.__damp) extraRotationRef.__damp = {} } // Apply the yaw pivot around the local vertical axis truckRef.current.rotateY(extraRotationRef.current) // Calculate progress delta for wheels and audio const deltaDamped = Math.abs(dampedProgressRef.current - lastDampedProgressRef.current) lastDampedProgressRef.current = dampedProgressRef.current // Accumulate wheel rotation based on absolute movement delta so they always roll forward locally const isMoving = dampedProgressRef.current > 0.001 && dampedProgressRef.current < 0.999 if (isMoving) { accumulatedRotationRef.current += deltaDamped * 250 // spinFactor } // Spin wheels animateWheels(wheelRefs, accumulatedRotationRef.current) // Add engine vibration to the inner group to prevent coordinate pollution on the root group if (truckRef.current.children && truckRef.current.children[0]) { const innerGroup = truckRef.current.children[0] innerGroup.position.y = Math.sin(state.clock.getElapsedTime() * 45) * 0.003 } }) return null }