Compare commits

...
Author SHA1 Message Date
German Khokhlov bdd6081fa0 docs: describe the shared attribution anchor 2026-11-06 21:31:56 +04:00
German Khokhlov d496de0d40 test(qa): check Tactical and Cockpit readout clearance
Require the 1rem reserve above a credits row no taller than 28px for Minimal and Tactical, and for Minimal in Cockpit. The phone check now asserts only what phones do: hide the readout and keep attribution.
2026-11-06 21:31:56 +04:00
German Khokhlov 055709c12b fix(hud): anchor lower-left readouts to the attribution row
The credits row takes its position from --gev-credits-bottom, and the
lower-left readout of Minimal, Tactical, Cyber and Cockpit sits
--gev-credits-reserve (2.75rem) above it: 16px over the 28px logo row.
Tactical's box overlapped the row by 4px, and Cockpit's corner offset
outranked every layout and put the readout 21-32px into it. Cockpit now
scales the readout from its bottom edge so it stays below the context
window.

Remove the overridden Minimal offsets in overlays.css, command-dock.css
and command-dock-compact.css so one rule owns each position.
2026-11-06 21:31:56 +04:00
German Khokhlov d61a9225eb Merge remote-tracking branch 'origin/main' into fix/minimal-hud-bottom-banner 2026-11-06 20:39:40 +04:00
Said Turksever f45d4bcc93 fix: resolve review findings and refresh PR #1523 against main 2026-11-03 16:17:24 +00:00
Said Turksever 3689d2e6ca docs: complete review bookkeeping for #1523 2026-11-03 16:10:09 +00:00
Said Turksever 6af7cef3a3 Merge remote-tracking branch 'upstream/main' into codex/pr1523-current-main 2026-11-03 15:11:27 +00:00
Said Turksever 25340f735c test: cover Minimal HUD banner and attribution clearance 2026-11-03 15:07:58 +00:00
Said Turksever 2a6a3922af Keep the Minimal HUD readout clear of the attribution row
The bottom-left MGRS / lat-lon readout sat 5.4rem up while the Cesium /
Esri credits row, logo included, reaches about 6.75rem, so the two
overlapped. Raise it to 7.25rem and drop the Scope-off override: the
backplates add no vertical padding, so both modes now leave the same gap.
2026-11-03 14:46:32 +00:00
Said Turksever 11602742a8 Document the Minimal HUD banner removal (#1523) 2026-11-03 14:31:16 +00:00
Said Turksever 657277dbf9 Hide the bottom MGRS banner in the Minimal HUD layout
The bottom-left readout already shows MGRS and lat/lon, and the centred
bar sat across the globe.
2026-11-03 14:30:55 +00:00
11 changed files with 205 additions and 89 deletions

No files matched your search

@@ -0,0 +1,4 @@
- The Minimal HUD layout no longer draws the centred MGRS / LAT / LON banner
across the globe; the bottom-left readout still shows the same position. In
Minimal, Tactical and Cockpit that readout now sits clear of the map
attribution row instead of overlapping it (Said Turksever, #1523).
+10
View File
@@ -458,6 +458,16 @@ that names Operator restores the default Tactical layout. Operator was the
only layout that showed the HUD's top banner bar, so its markup and styles are
removed too.
Minimal no longer shows the centred MGRS / LAT / LON bar near the bottom of the
globe (November 3, 2026, #1523): the bottom-left readout already carries the
same position, and the bar sat across the globe. Cyber keeps its own bar. The
attribution row's position is one variable, `--gev-credits-bottom` in
`command-dock-trays.css`, and the lower-left readout of Minimal, Tactical, Cyber
and Cockpit sits `--gev-credits-reserve` (2.75rem) above it, which leaves 1rem
over the 28px logo row with Scope on or off. Phones hide that readout.
`scripts/qa-scope-hud.mjs` checks the hidden banner and that clearance against
the rendered attribution row.
## Cyber HUD — September 30, 2026
Display > HUD > Layout includes Cyber, also available through the HUD voice
+114 -1
View File
@@ -5,7 +5,9 @@
* Optional: QA_BASE_URL, QA_SCREENSHOT_DIR (screenshots stay outside the repo).
* Proves a fresh boot defaults to Scope ON at 100%, native checkbox click/Space,
* feather 0/11/100/200, layout independence,
* share/scene restore, and visible text backing including rotated metadata.
* share/scene restore, visible text backing including rotated metadata,
* Minimal's banner removal, and coordinate/attribution clearance for Minimal
* and Tactical in map view and Minimal in Cockpit.
*/
import fs from 'node:fs';
import path from 'node:path';
@@ -666,6 +668,117 @@ try {
await settleTextBackings(false);
checkSceneChrome(`narrow ${layout}`, before, await readSceneChrome());
}
// The lower-left readout sits --gev-credits-reserve (2.75rem) above the
// attribution anchor; the reserve covers a 28px logo row, so the readout box
// clears the row by 1rem.
const readCornerGeometry = () =>
page.evaluate(() => {
const banner = document.querySelector('#intel-hud .hud-bottom-bar');
const coordinates = document.querySelector('#intel-hud .hud-bottom-left');
// The fixed host has zero size; Cesium positions its actual row inside it.
const credits = document.querySelector(
'#cesium-credits .cesium-widget-credits',
);
const coordinateBox = coordinates.getBoundingClientRect();
const creditBox = credits.getBoundingClientRect();
return {
bannerHidden: getComputedStyle(banner).display === 'none',
coordinatesVisible: coordinateBox.width > 0 && coordinateBox.height > 0,
coordinatesHidden: getComputedStyle(coordinates).display === 'none',
creditsVisible: creditBox.width > 0 && creditBox.height > 0,
creditRowHeight: creditBox.height,
gap: creditBox.top - coordinateBox.bottom,
bottom: getComputedStyle(coordinates).bottom,
};
});
const clearsCredits = (geometry) =>
geometry.coordinatesVisible &&
geometry.creditsVisible &&
geometry.creditRowHeight <= 28.5 &&
geometry.gap >= 15.5;
for (const viewport of [
{ width: 1600, height: 900 },
{ width: 1024, height: 700 },
{ width: 820, height: 700 },
{ width: 390, height: 844 },
]) {
await page.setViewport(viewport);
for (const layout of ['minimal', 'tactical']) {
await page.select('#hud-layout-select', layout);
for (const enabled of [true, false]) {
await page.evaluate(
(enabled) =>
window.__godsEyeView.styleManager.applyVisualState({
scope: { enabled },
}),
enabled,
);
await settleTextBackings(enabled);
// Wait for the responsive shell and the real attribution logo to settle.
await page.waitForFunction(
(mobile) =>
document.body.classList.contains('mobile-shell') === mobile &&
[...document.querySelectorAll('#cesium-credits img')].some(
(img) => img.complete && img.naturalWidth > 0,
),
{},
viewport.width <= 720,
);
const geometry = await readCornerGeometry();
const name = `${layout === 'minimal' ? 'Minimal' : 'Tactical'} ${viewport.width}px Scope ${enabled ? 'ON' : 'OFF'}`;
if (layout === 'minimal') {
check(
`${name}: centred coordinate banner stays hidden`,
geometry.bannerHidden,
geometry,
);
}
if (viewport.width <= 720) {
check(
`${name}: phone hides the coordinate readout and keeps attribution`,
geometry.coordinatesHidden && geometry.creditsVisible,
geometry,
);
} else {
check(
`${name}: coordinate block clears the full attribution row by 1rem`,
clearsCredits(geometry),
geometry,
);
}
}
}
}
// Cockpit's corner offset outranks every layout's. The body class carries
// that layout; entering for real needs a tracked aircraft.
await page.select('#hud-layout-select', 'minimal');
for (const viewport of [
{ width: 1600, height: 900 },
{ width: 1024, height: 700 },
]) {
await page.setViewport(viewport);
await page.waitForFunction(
() => !document.body.classList.contains('mobile-shell'),
);
await page.evaluate(() => document.body.classList.add('cockpit-mode'));
for (const enabled of [true, false]) {
await page.evaluate(
(enabled) =>
window.__godsEyeView.styleManager.applyVisualState({
scope: { enabled },
}),
enabled,
);
await settleTextBackings(enabled);
const geometry = await readCornerGeometry();
check(
`Cockpit Minimal ${viewport.width}px Scope ${enabled ? 'ON' : 'OFF'}: coordinate block clears the full attribution row by 1rem`,
clearsCredits(geometry),
geometry,
);
}
await page.evaluate(() => document.body.classList.remove('cockpit-mode'));
}
check('runtime has no uncaught browser errors', errors.length === 0, errors);
} finally {
await browser.close();
+19 -2
View File
@@ -299,9 +299,26 @@ function resolve(candidates, prop, width, label) {
return winner;
}
/**
* Inline a custom property declared once, on a bare :root, so an offset written
* through it evaluates to the length it stands for. Anything else stays a
* var() for toPx to vet.
*/
function inlineRootVars(value) {
const inlined = value.replace(/var\((--[\w-]+)\)/g, (match, name) => {
const declarations = RULES.flatMap((rule) =>
rule.decls.filter((decl) => decl.prop === name).map((decl) => ({ rule, decl })));
if (declarations.length !== 1) return match;
const [{ rule, decl }] = declarations;
if (rule.media.length || rule.parts.join() !== ':root') return match;
return decl.value.replace(/^calc\((.*)\)$/, '$1');
});
return inlined === value || /^calc\(/.test(inlined) ? inlined : `calc(${inlined})`;
}
/** Evaluate a length. Anything the model cannot resolve exactly fails loudly. */
function toPx(value, viewportHeight, where) {
const trimmed = value.trim();
const trimmed = inlineRootVars(value.trim());
const inner = /^calc\(/.test(trimmed) ? trimmed.slice(5, -1) : trimmed;
assert.doesNotMatch(inner, /\bcalc\(/, `nested calc() in ${where}: "${value}"`);
assert.doesNotMatch(inner, /\b(min|max|clamp|env|attr|round|mod)\(/, `unmodelled function in ${where}: "${value}"`);
@@ -579,7 +596,7 @@ test('the dock anchor changes at 720px — the 2vh cancellation is band-limited'
assert.equal(resolve(['#command-dock'], 'bottom', 800, 'dock').decl.value, '2vh');
assert.equal(resolve(['#command-dock'], 'bottom', 720, 'dock').decl.value, '8px');
assert.equal(
resolve(CREDIT_SELECTORS, 'bottom', 720, 'credit').decl.value,
inlineRootVars(resolve(CREDIT_SELECTORS, 'bottom', 720, 'credit').decl.value),
'calc(2vh + 5rem)',
'the credit keeps its 2vh base below 720px — that asymmetry is the whole hazard',
);
+37 -13
View File
@@ -429,24 +429,48 @@ test('Cyber dock pins escape the tray mask and its decoration overrides the lega
assert.match(decoration, /pointer-events:\s*none;/);
});
test('Cyber lower-left telemetry tracks the credit anchor with clearance for its logo row', () => {
const telemetry = cssRuleBody(
cyberStyles,
/#intel-hud\[data-variant='cyber'\] \.hud-bottom-left/,
);
const offset = telemetry.match(/bottom:\s*calc\(2vh \+ ([\d.]+)rem\);/);
assert.ok(offset, 'telemetry must retain the attribution viewport anchor');
test('lower-left telemetry tracks the credit anchor with clearance for its logo row', () => {
const credits = read('./styles/command-dock-trays.css');
const anchor = cssRuleBody(credits, /:root/);
assert.match(anchor, /--gev-credits-bottom:\s*calc\(2vh \+ 5rem\);/);
const reserve = anchor.match(/--gev-credits-reserve:\s*([\d.]+)rem;/);
assert.ok(
Number(reserve?.[1]) >= 2.75,
'reserve the 28px logo row, baseline descent and 12px clearance at the 16px root size',
);
const creditOffsets = [
...credits.matchAll(
/#cesium-credits\s*\{[^}]*bottom:\s*calc\(2vh \+ ([\d.]+)rem\);/g,
),
...credits.matchAll(/#cesium-credits\s*\{[^}]*bottom:\s*([^;]+);/g),
];
assert.ok(creditOffsets.length > 0);
for (const credit of creditOffsets) {
assert.ok(
Number(offset[1]) - Number(credit[1]) >= 2.75,
'reserve the 28px logo row, baseline descent and 12px clearance at the 16px root size',
assert.equal(credit[1], 'var(--gev-credits-bottom)');
}
const reserved =
/bottom:\s*calc\(var\(--gev-credits-bottom\) \+ var\(--gev-credits-reserve\)\);/;
assert.match(
cssRuleBody(cyberStyles, /#intel-hud\[data-variant='cyber'\] \.hud-bottom-left/),
reserved,
);
assert.match(
cssRuleBody(
credits,
/#intel-hud\[data-variant='tactical'\] \.hud-bottom-left,\s*#intel-hud\[data-variant='minimal'\] \.hud-bottom-left/,
),
reserved,
);
// Cockpit's corner offset outranks the layouts' rules, so it reserves too.
assert.match(
read('./styles/cockpit.css'),
new RegExp(
`\\nbody\\.cockpit-mode #intel-hud \\.hud-bottom-left\\s*\\{[^}]*${reserved.source}`,
),
);
// One rule owns each layout's lower-left offset; import order must not decide it.
for (const file of ['overlays.css', 'command-dock.css', 'command-dock-compact.css']) {
assert.doesNotMatch(
read(`./styles/${file}`),
/\[data-variant='\w+'\] \.hud-bottom-left\s*\{[^}]*bottom:/,
file,
);
}
assert.doesNotMatch(cyberStyles, /\.hud-bottom-left\s*\{\s*bottom:\s*60px;/);
+7 -1
View File
@@ -2059,10 +2059,16 @@ body.cockpit-mode #intel-hud .hud-top-left,
body.cockpit-mode #intel-hud .hud-top-right {
top: clamp(104px, 13vh, 138px);
}
body.cockpit-mode #intel-hud .hud-bottom-left,
body.cockpit-mode #intel-hud .hud-bottom-right {
bottom: clamp(84px, 11vh, 118px);
}
/* The lower-left readout sits over the attribution row, which keeps its map
position in Cockpit. Scaling from its bottom edge keeps it on that reserve
and below the context window's rail. */
body.cockpit-mode #intel-hud .hud-bottom-left {
bottom: calc(var(--gev-credits-bottom) + var(--gev-credits-reserve));
transform-origin: left bottom;
}
body.cockpit-mode #intel-hud .hud-summary-wrap {
max-width: min(34vw, 380px);
}
-8
View File
@@ -380,14 +380,6 @@ body:not(.ui-clean-view):not(.recording-mode) #cesium-credits {
bottom: calc(2vh + 4.7rem);
}
#intel-hud[data-variant='minimal'] .hud-bottom-left {
bottom: calc(2vh + 5.4rem);
}
#intel-hud[data-variant='minimal'] .hud-bottom-bar {
bottom: calc(2vh + 8.9rem);
}
@media (max-width: 900px) {
#command-dock #location-bar .location-toolbar-label {
overflow: hidden;
+12 -14
View File
@@ -448,6 +448,14 @@
letter-spacing: 0.035rem;
}
/* The attribution row's anchor above the command dock. HUD readouts over the
row sit the reserve above it: the 28px logo row, baseline descent and 12px
clearance at the 16px root size. */
:root {
--gev-credits-bottom: calc(2vh + 5rem);
--gev-credits-reserve: 2.75rem;
}
/* Keep the complete inline attribution row above the command dock. */
body:not(.ui-clean-view):not(.recording-mode) #cesium-credits,
body:not(.ui-clean-view):not(.recording-mode):has(
@@ -455,30 +463,20 @@ body:not(.ui-clean-view):not(.recording-mode):has(
)
#cesium-credits {
right: auto;
bottom: calc(2vh + 5rem);
bottom: var(--gev-credits-bottom);
left: 1.5rem;
width: auto !important;
max-width: calc(100vw - 3rem);
text-align: left;
}
#intel-hud[data-variant='tactical'] .hud-bottom-left,
#intel-hud[data-variant='tactical'] .hud-bottom-right {
bottom: calc(2vh + 6.5rem);
}
#intel-hud[data-variant='tactical'] .hud-bottom-left,
#intel-hud[data-variant='minimal'] .hud-bottom-left {
bottom: calc(2vh + 5.4rem);
}
:root[data-scope-mask='off']
#intel-hud[data-variant='minimal']
.hud-bottom-left {
bottom: calc(2vh + 6.5rem);
}
#intel-hud[data-variant='minimal'] .hud-bottom-bar {
bottom: calc(2vh + 9rem);
bottom: calc(var(--gev-credits-bottom) + var(--gev-credits-reserve));
}
@media (max-width: 900px) {
@@ -487,7 +485,7 @@ body:not(.ui-clean-view):not(.recording-mode):has(
#intel-hud[data-variant='minimal'].active
)
#cesium-credits {
bottom: calc(2vh + 5rem);
bottom: var(--gev-credits-bottom);
left: 0.75rem;
max-width: calc(100vw - 1.5rem);
}
-16
View File
@@ -243,14 +243,6 @@ body.recording-mode #cesium-credits {
left: 36px;
}
#intel-hud[data-variant='minimal'] .hud-bottom-left {
bottom: calc(2vh + 7.5rem);
}
#intel-hud[data-variant='minimal'] .hud-bottom-bar {
bottom: calc(2vh + 11.5rem);
}
@media (max-width: 980px) {
#command-dock {
grid-template-areas:
@@ -277,14 +269,6 @@ body.recording-mode #cesium-credits {
body:not(.ui-clean-view):not(.recording-mode) #cesium-credits {
bottom: calc(2vh + 9.5rem);
}
#intel-hud[data-variant='minimal'] .hud-bottom-left {
bottom: calc(2vh + 12rem);
}
#intel-hud[data-variant='minimal'] .hud-bottom-bar {
bottom: calc(2vh + 16rem);
}
}
@media (max-width: 720px) {
+2 -2
View File
@@ -1605,9 +1605,9 @@
}
/* Attribution's logo row extends above its text baseline. Reserve the whole
row plus a gap above the dock-relative 2vh + 5rem credit position. */
row plus a gap above the dock-relative credit anchor. */
#intel-hud[data-variant='cyber'] .hud-bottom-left {
bottom: calc(2vh + 7.75rem);
bottom: calc(var(--gev-credits-bottom) + var(--gev-credits-reserve));
}
#intel-hud[data-variant='cyber'] .hud-corner {
-32
View File
@@ -306,15 +306,6 @@
top: 88px;
}
#intel-hud[data-variant='minimal'] .hud-bottom-left {
bottom: 12vh;
}
#intel-hud[data-variant='minimal'] .hud-bottom-bar {
display: flex;
bottom: calc(12vh + 4rem);
}
#intel-hud[data-variant='minimal'] .hud-summary-wrap {
max-width: 280px;
}
@@ -374,29 +365,6 @@
opacity: 0.5;
}
:root[data-scope-mask='off']
#intel-hud[data-variant='minimal']
.hud-bottom-bar {
background: transparent;
}
:root[data-scope-mask='off']
#intel-hud[data-variant='minimal']
.hud-bottom-bar {
width: fit-content;
max-width: calc(100vw - 120px);
gap: 24px;
}
:root[data-scope-mask='off']
#intel-hud[data-variant='minimal']
.hud-bottom-bar
.hud-readout {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
:root[data-scope-mask='off']:not([data-ui-theme='cyber'])
#title-bar
h1::before {