fix(city): correct traffic signal orientation for right-hand traffic

- Fix vehicle signal head rotation to face approaching traffic correctly
- Refactor pedestrian signal positioning with descriptive naming
- Add getSignalHeads() method for snapshotting signal state
- Add cache-busting query parameter to app.js script tag
This commit is contained in:
2026-10-31 16:25:04 -03:00
parent 10603a120f
commit bd6801994a
6 changed files with 86 additions and 19 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

+85 -18
View File
@@ -556,13 +556,15 @@ function initTrafficLights(g) {
group.add(pole);
// Mast arm over the vertical road (northbound lane), facing
// south so approaching traffic sees the signal.
// south so approaching traffic sees the signal. Northbound
// drives on the east half (right-hand traffic); the SE-corner
// pole sits on that same (right) side.
const armNS = new THREE.Mesh(new THREE.BoxGeometry(0.12, 0.1, 2.2), poleMat);
armNS.position.set(x + sw, 4.0, z + sw - 1.1);
group.add(armNS);
const headNS = makeSignalHead();
headNS.position.set(x + sw, 3.7, z + sw - 2.2);
headNS.rotation.y = Math.PI; // face -Z (south)
headNS.rotation.y = 0; // face +Z (south — toward northbound traffic)
group.add(headNS);
heads.push({ head: headNS, axis: 'z' });
@@ -578,23 +580,30 @@ function initTrafficLights(g) {
// Pedestrian signals: short posts on the same corner, well
// inside the sidewalk, facing the crosswalks they control.
const pedZ = makePedSignal();
const pedZPole = new THREE.Mesh(new THREE.CylinderGeometry(0.05, 0.06, 2.4, 6), pedPoleMat);
pedZPole.position.set(x + sw + 0.6, 1.2, z + sw - 0.6);
group.add(pedZPole);
pedZ.position.set(x + sw + 0.6, 2.5, z + sw - 0.6);
pedZ.rotation.y = Math.PI; // faces south crosswalk
group.add(pedZ);
pedHeads.push({ head: pedZ, axis: 'z' });
//
// The east crosswalk crosses the horizontal (x) road, so its
// signal follows the x-axis vehicle state. It sits on the east
// sidewalk facing north, for walkers arriving from the north.
const pedEast = makePedSignal();
const pedEastPole = new THREE.Mesh(new THREE.CylinderGeometry(0.05, 0.06, 2.4, 6), pedPoleMat);
pedEastPole.position.set(x + sw + 0.6, 1.2, z + sw - 0.6);
group.add(pedEastPole);
pedEast.position.set(x + sw + 0.6, 2.5, z + sw - 0.6);
pedEast.rotation.y = Math.PI; // faces north (approach from the north)
group.add(pedEast);
pedHeads.push({ head: pedEast, axis: 'x' });
const pedX = makePedSignal();
const pedXPole = new THREE.Mesh(new THREE.CylinderGeometry(0.05, 0.06, 2.4, 6), pedPoleMat);
pedXPole.position.set(x + sw - 0.6, 1.2, z + sw + 0.6);
group.add(pedXPole);
pedX.position.set(x + sw - 0.6, 2.5, z + sw + 0.6);
pedX.rotation.y = -Math.PI / 2; // faces west crosswalk
group.add(pedX);
pedHeads.push({ head: pedX, axis: 'x' });
// The south crosswalk crosses the vertical (z) road, so its
// signal follows the z-axis vehicle state. It sits on the south
// sidewalk facing west, for walkers arriving from the east.
const pedSouth = makePedSignal();
const pedSouthPole = new THREE.Mesh(new THREE.CylinderGeometry(0.05, 0.06, 2.4, 6), pedPoleMat);
pedSouthPole.position.set(x + sw - 0.6, 1.2, z + sw + 0.6);
group.add(pedSouthPole);
pedSouth.position.set(x + sw - 0.6, 2.5, z + sw + 0.6);
pedSouth.rotation.y = -Math.PI / 2; // faces west (approach from the east)
group.add(pedSouth);
pedHeads.push({ head: pedSouth, axis: 'z' });
}
}
@@ -4329,6 +4338,64 @@ window.__city10 = {
if (trafficSystem) trafficSystem.setPhase(name);
return this.trafficPhase;
},
/**
* Snapshot of every signal head (position, facing, axis and the lamp
* currently lit) so automated tests can assert placement and states.
*/
trafficLightHeads() {
if (!trafficSystem) return { vehicle: [], pedestrian: [] };
const vehicle = trafficSystem.heads.map(({ head, axis }) => {
const intensities = head.userData.lamps.map(
l => +l.material.emissiveIntensity.toFixed(2)
);
const active = ['red', 'yellow', 'green'][intensities.indexOf(Math.max(...intensities))];
return {
axis,
x: +head.position.x.toFixed(2),
y: +head.position.y.toFixed(2),
z: +head.position.z.toFixed(2),
rotationY: +head.rotation.y.toFixed(3),
active,
intensities,
};
});
const pedestrian = trafficSystem.pedHeads.map(({ head, axis }) => {
const red = +head.userData.lamps.red.material.emissiveIntensity.toFixed(2);
const green = +head.userData.lamps.green.material.emissiveIntensity.toFixed(2);
return {
axis,
x: +head.position.x.toFixed(2),
y: +head.position.y.toFixed(2),
z: +head.position.z.toFixed(2),
rotationY: +head.rotation.y.toFixed(3),
active: red > green ? 'red' : 'green',
};
});
return { vehicle, pedestrian };
},
/**
* Per-vehicle stop-line probe: travel axis, distance to the next stop
* line and current speed, for red-light compliance checks.
*/
trafficProbe() {
if (!vehicleSystem) return [];
const out = [];
for (const v of vehicleSystem.vehicles) {
if (!v.pts) continue;
const probe = probeAhead(v.pts, v.s, v.L);
const cur = new THREE.Vector3();
posOnPath(v.pts, v.s, v.L, cur);
out.push({
x: +cur.x.toFixed(2),
z: +cur.z.toFixed(2),
speed: +(v.speed || 0).toFixed(2),
axis: probe.axis,
distToStop: +distanceToStopLine(probe, cur).toFixed(2),
state: trafficSystem ? trafficSystem.axisState(probe.axis) : null,
});
}
return out;
},
playerCrossingAllowed,
get barriersDown() { return trainSystem ? trainSystem.blocks : null; },
setHour(h) {
+1 -1
View File
@@ -372,5 +372,5 @@
citySlug: @json($city->slug),
};
</script>
<script type="module" src="{{ asset('js/city/app.js') }}"></script>
<script type="module" src="{{ asset('js/city/app.js') }}?v={{ filemtime(public_path('js/city/app.js')) }}"></script>
@endpush