diff --git a/tools/mapview/README.md b/tools/mapview/README.md
index e22cd44..4d77edb 100644
--- a/tools/mapview/README.md
+++ b/tools/mapview/README.md
@@ -62,8 +62,20 @@ convenient nearby image.
## Orientation
-Seen from above with +Z up the page, the engine's +X runs to the **left**. The
-console's own picture of Brewer's Bane is what settles it: long leg up the right
-side to Score Zone 1, the corner, then the run out to Score Zone 2. Drawn the
-other way round every plan comes out mirrored, which is the bug this shares a
-fix with in [../pages/](../pages/).
+**+X runs right, +Z runs up.** That is the engine's projection rather than a
+choice. `L4GaugeImagePrimitive::Draw` plots `MoveToAbsolute(dest->x, dest->z)`,
+so the screen axes are view-space X and Z; and the graphics view's origin is at
+the bottom left with Y increasing upward — `BackgroundLine` draws
+`endpoints.bottomLeft` to `endpoints.topRight`, and the port's zero-degree blit
+is documented with the origin at the bottom-left corner.
+
+Worth knowing: **the setup console's pictures of the tracks are mirrored against
+this.** They are illustrations, not screenshots. The plans in `TRACKS.html`
+deliberately follow the console pictures, because nine of those cards *are* the
+console pictures and the reconstructed nine have to sit beside them consistently.
+This viewer follows the game instead. The two disagree by a mirror, and both are
+right for what they are.
+
+The nav display also centres on the vehicle and turns with it, inverting the
+viewer's transform and taking yaw only unless `rockAndRoll` is set. Heading 0
+here is the north-up case; Q/E turn the map.
diff --git a/tools/mapview/mapview.html b/tools/mapview/mapview.html
index e5c2133..a738743 100644
--- a/tools/mapview/mapview.html
+++ b/tools/mapview/mapview.html
@@ -108,6 +108,8 @@ kbd {
↑↓←→pan N/S/E/W
+-zoom
[]previous / next track
+ QEturn the map ±15°
+ Rheading back to zero
Ffit the course
Afit everything, strays included
GGPS / NavDisplay LOD
@@ -131,6 +133,21 @@ kbd {
Colours are the game's own: palette indices resolved through
secpal.pcc, the palette the pod's secondary port is configured
with. The walls are grey because index 51 is grey.
+
+ +X runs right and +Z runs up, which is the engine's
+ projection and not a choice: L4GaugeImagePrimitive::Draw plots
+ (dest->x, dest->z), and the graphics view has its origin at
+ the bottom left with Y increasing upward. Note the setup console's own
+ pictures of the tracks are mirrored against this — they are
+ illustrations, not screenshots. The plans in TRACKS.html follow
+ the console pictures so the drawn and reconstructed cards agree with each
+ other; this viewer follows the game.
+
+ Heading. The real nav display centres on your vehicle
+ and turns with it — it inverts the viewer's transform, taking yaw only
+ unless rockAndRoll is set. Free panning at heading 0 is the
+ north-up case; Q and E turn the map to see what it looks
+ like underway.
@@ -145,14 +162,27 @@ const S = {
cx: 0, cz: 0, // view centre, world units
mpp: 10, // meters per pixel - the engine's own scale term
gps: false, // false = NavDisplay (LOD follows zoom), true = GPS
+ yaw: 0, // map heading, radians. NavDisplay takes the
+ // vehicle's; 0 is the map with +Z straight up.
stats: {},
};
-// Seen from above with +Z up the page the engine's +X runs to the LEFT.
-// The console's own picture of Brewer's Bane is what settles the
-// handedness; drawn the other way every plan comes out mirrored.
+// The engine's own projection, and it is not the obvious one.
+// L4GaugeImagePrimitive::Draw plots MoveToAbsolute(dest->x, dest->z) - so
+// the screen axes are view-space X and Z - and the graphics view puts its
+// origin at the bottom left with Y increasing UP (BackgroundLine draws
+// bottomLeft -> topRight, and the port's zero-degree blit is documented
+// with the origin at the bottom left corner). So on the map +X runs RIGHT
+// and +Z runs UP. Canvas y grows downward, hence the single negation.
+//
+// NavDisplay also inverts the viewer's transform before scaling, taking
+// yaw only unless rockAndRoll is set - the map turns with the vehicle and
+// is centred on it. Free panning with yaw 0 is the north-up case.
function toScreen(x, z, w, h) {
- return [w / 2 - (x - S.cx) / S.mpp, h / 2 - (z - S.cz) / S.mpp];
+ const dx = x - S.cx, dz = z - S.cz;
+ const c = Math.cos(S.yaw), s = Math.sin(S.yaw);
+ return [w / 2 + (dx * c - dz * s) / S.mpp,
+ h / 2 - (dx * s + dz * c) / S.mpp];
}
// Quaternion (x,y,z,w) applied to a point - MUNGA's own convention.
@@ -276,6 +306,7 @@ function hud() {
['pixelsPerMeter', px.toFixed(4)],
['LOD value', s.lodValue.toFixed(3)],
['centre X / Z', S.cx.toFixed(0) + ' / ' + S.cz.toFixed(0)],
+ ['heading', (((S.yaw * 180 / Math.PI) % 360 + 360) % 360).toFixed(0) + '°'],
['objects drawn', s.drawn],
['dropped by LOD', s.culled],
['primitives', s.prims],
@@ -292,10 +323,13 @@ function hud() {
}
// ------------------------------------------------------------ controls
-function pan(dx, dz) {
+// Pan in what you see, not in world axes: with the map turned, "up" has
+// to stay up. Screen delta rotated back out through the heading.
+function pan(sx, sy) {
const step = 0.1 * Math.min(cv.clientWidth, cv.clientHeight) * S.mpp;
- S.cx += dx * step;
- S.cz += dz * step;
+ const c = Math.cos(S.yaw), s = Math.sin(S.yaw);
+ S.cx += (sx * c + sy * s) * step;
+ S.cz += (-sx * s + sy * c) * step;
draw();
}
@@ -313,11 +347,14 @@ function pick(n) {
addEventListener('keydown', e => {
if (e.target.tagName === 'SELECT') return;
const k = e.key;
- // Right on screen is -X, up is +Z: north/south/east/west as drawn.
+ // Screen directions: right is +X and up is +Z when the heading is zero.
if (k === 'ArrowUp') pan(0, 1);
else if (k === 'ArrowDown') pan(0, -1);
- else if (k === 'ArrowLeft') pan(1, 0);
- else if (k === 'ArrowRight') pan(-1, 0);
+ else if (k === 'ArrowLeft') pan(-1, 0);
+ else if (k === 'ArrowRight') pan(1, 0);
+ else if (k === 'q' || k === 'Q') { S.yaw -= Math.PI / 12; draw(); }
+ else if (k === 'e' || k === 'E') { S.yaw += Math.PI / 12; draw(); }
+ else if (k === 'r' || k === 'R') { S.yaw = 0; draw(); }
else if (k === '+' || k === '=') zoom(1 / 1.25);
else if (k === '-' || k === '_') zoom(1.25);
else if (k === '[') pick(S.track - 1);
@@ -336,8 +373,10 @@ cv.addEventListener('pointerdown', e => {
});
cv.addEventListener('pointermove', e => {
if (!drag) return;
- S.cx -= (e.clientX - drag.x) * S.mpp;
- S.cz += (e.clientY - drag.y) * S.mpp;
+ const dx = (e.clientX - drag.x) * S.mpp, dy = (e.clientY - drag.y) * S.mpp;
+ const c = Math.cos(S.yaw), s = Math.sin(S.yaw);
+ S.cx -= dx * c - dy * s;
+ S.cz -= -dx * s - dy * c;
drag = {x: e.clientX, y: e.clientY};
draw();
});
diff --git a/tools/mapview/mapview.js b/tools/mapview/mapview.js
index 925a2e5..1542d70 100644
--- a/tools/mapview/mapview.js
+++ b/tools/mapview/mapview.js
@@ -9,14 +9,27 @@ const S = {
cx: 0, cz: 0, // view centre, world units
mpp: 10, // meters per pixel - the engine's own scale term
gps: false, // false = NavDisplay (LOD follows zoom), true = GPS
+ yaw: 0, // map heading, radians. NavDisplay takes the
+ // vehicle's; 0 is the map with +Z straight up.
stats: {},
};
-// Seen from above with +Z up the page the engine's +X runs to the LEFT.
-// The console's own picture of Brewer's Bane is what settles the
-// handedness; drawn the other way every plan comes out mirrored.
+// The engine's own projection, and it is not the obvious one.
+// L4GaugeImagePrimitive::Draw plots MoveToAbsolute(dest->x, dest->z) - so
+// the screen axes are view-space X and Z - and the graphics view puts its
+// origin at the bottom left with Y increasing UP (BackgroundLine draws
+// bottomLeft -> topRight, and the port's zero-degree blit is documented
+// with the origin at the bottom left corner). So on the map +X runs RIGHT
+// and +Z runs UP. Canvas y grows downward, hence the single negation.
+//
+// NavDisplay also inverts the viewer's transform before scaling, taking
+// yaw only unless rockAndRoll is set - the map turns with the vehicle and
+// is centred on it. Free panning with yaw 0 is the north-up case.
function toScreen(x, z, w, h) {
- return [w / 2 - (x - S.cx) / S.mpp, h / 2 - (z - S.cz) / S.mpp];
+ const dx = x - S.cx, dz = z - S.cz;
+ const c = Math.cos(S.yaw), s = Math.sin(S.yaw);
+ return [w / 2 + (dx * c - dz * s) / S.mpp,
+ h / 2 - (dx * s + dz * c) / S.mpp];
}
// Quaternion (x,y,z,w) applied to a point - MUNGA's own convention.
@@ -140,6 +153,7 @@ function hud() {
['pixelsPerMeter', px.toFixed(4)],
['LOD value', s.lodValue.toFixed(3)],
['centre X / Z', S.cx.toFixed(0) + ' / ' + S.cz.toFixed(0)],
+ ['heading', (((S.yaw * 180 / Math.PI) % 360 + 360) % 360).toFixed(0) + '°'],
['objects drawn', s.drawn],
['dropped by LOD', s.culled],
['primitives', s.prims],
@@ -156,10 +170,13 @@ function hud() {
}
// ------------------------------------------------------------ controls
-function pan(dx, dz) {
+// Pan in what you see, not in world axes: with the map turned, "up" has
+// to stay up. Screen delta rotated back out through the heading.
+function pan(sx, sy) {
const step = 0.1 * Math.min(cv.clientWidth, cv.clientHeight) * S.mpp;
- S.cx += dx * step;
- S.cz += dz * step;
+ const c = Math.cos(S.yaw), s = Math.sin(S.yaw);
+ S.cx += (sx * c + sy * s) * step;
+ S.cz += (-sx * s + sy * c) * step;
draw();
}
@@ -177,11 +194,14 @@ function pick(n) {
addEventListener('keydown', e => {
if (e.target.tagName === 'SELECT') return;
const k = e.key;
- // Right on screen is -X, up is +Z: north/south/east/west as drawn.
+ // Screen directions: right is +X and up is +Z when the heading is zero.
if (k === 'ArrowUp') pan(0, 1);
else if (k === 'ArrowDown') pan(0, -1);
- else if (k === 'ArrowLeft') pan(1, 0);
- else if (k === 'ArrowRight') pan(-1, 0);
+ else if (k === 'ArrowLeft') pan(-1, 0);
+ else if (k === 'ArrowRight') pan(1, 0);
+ else if (k === 'q' || k === 'Q') { S.yaw -= Math.PI / 12; draw(); }
+ else if (k === 'e' || k === 'E') { S.yaw += Math.PI / 12; draw(); }
+ else if (k === 'r' || k === 'R') { S.yaw = 0; draw(); }
else if (k === '+' || k === '=') zoom(1 / 1.25);
else if (k === '-' || k === '_') zoom(1.25);
else if (k === '[') pick(S.track - 1);
@@ -200,8 +220,10 @@ cv.addEventListener('pointerdown', e => {
});
cv.addEventListener('pointermove', e => {
if (!drag) return;
- S.cx -= (e.clientX - drag.x) * S.mpp;
- S.cz += (e.clientY - drag.y) * S.mpp;
+ const dx = (e.clientX - drag.x) * S.mpp, dy = (e.clientY - drag.y) * S.mpp;
+ const c = Math.cos(S.yaw), s = Math.sin(S.yaw);
+ S.cx -= dx * c - dy * s;
+ S.cz -= -dx * s - dy * c;
drag = {x: e.clientX, y: e.clientY};
draw();
});
diff --git a/tools/mapview/mapview.tpl b/tools/mapview/mapview.tpl
index 2148fbf..51d7765 100644
--- a/tools/mapview/mapview.tpl
+++ b/tools/mapview/mapview.tpl
@@ -28,6 +28,8 @@
↑↓←→pan N/S/E/W
+-zoom
[]previous / next track
+ QEturn the map ±15°
+ Rheading back to zero
Ffit the course
Afit everything, strays included
GGPS / NavDisplay LOD
@@ -51,6 +53,21 @@
Colours are the game's own: palette indices resolved through
secpal.pcc, the palette the pod's secondary port is configured
with. The walls are grey because index 51 is grey.
+
+ +X runs right and +Z runs up, which is the engine's
+ projection and not a choice: L4GaugeImagePrimitive::Draw plots
+ (dest->x, dest->z), and the graphics view has its origin at
+ the bottom left with Y increasing upward. Note the setup console's own
+ pictures of the tracks are mirrored against this — they are
+ illustrations, not screenshots. The plans in TRACKS.html follow
+ the console pictures so the drawn and reconstructed cards agree with each
+ other; this viewer follows the game.
+
+ Heading. The real nav display centres on your vehicle
+ and turns with it — it inverts the viewer's transform, taking yaw only
+ unless rockAndRoll is set. Free panning at heading 0 is the
+ north-up case; Q and E turn the map to see what it looks
+ like underway.