Refine HUD backings and default scope feather to 100 percent

This commit is contained in:
Bilawal Sidhu committed 2026-10-25 23:37:43 -05:00
1 parent d8c264b501
commit cfbdf22121
8 files changed
+144 -68

No files matched your search

@@ -1,4 +1,5 @@
- Add a Scope checkbox and extend feathering to 200%, retaining scope settings
across HUD layouts, share links and scenes. Give Tactical, Operator and Minimal
text subtle black backings, including vertical metadata, while keeping map
attribution clear (bilawalsidhu, #1397).
across HUD layouts, share links and scenes. First run opens with Scope ON at
100% feather; explicit settings and historical links retain their values. Give
Tactical, Operator and Minimal text darker black backings without halos or
overlapping lines, including vertical metadata, while keeping map attribution clear (bilawalsidhu, #1397).
+13 -10
View File
@@ -194,11 +194,12 @@ are independent of Tactical, Operator, Minimal and Cyber layout changes. Share
links (`sc`/`scf`) and scene visual state restore both choices, including feather
above 100%. The supported feather range is 0–200% of the keyhole radius: 0% is a
hard edge and 200% extends the gradient to the center while keeping its midpoint
on the same keyhole edge. First run remains 11%; links predating `scf` retain
their historical 35% parse fallback.
on the same keyhole edge. First run opens with Scope ON and feather 100%; links
predating `scf` retain their historical 35% parse fallback.
Tactical, Operator and Minimal give every telemetry readout a small, softly
edged black backing, including vertical metadata and multiline AIS. Glyphs keep
Tactical, Operator and Minimal give every telemetry readout a small 70%-black
backing with rounded corners, including vertical metadata and multiline AIS.
These darker backings have no shadow halo or overlap between adjacent lines. Glyphs keep
the active style hue with stronger alpha, while the red REC dot retains its own
color. Readout backing sits outside each glyph's opacity; Operator's corner and
bar frames stay transparent rather than stacking another dark panel. Summary
@@ -208,7 +209,7 @@ lower coordinates clear map attribution. Cyber retains its existing skin.
`node scripts/qa-scope-hud.mjs` exercises native checkbox click and Space,
feather range and keyboard boundaries, all four layout transitions, share/scene
restore, text-backing coverage and geometry, multiline AIS, summary clipping and
restore, text-backing coverage and non-overlapping geometry, multiline AIS, summary clipping and
narrow viewport containment. Optional `QA_SCREENSHOT_DIR` saves mode screenshots.
## Cyber HUD — September 30, 2026
@@ -5519,10 +5520,11 @@ the `_detectionUserOverridden` / explicit-intent flags that would suppress a
separate landed behaviour. Pinned in `src/reasonableDefaults.test.mjs` (feather,
detection, OUTSIDE opacity) and `src/data/layerState.test.mjs` (3D).
**The 2026-08-24 defaults** (superseding the interim 08-23 values of 8%/3%):
**Current defaults** (scope feather revised to 100% on 2026-10-25;
detection and outside opacity retain the 2026-08-24 defaults):
the first-run look is Detection DENSE
`75%`, ELASTIC allocation, Fade `7%`, OUTSIDE opacity `1%`, scope feather `11%`,
and 3D fleet mode PROXIMITY, with `AIRCRAFT_BRACKET_FLOOR_ANCHOR` at `0.01` so
`75%`, ELASTIC allocation, Fade `7%`, OUTSIDE opacity `1%`, Scope ON with feather
`100%`, and 3D fleet mode PROXIMITY, with `AIRCRAFT_BRACKET_FLOOR_ANCHOR` at `0.01` so
brackets keep their approved brightness exactly at the new OUTSIDE default. Two
terms that must never be conflated: scope FEATHER softens the black scope-mask
edge; detection FADE is the label/card fading band around the keyhole. The
@@ -5592,8 +5594,9 @@ easier to meet (detection is now on more often), but does not create it.
calibrated so `AIRCRAFT_BRACKET_ALPHA_FLOOR` (0.35) lands exactly at the
default, and the mapping follows bracket brightness rather than slider
position. The `ko` PARSE fallback stays at `5`.
- **Scope feather: `11%`** — a soft scope-mask edge (moved `0% → 8% → 11%`; `0%` hard crop for one day,
`35%` before that). `SCOPE_FEATHER_RATIO_DEFAULT` in `src/scopeMask.js`,
- **Scope: `ON`, feather: `100%`** — a broad, soft scope-mask edge (revised on
2026-10-25 from the prior `11%`; earlier defaults were `0%`, `8%` and `35%`).
`SCOPE_FEATHER_RATIO_DEFAULT` in `src/scopeMask.js`,
mirrored by `#scope-feather-slider`'s markup value AND readout and
`_scopeFeatherPct` in `sharelink.js`. The slider spans
0–200, and an explicit `0` is still the hard-crop path — pinned, so moving the
+71 -13
View File
@@ -3,7 +3,8 @@
* Scope and HUD browser acceptance. Requires the development server.
* Run: node scripts/qa-scope-hud.mjs
* Optional: QA_BASE_URL, QA_SCREENSHOT_DIR (screenshots stay outside the repo).
* Proves native checkbox click/Space, feather 0/11/100/200, layout independence,
* 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.
*/
import fs from 'node:fs';
@@ -55,12 +56,7 @@ const readScope = () =>
};
});
try {
await page.setViewport({ width: 1440, height: 900 });
const url = new URL(process.env.QA_BASE_URL || 'http://127.0.0.1:4173');
url.searchParams.set('welcome', '0');
url.hash =
'v=2&lat=30.2672&lon=-97.7431&alt=2500&heading=0&pitch=-45&roll=0&map=esri&sc=1&scf=11&l=';
async function openApplication(url) {
await page.goto(url.href, { waitUntil: 'domcontentloaded' });
await page.waitForFunction(
() => window.__godsEyeView?.styleManager?._applicationShortcuts,
@@ -76,6 +72,36 @@ try {
},
{ timeout: 60_000 },
);
}
try {
await page.setViewport({ width: 1440, height: 900 });
const url = new URL(process.env.QA_BASE_URL || 'http://127.0.0.1:4173');
url.searchParams.set('welcome', '0');
// This browser has a fresh profile. Start without authored state so the
// first assertion exercises the defaults rather than a share-link restore.
url.hash = '';
await openApplication(url);
const fresh = await readScope();
check(
'bare-URL fresh boot opens Scope ON with 100% feather',
fresh.enabled &&
fresh.checked &&
!fresh.disabled &&
fresh.feather === 1 &&
fresh.value === 100 &&
fresh.readout === '100%' &&
fresh.cornerAlpha > 0,
fresh,
);
// Use a known city view for canvas and readability checks below.
url.hash =
'v=2&lat=30.2672&lon=-97.7431&alt=2500&heading=0&pitch=-45&roll=0&map=esri&sc=1&scf=100&l=';
// Changing only the hash does not rerun startup restoration. Leave the app
// document first so this fixture is applied by a new application boot.
await page.goto('about:blank');
await openApplication(url);
await page.evaluate(() => {
const manager = window.__godsEyeView.styleManager;
manager.setPanelCollapsed('pp-toggles', false, {
@@ -87,12 +113,13 @@ try {
});
const initial = await readScope();
check(
'default Scope checkbox and 11% feather agree with the mask',
'Austin fixture restores Scope ON and 100% feather',
initial.enabled &&
initial.checked &&
!initial.disabled &&
initial.feather === 0.11 &&
initial.value === 11 &&
initial.feather === 1 &&
initial.value === 100 &&
initial.readout === '100%' &&
initial.cornerAlpha > 0,
initial,
);
@@ -112,7 +139,7 @@ try {
!off.enabled &&
!off.checked &&
off.disabled &&
off.feather === 0.11 &&
off.feather === 1 &&
off.cornerAlpha === 0,
off,
);
@@ -124,7 +151,7 @@ try {
on.enabled &&
on.checked &&
!on.disabled &&
on.feather === 0.11 &&
on.feather === 1 &&
on.cornerAlpha > 0,
on,
);
@@ -243,7 +270,8 @@ try {
const color = backing && getComputedStyle(backing).backgroundColor;
if (
!backing ||
!color?.includes('0.34') ||
color !== 'rgba(0, 0, 0, 0.7)' ||
getComputedStyle(backing).boxShadow !== 'none' ||
backing.parentElement.closest('.hud-readout')
)
missing.push(node.textContent.trim());
@@ -252,6 +280,30 @@ try {
(el) => el.offsetWidth > 0 && el.offsetHeight > 0,
);
const empty = wrapped.filter((el) => !el.innerText.trim()).length;
const overlaps = [];
for (const parent of new Set(wrapped.map((el) => el.parentElement))) {
const siblings = wrapped.filter((el) => el.parentElement === parent);
for (let i = 1; i < siblings.length; i += 1) {
const previous = siblings[i - 1].getBoundingClientRect();
const current = siblings[i].getBoundingClientRect();
const horizontal =
Math.min(previous.right, current.right) -
Math.max(previous.left, current.left);
const vertical =
Math.min(previous.bottom, current.bottom) -
Math.max(previous.top, current.top);
// Horizontal bars share a vertical band. Only intersecting text boxes
// can stack their backing alpha; allow subpixel layout rounding.
if (horizontal > 0.5 && vertical > 0.5) {
overlaps.push({
parent: parent.className,
previous: siblings[i - 1].innerText,
current: siblings[i].innerText,
vertical,
});
}
}
}
const lineIds = [
'hud-mgrs',
'hud-latlon',
@@ -291,6 +343,7 @@ try {
visible,
missing,
empty,
overlaps,
multiline,
edges,
broadBackings: broadBackings.length,
@@ -304,6 +357,11 @@ try {
coverage.broadBackings === 0,
coverage,
);
check(
`${layout}: adjacent readout backings do not overlap`,
coverage.overlaps.length === 0,
coverage,
);
check(
`${layout}: desktop telemetry remains single line; rotated backing follows the labels`,
coverage.multiline.length === 0 && coverage.edges.every(Boolean),
+29 -16
View File
@@ -11,9 +11,8 @@ import { expandApplicationHtml } from '../build/application-html.js';
// actually decides fresh-boot layer state — including the early return that
// makes each layer's own initializer the operative default.
// 2. Scope feather. Owner: "I like to hide feather" set it to 0% on
// 2026-08-22; the owner revised that to 8% on 2026-08-23 and locked 11%
// on 2026-08-24, a soft
// edge. The hard crop is still one drag away, and that is pinned too.
// 2026-08-22; later revisions were 8%, then 11%, then 100% on
// 2026-10-25. Scope stays ON on a fresh boot. The hard crop remains available.
// 3. Detection ON (Dense @ 75%) for EVERY style, Normal included.
// Owner: "detect should also be on by default."
// 4. Detection OUTSIDE opacity 1% (owner final lock, 2026-08-24; 3% on 08-23, 5% before), with
@@ -38,6 +37,7 @@ import { AIRCRAFT_BRACKET_FLOOR_ANCHOR } from './data/detectionPolicy.js';
import {
SCOPE_FEATHER_RATIO_DEFAULT,
getScopeMaskFeather,
isScopeMaskEnabled,
scopeMaskGeometry,
setScopeMaskFeather,
} from './scopeMask.js';
@@ -75,32 +75,40 @@ function managerForHash(hash) {
}
// ---------------------------------------------------------------------------
// 2. Scope feather — a subtle soft edge on a first run
// 2. Scope ON with 100% feather on a first run
// ---------------------------------------------------------------------------
test('first run opens with a subtle scope feather, at every surface that decides it', () => {
assert.equal(SCOPE_FEATHER_RATIO_DEFAULT, 0.11,
'owner final lock 2026-08-24, superseding the 08-22 hard-crop and 08-23 8% rulings');
assert.equal(getScopeMaskFeather(), 0.11,
test('first run opens with Scope ON and 100% feather at every default surface', () => {
assert.equal(isScopeMaskEnabled(), true, 'the live mask starts enabled');
assert.equal(SCOPE_FEATHER_RATIO_DEFAULT, 1, 'owner preference 2026-10-25');
assert.equal(getScopeMaskFeather(), 1,
'and the live module starts there, not merely documents it');
// The slider and its readout are the same default rendered as markup — a
// fresh boot applies no restore, so a stale value here would show one number
// over a mask drawn at another.
assert.match(indexHtml, /id="scope-feather-slider"[^>]*\svalue="11"/,
'index.html: the feather slider ships at 11');
assert.match(indexHtml, /id="scope-feather-value"[^>]*>11%</,
assert.match(indexHtml, /id="scope-feather-slider"[^>]*\svalue="100"/,
'index.html: the feather slider ships at 100');
assert.match(indexHtml, /id="scope-feather-value"[^>]*>100%</,
'index.html: and its readout agrees with the handle');
assert.match(indexHtml, /<input type="checkbox" id="scope-toggle"[^>]*\schecked\s*\/>/,
'the native Scope checkbox starts checked');
// The link this session generates must describe the mask this session draws,
// for the window before the first _syncShareState.
assert.match(shareSource, /this\._scopeFeatherPct = 11;/,
assert.match(shareSource, /this\._scopeFeatherPct = 100;/,
'sharelink.js: the generator starts from the same value the mask starts at');
const params = managerForHash('')._buildHashParams();
assert.equal(params.get('sc'), '1');
assert.equal(params.get('scf'), '100');
});
test('an explicit feather still wins over the subtle default', () => {
test('an explicit feather still wins over the 100% default', () => {
// A link is authored state. The new default governs a session that said
// nothing; it must never overwrite one that said something.
assert.equal(managerForHash('#lat=10&lon=20&scf=11').parseInitialHash().scopeFeatherPct, 11,
'links carrying the previous default preserve their authored feather');
assert.equal(managerForHash('#lat=10&lon=20&scf=35').parseInitialHash().scopeFeatherPct, 35);
assert.equal(managerForHash('#lat=10&lon=20&scf=64').parseInitialHash().scopeFeatherPct, 64);
assert.equal(managerForHash('#lat=10&lon=20&scf=0').parseInitialHash().scopeFeatherPct, 0,
@@ -113,7 +121,13 @@ test('an explicit feather still wins over the subtle default', () => {
'a pre-scf link restores the author\'s view, not the new default');
});
test('the subtle default did not weaken the feather control, and 0 is still reachable', () => {
test('share links preserve explicit Scope OFF and feather choices over the defaults', () => {
const shared = managerForHash('#lat=10&lon=20&sc=0&scf=11').parseInitialHash();
assert.equal(shared.scopeEnabled, false);
assert.equal(shared.scopeFeatherPct, 11);
});
test('the 100% default retains the expanded feather range and hard edge', () => {
// The cheap way to move a default would be to nerf the control. Prove the
// slider still spans its full range and the geometry is still DERIVED from
// the ratio — a check that would pass vacuously if it only ever saw one value.
@@ -128,8 +142,7 @@ test('the subtle default did not weaken the feather control, and 0 is still reac
assert.ok(Math.abs((geo.outerR - geo.innerR) - keyholeR * ratio) < 1e-9,
`feather ${ratio} must still widen the band to that fraction of the keyhole`);
}
// The new default is a real, narrow band — not the hard crop, and nowhere
// near the retired 35 % halo.
// The first-run value draws a band derived from the same keyhole radius.
setScopeMaskFeather(SCOPE_FEATHER_RATIO_DEFAULT);
const soft = scopeMaskGeometry(1200, 900);
const keyholeR = 900 * 0.5 * KEYHOLE_OUTER_RADIUS;
+5 -8
View File
@@ -49,13 +49,10 @@ const SCOPE_OUTSIDE_COLOR = { r: 5, g: 5, b: 8 };
/**
* Default edge feather as a fraction of the keyhole radius.
*
* 0.11 since 2026-08-24 (owner final lock; 0.08 on 08-23, hard-crop 0 on
* 08-22 — this supersedes both), REVISING the 2026-08-22 ruling that
* set it to zero: a subtle soft edge rather than either the hard crop or the
* retired 35 % halo. The slider spans 0..200; this is
* only where it STARTS. Any feather a
* share link carries, or the operator dials in, overrides this immediately, and
* the hard-crop path at 0 is still reachable from the handle.
* 1.0 since 2026-10-25 (owner preference), superseding the 11% default.
* The slider spans 0..200; this is only where it starts. An explicit feather
* from a share link, scene or the operator overrides the default immediately,
* including the hard-crop path at 0.
*
* Keep in lockstep with `#scope-feather-slider`'s markup value AND readout in
* index.html and `_scopeFeatherPct` in sharelink.js — a fresh boot applies no
@@ -65,7 +62,7 @@ const SCOPE_OUTSIDE_COLOR = { r: 5, g: 5, b: 8 };
* carries `scf=0` explicitly because the generator always writes the field.
* Pinned in reasonableDefaults.test.mjs.
*/
export const SCOPE_FEATHER_RATIO_DEFAULT = 0.11;
export const SCOPE_FEATHER_RATIO_DEFAULT = 1;
/**
* Terminus opacity at/above SCOPE_TERMINUS_FAR_M — slightly translucent so
* faint stars survive in the corners at globe scale.
+3 -4
View File
@@ -113,10 +113,9 @@ export class ShareLinkManager {
this._detectionOutsideOpacityPct = 1;
this._celestialRingEnabled = false;
this._scopeEnabled = true;
// Feather opens on a soft 11% scope-mask edge (owner final lock 2026-08-24,
// superseding the 08-22 hard-crop and 08-23 8% rulings) — mirrors
// SCOPE_FEATHER_RATIO_DEFAULT in scopeMask.js and the slider's markup value.
this._scopeFeatherPct = 11;
// Mirrors the first-run mask and slider default. The historical missing-scf
// parse fallback below remains 35 because it restores an older authored view.
this._scopeFeatherPct = 100;
// null = the altitude-adaptive terminus (the default). A number pins the
// outside-fill opacity as a percent, 94..100. (`sce`, 2026-08-17)
this._scopeTerminusPct = null;
+14 -7
View File
@@ -386,11 +386,13 @@
)
.hud-readout,
:root:not([data-ui-theme='cyber']) .scene-text-holdout {
background: rgba(0, 0, 0, 0.34);
box-shadow: 0 0 6px 3px rgba(0, 0, 0, 0.16);
background: rgba(0, 0, 0, 0.7);
box-shadow: none;
border-radius: 2px;
padding: 1px 4px;
margin: -1px -4px;
padding-block: 0;
padding-inline: 6px;
margin-block: 0;
margin-inline: -6px;
width: fit-content;
max-width: 100%;
box-sizing: content-box;
@@ -416,7 +418,7 @@
[data-variant='tactical']
)
.hud-readout
:not(#hud-rec-dot) {
:where(:not(#hud-rec-dot)) {
color: rgb(from var(--hud-color) r g b / 0.86);
text-shadow:
0 1px 2px rgba(0, 0, 0, 0.8),
@@ -431,7 +433,7 @@
:is(.hud-top-right, .hud-bottom-right)
.hud-readout,
:root:not([data-ui-theme='cyber']) #style-indicator .scene-text-holdout {
margin-left: auto;
margin-inline-start: auto;
}
/* Rotating the backing with its text also covers both vertical annotations. */
@@ -443,7 +445,7 @@
.hud-edge
.hud-readout
> * {
opacity: 0.72;
opacity: 0.78;
}
#intel-hud:is(
@@ -483,6 +485,11 @@
text-overflow: ellipsis;
}
/* The title's inline text box needs a little vertical room around its glyphs. */
:root:not([data-ui-theme='cyber']) #title-bar h1 > .scene-text-holdout {
padding-block: 0.12em;
}
:root:not([data-ui-theme='cyber']) #title-bar .subtitle {
margin-top: 8px;
}
+5 -7
View File
@@ -113,13 +113,11 @@
</label>
<div class="pp-slider-row visible" id="scope-slider-row">
<span class="pp-slider-mini-label" data-i18n>Feather</span>
<!-- Feather starts at an 11% soft scope-mask edge (owner final lock
2026-08-24, superseding the 08-22 hard-crop and 08-23 8% rulings);
mirrors SCOPE_FEATHER_RATIO_DEFAULT in src/scopeMask.js. Feather
softens the black mask edge; label fading is the separate Fade
slider. -->
<input type="range" class="gev-quantitative-slider pp-slider" id="scope-feather-slider" min="0" max="200" value="11" aria-label="Scope edge feather" title="Scope edge feather as a percentage of the keyhole radius" data-i18n-aria-label data-i18n-title />
<span class="pp-slider-value" id="scope-feather-value">11%</span>
<!-- First-run feather is 100% (owner preference 2026-10-25), matching
SCOPE_FEATHER_RATIO_DEFAULT in src/scopeMask.js. Feather softens
the black mask edge; detection fading uses the separate Fade slider. -->
<input type="range" class="gev-quantitative-slider pp-slider" id="scope-feather-slider" min="0" max="200" value="100" aria-label="Scope edge feather" title="Scope edge feather as a percentage of the keyhole radius" data-i18n-aria-label data-i18n-title />
<span class="pp-slider-value" id="scope-feather-value">100%</span>
</div>
</div>
<div class="pp-toggle-group">