fix(city): correct vehicle yaw calculation and expose orientation API

- Extracts yawFromDelta helper to standardize rotation math
- Removes incorrect PI/2 offset causing reverse-facing vehicles
- Accounts for direction sign in shuttle vehicle rotation
- Adds vehicleOrientations() for test validation
This commit is contained in:
2026-10-17 22:25:53 -03:00
parent fa25dd7307
commit 18778b3d26
+48 -4
View File
@@ -980,6 +980,15 @@ function probeAhead(pts, s, L) {
return { distToCorner: 999, axis: 'x', corner: pts[0] }; return { distToCorner: 999, axis: 'x', corner: pts[0] };
} }
/**
* Yaw so a mesh whose nose is local +X faces the world direction (dx, dz).
*
* Three.js: local +X after rotation.y=θ becomes (cos θ, 0, -sin θ).
*/
function yawFromDelta(dx, dz) {
return Math.atan2(-dz, dx);
}
function updateLoopVehicle(v, dt) { function updateLoopVehicle(v, dt) {
const probe = probeAhead(v.pts, v.s, v.L); const probe = probeAhead(v.pts, v.s, v.L);
const distToCorner = probe.distToCorner; const distToCorner = probe.distToCorner;
@@ -1008,7 +1017,7 @@ function updateLoopVehicle(v, dt) {
posOnPath(v.pts, v.s + 0.6, v.L, tmp); posOnPath(v.pts, v.s + 0.6, v.L, tmp);
v.mesh.position.copy(pos); v.mesh.position.copy(pos);
if (tmp.distanceToSquared(pos) > 0.0001) { if (tmp.distanceToSquared(pos) > 0.0001) {
v.mesh.rotation.y = Math.atan2(tmp.x - pos.x, tmp.z - pos.z) + Math.PI / 2; v.mesh.rotation.y = yawFromDelta(tmp.x - pos.x, tmp.z - pos.z);
} }
const wheelSpin = v.speed * dt * 3; const wheelSpin = v.speed * dt * 3;
for (const w of v.mesh.userData.wheels || []) w.rotation.z += wheelSpin; for (const w of v.mesh.userData.wheels || []) w.rotation.z += wheelSpin;
@@ -1049,7 +1058,7 @@ function updateShuttleVehicle(v, dt) {
v.mesh.position.copy(pos); v.mesh.position.copy(pos);
const look = new THREE.Vector3().lerpVectors(v.a, v.b, Math.max(0, Math.min(1, v.t + v.dir * 0.05))); const look = new THREE.Vector3().lerpVectors(v.a, v.b, Math.max(0, Math.min(1, v.t + v.dir * 0.05)));
if (look.distanceToSquared(pos) > 0.0001) { if (look.distanceToSquared(pos) > 0.0001) {
v.mesh.rotation.y = Math.atan2(look.x - pos.x, look.z - pos.z) + Math.PI / 2; v.mesh.rotation.y = yawFromDelta(look.x - pos.x, look.z - pos.z);
} }
for (const w of v.mesh.userData.wheels || []) w.rotation.z += v.speed * dt * 3; for (const w of v.mesh.userData.wheels || []) w.rotation.z += v.speed * dt * 3;
} }
@@ -2632,10 +2641,11 @@ function vehicleBoxes() {
posOnPath(v.pts, v.s, v.L, p); posOnPath(v.pts, v.s, v.L, p);
const ahead = new THREE.Vector3(); const ahead = new THREE.Vector3();
posOnPath(v.pts, v.s + 0.6, v.L, ahead); posOnPath(v.pts, v.s + 0.6, v.L, ahead);
rot = Math.atan2(ahead.x - p.x, ahead.z - p.z); rot = yawFromDelta(ahead.x - p.x, ahead.z - p.z);
} else { } else {
p.lerpVectors(v.a, v.b, v.t); p.lerpVectors(v.a, v.b, v.t);
rot = Math.atan2(v.b.x - v.a.x, v.b.z - v.a.z) + Math.PI / 2; const dirSign = v.dir >= 0 ? 1 : -1;
rot = yawFromDelta((v.b.x - v.a.x) * dirSign, (v.b.z - v.a.z) * dirSign);
} }
const halfL = v.mesh?.userData?.halfL ?? (v.pts ? 2.1 : 2.0); const halfL = v.mesh?.userData?.halfL ?? (v.pts ? 2.1 : 2.0);
const halfW = v.mesh?.userData?.halfW ?? 1.0; const halfW = v.mesh?.userData?.halfW ?? 1.0;
@@ -3459,6 +3469,40 @@ window.__city10 = {
}; };
}, },
vehiclePositions() { return vehicleSystem ? vehicleSystem.positions() : []; }, vehiclePositions() { return vehicleSystem ? vehicleSystem.positions() : []; },
/** Sample vehicle heading vs velocity so tests can detect reverse-facing models. */
vehicleOrientations() {
if (!vehicleSystem) return [];
const out = [];
for (const v of vehicleSystem.vehicles) {
const p = new THREE.Vector3();
const ahead = new THREE.Vector3();
if (v.pts) {
posOnPath(v.pts, v.s, v.L, p);
posOnPath(v.pts, v.s + 0.8, v.L, ahead);
} else {
p.lerpVectors(v.a, v.b, v.t);
const lookT = Math.max(0, Math.min(1, v.t + v.dir * 0.05));
ahead.lerpVectors(v.a, v.b, lookT);
}
const dx = ahead.x - p.x;
const dz = ahead.z - p.z;
const len = Math.hypot(dx, dz) || 1;
const vx = dx / len;
const vz = dz / len;
// Mesh nose is local +X → world forward = (cos yaw, -sin yaw).
const yaw = v.mesh.rotation.y;
const fx = Math.cos(yaw);
const fz = -Math.sin(yaw);
const dot = fx * vx + fz * vz;
out.push({
kind: v.mesh?.userData?.kind || 'car',
speed: +(v.speed || 0).toFixed(2),
dot: +dot.toFixed(3),
aligned: dot > 0.5,
});
}
return out;
},
pedestrianPositions() { return pedestrianSystem ? pedestrianSystem.positions() : []; }, pedestrianPositions() { return pedestrianSystem ? pedestrianSystem.positions() : []; },
get me() { return meCharacter; }, get me() { return meCharacter; },
refreshMe: async function () { refreshMe: async function () {