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:
+48
-4
@@ -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 () {
|
||||||
|
|||||||
Reference in New Issue
Block a user