Merge branch 'main' into feat/hebrew-i18n

This commit is contained in:
German Khokhlov authored and GitHub committed 2026-11-10 21:24:21 +04:00
commit 2919e90c63
85 files changed
+3208 -126

No files matched your search

+75 -14
View File
@@ -212,6 +212,10 @@ const _paintQueue = [];
const _paintItemPool = [];
const _paintRectPool = [];
const _paintRectByKey = new Map();
/** @type {Map<string, Map<string, object>>} This frame's paint items by source and entry id. */
const _queuedItemBySource = new Map();
/** Index rows that stop painting are dropped this many frames apart. */
const QUEUED_ITEM_PRUNE_FRAMES = 600;
const _hitRects = [];
const _paintedBySource = Object.create(null);
const _paintedSourceKeys = [];
@@ -1042,6 +1046,32 @@ export function getOverlayPaintRect(sourceId, entryId) {
return rect && rect.key === key && rect.stamp === _frameStamp ? rect : null;
}
/**
* Opacity an entry paints with this frame, or 0 when it is not queued or the
* yield-collision pass hid it. Custom lanes may call it before later lanes
* publish paint rects. Leaves out the Cyber sonar fade.
* @param {string} sourceId
* @param {string} entryId
* @returns {number}
*/
export function getQueuedOverlayEntryAlpha(sourceId, entryId) {
if (_destroyed) return 0;
const item = _queuedItemBySource.get(sourceId)?.get(entryId);
// Pooled items are reassigned every frame: check the frame and the entry.
if (!item || item.stamp !== _frameStamp) return 0;
// Declutter has already run when custom lanes call this.
if (item.collisionHidden) return 0;
const record = item.record;
if (record.entry.id !== entryId || record.entry.source !== sourceId) return 0;
return (
record.sourceAlpha *
item.temporalAlpha *
record.distanceAlpha *
record.altitudeAlpha *
(_keyhole ? entryKeyholeAlpha(record.entry, item.placement, _keyhole) : 1)
);
}
/**
* Resolve the topmost interactive world-overlay entry at CSS-pixel coords.
* @param {number} x
@@ -2223,7 +2253,34 @@ function addPaintItem(record, placement, temporalAlpha, selected) {
item.zIndex = record.entry.zIndex;
item.key = record.key;
item.collisionHidden = false;
item.stamp = _frameStamp;
_paintQueue[_paintCount++] = item;
// Keyed by source and id, not `record.key`, so a lookup builds no string.
// Also holds cards fading out after they left `_records`.
let bySource = _queuedItemBySource.get(record.entry.source);
if (!bySource) {
bySource = new Map();
_queuedItemBySource.set(record.entry.source, bySource);
}
bySource.set(record.entry.id, item);
}
/** Drop index rows for entries that stopped painting (bounded, not per frame). */
function pruneQueuedItemIndex() {
for (const [sourceId, bySource] of _queuedItemBySource) {
for (const [entryId, item] of bySource) {
// A pooled item that now paints another entry keeps a fresh stamp.
const entry = item.record.entry;
if (
item.stamp !== _frameStamp ||
entry.id !== entryId ||
entry.source !== sourceId
) {
bySource.delete(entryId);
}
}
if (bySource.size === 0) _queuedItemBySource.delete(sourceId);
}
}
function sourceCanPaint(sourceId) {
@@ -2295,6 +2352,7 @@ function solveDomains(timestamp) {
solveRevision += domain.arbiter.solveRevision;
}
_paintQueue.length = _paintCount;
if (_frameStamp % QUEUED_ITEM_PRUNE_FRAMES === 0) pruneQueuedItemIndex();
_solveDirty = false;
_diagnostics.solveMs = solveMs;
_diagnostics.selectedCount = selectedCount;
@@ -2423,24 +2481,26 @@ function paintCustomLane(lane) {
}
}
/** Keyhole edge fade of one placed entry (1 for entries that do not fade). */
function entryKeyholeAlpha(entry, placement, keyhole) {
if (entry.edgeFade !== 'keyhole') return 1;
const keyholeX = placement.centerX - keyhole.centerX;
const keyholeY = placement.centerY - keyhole.centerY;
return keyholeX * keyholeX + keyholeY * keyholeY <=
keyhole.radius * keyhole.radius
? 1
: keyholeLabelAlphaFromGeometry(
placement.centerX,
placement.centerY,
keyhole,
);
}
function paintEntryItem(item, keyhole, sonar) {
if (item.collisionHidden) return;
const { record, placement } = item;
const entry = record.entry;
let keyholeAlpha = 1;
if (entry.edgeFade === 'keyhole') {
const keyholeX = placement.centerX - keyhole.centerX;
const keyholeY = placement.centerY - keyhole.centerY;
keyholeAlpha =
keyholeX * keyholeX + keyholeY * keyholeY <=
keyhole.radius * keyhole.radius
? 1
: keyholeLabelAlphaFromGeometry(
placement.centerX,
placement.centerY,
keyhole,
);
}
const keyholeAlpha = entryKeyholeAlpha(entry, placement, keyhole);
// All five channels are normalized at their source. Keep the multiply on
// the hot paint path so its intermediate doubles remain unboxed; the pure
// `combinedOverlayAlpha` export still specifies/tests the same binding.
@@ -2829,6 +2889,7 @@ export function destroyWorldOverlay() {
_accessibleActivatorByKey.clear();
_accessibilitySignature = '';
_paintRectByKey.clear();
_queuedItemBySource.clear();
_paintCount = 0;
_paintRectCount = 0;
_hitRectCount = 0;
+120
View File
@@ -33,6 +33,7 @@ import {
activateWorldOverlayAction,
initWorldOverlay,
subscribeWorldOverlayActions,
getQueuedOverlayEntryAlpha,
isOverlayPointVisible,
normalizeOverlayEntry,
paintLaneForOverlayEntry,
@@ -1222,6 +1223,79 @@ test('custom detection lane receives the shared host frame and paints below ordi
env.cleanup();
});
test('a detection lane sees which cards this frame will paint before they publish paint rects', () => {
// The detection lane paints before the cards publish this frame's paint
// rects, so it reads the queued alpha instead.
const env = installMockEnvironment({ width: 400, height: 300, dpr: 1 });
initWorldOverlay(env.viewer);
const seen = [];
registerWorldOverlayPaintLane('detection', () => {
seen.push({
shown: getQueuedOverlayEntryAlpha('cards', 'SHOWN'),
hidden: getQueuedOverlayEntryAlpha('hidden-cards', 'HIDDEN'),
unknown: getQueuedOverlayEntryAlpha('cards', 'NOT-PUBLISHED'),
rect: getOverlayPaintRect('cards', 'SHOWN'),
});
}, { id: 'detection', active: true, target: 'detection' });
setOverlayEntries('cards', [selectedEntry('SHOWN', {
position: positionAtScreen(200, 150),
})]);
setOverlayEntries('hidden-cards', [selectedEntry('HIDDEN', {
position: positionAtScreen(100, 100),
})]);
setOverlaySourceVisible('hidden-cards', false);
env.postRender.raise();
assert.deepEqual(seen[0], { shown: 1, hidden: 0, unknown: 0, rect: null },
'the card is queued while its paint rect is still unpublished');
assert.ok(getOverlayPaintRect('cards', 'SHOWN'), 'the card did paint after the lane');
assert.equal(getOverlayPaintRect('hidden-cards', 'HIDDEN'), null);
clearOverlaySource('cards');
env.postRender.raise();
assert.equal(seen[1].shown, 0, 'a card cleared since the last frame is no longer queued');
destroyWorldOverlay();
assert.equal(getQueuedOverlayEntryAlpha('cards', 'SHOWN'), 0);
env.cleanup();
});
test('a detection lane reads zero for a yielding entry the declutter pass hid this frame', () => {
// Declutter runs before custom lanes, so a yielding entry it hid must not
// report its full queued alpha to them.
const env = installMockEnvironment({ width: 400, height: 300, dpr: 1 });
initWorldOverlay(env.viewer);
const seen = [];
registerWorldOverlayPaintLane('detection', () => {
seen.push({
winner: getQueuedOverlayEntryAlpha('cards', 'ABOVE'),
yielder: getQueuedOverlayEntryAlpha('labels', 'YIELDER'),
});
}, { id: 'detection', active: true, target: 'detection' });
// Two cards take both vertical slots, so a vertical-only yielder has none.
const at = positionAtScreen(200, 150);
const card = (id, placement) => selectedEntry(id, {
position: at,
placement,
verticalOnly: true,
collisionGroup: 'shared-card',
});
setOverlayEntries('cards', [card('ABOVE', 'above'), card('BELOW', 'below')]);
setOverlayEntries('labels', [selectedEntry('YIELDER', {
position: at,
verticalOnly: true,
collisionGroup: 'yielding-label',
yieldToCollisionGroup: 'shared-card',
})]);
env.postRender.raise();
assert.ok(getOverlayPaintRect('cards', 'ABOVE'), 'the winner painted');
assert.equal(getOverlayPaintRect('labels', 'YIELDER'), null, 'the yielder was hidden');
assert.ok(seen[0].winner > 0, 'the winner reads its queued alpha');
assert.equal(seen[0].yielder, 0, 'the yielder hidden this frame reads zero');
destroyWorldOverlay();
env.cleanup();
});
test('custom-lane painter state is bracketed by host save/restore', () => {
// A painter that leaks a clip or alpha must not corrupt later frames: the
// host wraps every custom-lane painter in save/try/finally-restore. Pin the
@@ -2489,3 +2563,49 @@ test('promoting an ambient entry to selected paints it once with no fading dupli
assert.equal(paint(), 1, 'deselect restores the ambient card without re-entry cooldown');
} finally { env.cleanup(); }
});
test('the queued-card index stays bounded while card identities churn', () => {
// The index is internal: capture it through Map.prototype.set, like the
// projection-record test.
const originalSet = Map.prototype.set;
let queuedIndex = null;
Map.prototype.set = function captureQueuedIndex(key, value) {
if (typeof key === 'string' && key.startsWith('churn-card-')
&& value && typeof value === 'object' && 'temporalAlpha' in value
&& 'placement' in value && 'stamp' in value) {
queuedIndex = this;
}
return originalSet.call(this, key, value);
};
const env = installMockEnvironment({ width: 400, height: 300, dpr: 1 });
try {
initWorldOverlay(env.viewer);
const FRAMES = 1300;
for (let frame = 0; frame < FRAMES; frame++) {
// One card whose id changes every frame reuses one pooled paint item,
// like a vessel cohort republished as the camera moves.
setOverlayEntries('cards', [selectedEntry(`churn-card-${frame}`, {
position: positionAtScreen(200, 150),
})]);
env.advanceTime(16);
env.postRender.raise();
assert.equal(getQueuedOverlayEntryAlpha('cards', `churn-card-${frame}`), 1,
'the live card is queued in the frame just painted');
if (frame > 0) {
assert.equal(getQueuedOverlayEntryAlpha('cards', `churn-card-${frame - 1}`), 0,
'a stale row never reports the previous card as queued');
}
}
assert.ok(queuedIndex, 'the queued-card index was never populated');
// Rows are pruned every 600 painted frames, so with one new id per frame
// the index holds at most one prune window of ids plus the live card.
const bound = 600 + 1;
assert.ok(
queuedIndex.size <= bound,
`queued-card index holds ${queuedIndex.size} rows after ${FRAMES} identities (bound ${bound})`,
);
} finally {
Map.prototype.set = originalSet;
env.cleanup();
}
});
@@ -1217,6 +1217,9 @@ function buildPhase6DetectionWorkload(count) {
: `FL${String(180 + (index % 220)).padStart(3, '0')}`,
type: ['AIR', 'SAT', 'SEA', 'VEH'][index % 4],
tier: index % 17 === 0 ? 'military' : undefined,
// Vessels name their ambient card, so each callout asks the host for it.
cardSourceId: index % 4 === 2 ? 'ais-live-vessels' : undefined,
cardEntryId: index % 4 === 2 ? `vessel:${index}` : undefined,
});
}
return {