feat: add Nepal flood incident scene

This commit is contained in:
manjunath22466 committed 2026-09-15 19:48:11 +05:30
1 parent 76c20be277
commit 60768ccc0b
48 files changed
+15431 -161

No files matched your search

+65 -4
View File
@@ -103,6 +103,7 @@ export const WORLD_OVERLAY_OCCLUDER_SELECTORS = Object.freeze([
'#space-mission-panel',
'#space-mission-panel-host',
'#military-awareness-panel',
'#bhote-koshi-event-panel',
// Cockpit: solid backdrop-filled windows only (both bounded to
// `min(340px, 28vw)` wide and `min(42vh, 410px)` tall, both `hidden` until
// toggled). Every other cockpit selector was removed — see the block comment.
@@ -460,7 +461,20 @@ export function normalizeOverlayEntry(sourceId, entry) {
edgeFade: entry.edgeFade === false || entry.edgeFade === 'none' ? 'none' : 'keyhole',
horizonCull: entry.horizonCull !== false,
terrainOcclusion: entry.terrainOcclusion === true,
sourceAlpha: clamp01(entry.sourceAlpha ?? entry.alpha, 1),
sourceAlpha: typeof (entry.sourceAlpha ?? entry.alpha) === 'function'
? (entry.sourceAlpha ?? entry.alpha)
: clamp01(entry.sourceAlpha ?? entry.alpha, 1),
presentationScale: typeof entry.presentationScale === 'function'
? entry.presentationScale
: Math.max(0, Number(entry.presentationScale) || 1),
leaderProgress: typeof entry.leaderProgress === 'function'
? entry.leaderProgress
: clamp01(entry.leaderProgress, 1),
contentAlpha: typeof entry.contentAlpha === 'function'
? entry.contentAlpha
: clamp01(entry.contentAlpha, 1),
anchorDot: entry.anchorDot === true,
leaderStyle: entry.leaderStyle === 'elbow' ? 'elbow' : 'straight',
temporalAlpha: clamp01(entry.temporalAlpha, 1),
gapPx: Number.isFinite(Number(entry.gapPx)) ? Math.max(0, Number(entry.gapPx)) : 12,
leaderOffsetPx: Number.isFinite(Number(entry.leaderOffsetPx))
@@ -1513,6 +1527,8 @@ function getProjectionRecord(entry) {
candidate: null,
distanceAlpha: 1,
paintScale: 1,
leaderProgress: 1,
contentAlpha: 1,
altitudeAlpha: 1,
sourceAlpha: 1,
protectedPlacement: null,
@@ -1575,6 +1591,30 @@ function resetFrameDomains() {
}
}
// Evaluate optional scene presentation outside the shared projection hot path.
// Passing records (rather than intermediate doubles) also avoids boxing the
// ordinary moving-source workload at a non-inlined call boundary.
function applyPresentation(entry, source, record) {
try {
const scale = Number(typeof entry.presentationScale === 'function'
? entry.presentationScale() : entry.presentationScale);
record.paintScale *= Number.isFinite(scale) ? Math.max(0, scale) : 1;
const alpha = Number(typeof entry.sourceAlpha === 'function'
? entry.sourceAlpha() : entry.sourceAlpha);
record.sourceAlpha = source.options.alpha
* (Number.isFinite(alpha) ? Math.max(0, Math.min(1, alpha)) : 1);
const leader = Number(typeof entry.leaderProgress === 'function'
? entry.leaderProgress() : entry.leaderProgress);
record.leaderProgress = Number.isFinite(leader) ? Math.max(0, Math.min(1, leader)) : 1;
const content = Number(typeof entry.contentAlpha === 'function'
? entry.contentAlpha() : entry.contentAlpha);
record.contentAlpha = Number.isFinite(content) ? Math.max(0, Math.min(1, content)) : 1;
return true;
} catch {
return false;
}
}
function snapshotAndProject(entry, source, viewProjection, keyhole) {
const record = getProjectionRecord(entry);
let position;
@@ -1651,7 +1691,14 @@ function snapshotAndProject(entry, source, viewProjection, keyhole) {
record.altitudeAlpha = entry.altitudeFadeEnd === Number.POSITIVE_INFINITY
? (Number.isFinite(cameraAltitude) && cameraAltitude < entry.minAltitude ? 0 : 1)
: altitudeFade(cameraAltitude, record.altitudeOptions);
record.sourceAlpha = source.options.alpha * entry.sourceAlpha;
record.leaderProgress = 1;
record.contentAlpha = 1;
if (entry.presentationScale !== 1 || entry.leaderProgress !== 1
|| entry.contentAlpha !== 1 || typeof entry.sourceAlpha === 'function') {
if (!applyPresentation(entry, source, record)) return null;
} else {
record.sourceAlpha = source.options.alpha * entry.sourceAlpha;
}
if (record.distanceAlpha <= 0 || record.paintScale <= 0
|| record.altitudeAlpha <= 0 || record.sourceAlpha <= 0) return null;
@@ -2075,7 +2122,14 @@ function paintEntryItem(item, keyhole) {
* record.distanceAlpha * record.altitudeAlpha * keyholeAlpha;
if (finalAlpha <= 0.001) return;
if (record.paintScale === 1) {
paintOverlayEntry(_ctx, entry, placement, finalAlpha);
paintOverlayEntry(
_ctx,
entry,
placement,
finalAlpha,
record.leaderProgress,
record.contentAlpha,
);
} else {
const scaled = localizeScaledPlacement(
placement,
@@ -2085,7 +2139,14 @@ function paintEntryItem(item, keyhole) {
_ctx.save();
_ctx.translate(placement.rect.x, placement.rect.y);
_ctx.scale(record.paintScale, record.paintScale);
paintOverlayEntry(_ctx, entry, scaled, finalAlpha);
paintOverlayEntry(
_ctx,
entry,
scaled,
finalAlpha,
record.leaderProgress,
record.contentAlpha,
);
_ctx.restore();
}
publishPaintRect(item);
+79
View File
@@ -1821,6 +1821,85 @@ test('CCTV host binding preserves the smoothstep scale and 0.45/0.35 fade curve'
}
});
test('scene presentation samples once per frame and recovers after a callback failure', () => {
const env = installMockEnvironment({ width: 500, height: 400, dpr: 1 });
const alphas = [];
Object.defineProperty(env.ctx, 'globalAlpha', {
configurable: true,
get() { return alphas.at(-1) ?? 1; },
set(value) { alphas.push(value); },
});
initWorldOverlay(env.viewer);
const entry = createCctvThumbnailOverlayEntry({
id: 'scene-presentation', position: position(), title: 'SCENE',
frameSlot: { frame: { ready: true }, stamp: 1 }, active: true,
});
Object.assign(entry, {
horizonCull: false, maxDistance: Number.POSITIVE_INFINITY, edgeFade: 'none',
altitudeScale: null, altitudeFadeEnd: Number.POSITIVE_INFINITY,
});
let scale = 0.5;
let alpha = 0.8;
let leader = 0.5;
let content = 0.25;
let fail = false;
const sampled = { scale: 0, alpha: 0, leader: 0, content: 0 };
entry.presentationScale = () => { sampled.scale++; return scale; };
entry.sourceAlpha = () => {
sampled.alpha++;
if (fail) throw new Error('presentation unavailable');
return alpha;
};
entry.leaderProgress = () => { sampled.leader++; return leader; };
entry.contentAlpha = () => { sampled.content++; return content; };
const publish = () => setOverlayEntries('scene-presentation', [entry], {
cohortLimit: 1, collisionCapacity: 0,
});
const frame = () => {
env.ctx.calls.length = 0;
alphas.length = 0;
env.postRender.raise();
};
try {
publish();
frame();
assert.deepEqual(sampled, { scale: 1, alpha: 1, leader: 1, content: 1 });
assert.equal(env.ctx.calls.find(([name]) => name === 'scale')?.[1], 0.5);
assert.ok(Math.abs(alphas.at(-1) - 0.2) < 1e-12);
assert.ok(env.ctx.calls.some(([name]) => name === 'drawImage'));
fail = true;
assert.doesNotThrow(frame);
assert.equal(env.ctx.calls.some(([name]) => name === 'drawImage'), false);
assert.deepEqual(sampled, { scale: 2, alpha: 2, leader: 1, content: 1 });
fail = false;
scale = Number.NaN;
alpha = 2;
leader = Number.NaN;
content = 2;
frame();
assert.equal(env.ctx.calls.some(([name]) => name === 'scale'), false);
assert.equal(alphas.at(-1), 1);
assert.ok(env.ctx.calls.some(([name]) => name === 'drawImage'));
scale = 0;
frame();
assert.equal(env.ctx.calls.some(([name]) => name === 'drawImage'), false);
// Reusing the record for an ordinary card must not retain animated values.
Object.assign(entry, {
presentationScale: 1, sourceAlpha: 1, leaderProgress: 1, contentAlpha: 1,
});
publish();
frame();
assert.equal(alphas.at(-1), 1);
assert.ok(env.ctx.calls.some(([name]) => name === 'drawImage'));
} finally {
env.cleanup();
}
});
test('CCTV leader remains one CSS pixel at the 0.35 altitude scale point', () => {
const env = installMockEnvironment({ width: 500, height: 400, dpr: 1 });
env.viewer.camera.positionCartographic.height = 9500;
+134 -27
View File
@@ -311,7 +311,10 @@ export function measureOverlayEntry(ctx, entry, out = {}) {
out.padY = Math.max(0, Number(entry?.thumbnailPadTop) || 0);
out.padBottom = Math.max(0, Number(entry?.thumbnailPadBottom) || 0);
out.titleGap = Math.max(0, Number(entry?.thumbnailTitleGap) || 0);
out.titleH = Math.max(0, Number(entry?.thumbnailTitleHeight) || out.titleH);
const thumbnailTitleHeight = Number(entry?.thumbnailTitleHeight);
out.titleH = Number.isFinite(thumbnailTitleHeight)
? Math.max(0, thumbnailTitleHeight)
: out.titleH;
out.thumbW = Math.max(1, Number(entry?.thumbnailWidth) || 96);
out.thumbH = Math.max(1, Number(entry?.thumbnailHeight) || 54);
out.w = out.thumbW + out.padX * 2;
@@ -444,28 +447,82 @@ export function placementVariants({
return out;
}
function drawLeader(ctx, placement, accent) {
function drawLeader(
ctx,
placement,
accent,
progress = 1,
width = 1,
style = 'straight',
) {
const reveal = Math.max(0, Math.min(1, Number(progress) || 0));
if (reveal <= 0) return;
ctx.strokeStyle = accent || WORLD_OVERLAY_STYLE.leader;
// Leaders are screen-space strokes. Card content may be painted inside an
// altitude/distance scale transform, so counter-scale the canvas width to
// retain the shipped one-CSS-pixel leader at every card size.
ctx.lineWidth = 1 / (placement.paintScale || 1);
ctx.beginPath();
if (placement.leaderOffset === 0) {
ctx.moveTo(placement.leadFromX, placement.leadFromY);
} else if (placement.corner === 'above' || placement.corner === 'below') {
ctx.moveTo(placement.leadFromX, placement.leadFromY + placement.leaderOffset);
} else {
ctx.moveTo(placement.leadFromX + placement.leaderOffset, placement.leadFromY);
ctx.lineWidth = width / (placement.paintScale || 1);
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
let startX = placement.leadFromX;
let startY = placement.leadFromY;
if (placement.leaderOffset !== 0) {
if (placement.corner === 'above' || placement.corner === 'below') {
startY += placement.leaderOffset;
} else {
startX += placement.leaderOffset;
}
}
ctx.lineTo(placement.leadToX, placement.leadToY);
ctx.beginPath();
ctx.moveTo(startX, startY);
if (
style === 'elbow'
&& (placement.corner === 'left' || placement.corner === 'right')
) {
const elbowY = placement.rect.y + Math.min(
Math.max(12, placement.rect.h * 0.12),
26,
);
const verticalLength = Math.abs(elbowY - startY);
const horizontalLength = Math.abs(placement.leadToX - startX);
const totalLength = verticalLength + horizontalLength;
const visibleLength = totalLength * reveal;
if (visibleLength <= verticalLength && verticalLength > 0) {
const direction = Math.sign(elbowY - startY);
ctx.lineTo(startX, startY + visibleLength * direction);
} else {
ctx.lineTo(startX, elbowY);
const remaining = Math.max(0, visibleLength - verticalLength);
const direction = Math.sign(placement.leadToX - startX);
ctx.lineTo(startX + Math.min(horizontalLength, remaining) * direction, elbowY);
}
ctx.stroke();
return;
}
ctx.lineTo(
startX + (placement.leadToX - startX) * reveal,
startY + (placement.leadToY - startY) * reveal,
);
ctx.stroke();
}
function drawCardChrome(ctx, entry, placement, selected = false) {
function drawAnchorDot(ctx, entry, placement) {
if (!entry.anchorDot) return;
const paintScale = placement.paintScale || 1;
const radius = WORLD_OVERLAY_STYLE.anchorDotRadius / paintScale;
ctx.beginPath();
ctx.arc(placement.anchorX, placement.anchorY, radius, 0, Math.PI * 2);
ctx.fillStyle = entry.accent || WORLD_OVERLAY_STYLE.accent;
ctx.fill();
ctx.strokeStyle = WORLD_OVERLAY_STYLE.anchorDotStroke;
ctx.lineWidth = WORLD_OVERLAY_STYLE.anchorDotStrokeWidth / paintScale;
ctx.stroke();
}
function drawCardChrome(ctx, entry, placement, selected = false, drawLeaderLine = true) {
const { x, y, w, h } = placement.rect;
const accent = entry.accent || WORLD_OVERLAY_STYLE.accent;
drawLeader(ctx, placement, accent);
if (drawLeaderLine) drawLeader(ctx, placement, accent, 1, 1, entry.leaderStyle);
ctx.beginPath();
roundedRectPath(ctx, x, y, w, h);
ctx.fillStyle = selected ? WORLD_OVERLAY_STYLE.selectedBackground : WORLD_OVERLAY_STYLE.background;
@@ -667,18 +724,46 @@ export function paintTrack(ctx, entry, placement, alpha = 1) {
return placement.rect;
}
/** Paint a standard detail card. */
export function paintCard(ctx, entry, placement, alpha = 1) {
/** Paint a standard detail card, with an opt-in staged anchor/leader reveal. */
export function paintCard(
ctx,
entry,
placement,
alpha = 1,
leaderProgress = 1,
contentAlpha = 1,
) {
ctx.save();
ctx.globalAlpha = alpha;
drawCardChrome(ctx, entry, placement, false);
if (entry.anchorDot) {
drawLeader(
ctx,
placement,
entry.accent || WORLD_OVERLAY_STYLE.accent,
leaderProgress,
WORLD_OVERLAY_STYLE.leaderWidth,
entry.leaderStyle,
);
drawAnchorDot(ctx, entry, placement);
ctx.globalAlpha = alpha * Math.max(0, Math.min(1, Number(contentAlpha) || 0));
drawCardChrome(ctx, entry, placement, false, false);
} else {
drawCardChrome(ctx, entry, placement, false);
}
drawCardText(ctx, entry, placement, false);
ctx.restore();
return placement.rect;
}
/** Paint a thumbnail card. The image slot remains source-owned. */
export function paintThumbnail(ctx, entry, placement, alpha = 1) {
export function paintThumbnail(
ctx,
entry,
placement,
alpha = 1,
leaderProgress = 1,
contentAlpha = 1,
) {
const layout = entry._overlayLayout || {};
const { x, y, w, h } = placement.rect;
const padX = Number.isFinite(layout.padX) ? layout.padX : 4;
@@ -690,7 +775,16 @@ export function paintThumbnail(ctx, entry, placement, alpha = 1) {
ctx.save();
ctx.globalAlpha = alpha;
drawLeader(ctx, placement, entry.thumbnailLeaderColor || WORLD_OVERLAY_STYLE.leader);
drawLeader(
ctx,
placement,
entry.thumbnailLeaderColor || WORLD_OVERLAY_STYLE.leader,
leaderProgress,
entry.anchorDot ? WORLD_OVERLAY_STYLE.leaderWidth : 1,
entry.leaderStyle,
);
drawAnchorDot(ctx, entry, placement);
ctx.globalAlpha = alpha * Math.max(0, Math.min(1, Number(contentAlpha) || 0));
ctx.beginPath();
roundedRectPath(ctx, x, y, w, h, Number(entry.thumbnailRadius) || 4);
ctx.fillStyle = entry.thumbnailBackground || WORLD_OVERLAY_STYLE.background;
@@ -722,11 +816,13 @@ export function paintThumbnail(ctx, entry, placement, alpha = 1) {
ctx.textBaseline = 'alphabetic';
ctx.fillStyle = entry.thumbnailTitleColor || WORLD_OVERLAY_STYLE.title;
ctx.font = entry.thumbnailTitleFont || WORLD_OVERLAY_STYLE.fontTitle;
ctx.fillText(
titleChars > 0 ? title.slice(0, titleChars) : title,
imageX,
imageY + thumbH + titleH - 3,
);
if (title && titleH > 0) {
ctx.fillText(
titleChars > 0 ? title.slice(0, titleChars) : title,
imageX,
imageY + thumbH + titleH - 3,
);
}
ctx.restore();
return placement.rect;
}
@@ -749,7 +845,7 @@ export function paintTracked(ctx, entry, placement, alpha = 1) {
const accent = entry.accent || WORLD_OVERLAY_STYLE.accent;
ctx.save();
ctx.globalAlpha = alpha;
drawLeader(ctx, placement, accent);
drawLeader(ctx, placement, accent, 1, 1, entry.leaderStyle);
ctx.beginPath();
roundedRectPath(ctx, x, y, w, h, 5);
ctx.fillStyle = WORLD_OVERLAY_STYLE.background;
@@ -851,12 +947,23 @@ export function paintDetectionCallout(ctx, callout, alpha = 1) {
}
/** Dispatch a normalized entry to its pure variant painter. */
export function paintOverlayEntry(ctx, entry, placement, alpha = 1) {
export function paintOverlayEntry(
ctx,
entry,
placement,
alpha = 1,
leaderProgress = 1,
contentAlpha = 1,
) {
if (entry.cardStyle === 'tactical') return paintTacticalCard(ctx, entry, placement, alpha);
if (entry.variant === 'tracked') return paintTracked(ctx, entry, placement, alpha);
if (entry.selected || entry.variant === 'selected') return paintSelected(ctx, entry, placement, alpha);
if (entry.variant === 'thumbnail') return paintThumbnail(ctx, entry, placement, alpha);
if (entry.variant === 'card') return paintCard(ctx, entry, placement, alpha);
if (entry.variant === 'thumbnail') {
return paintThumbnail(ctx, entry, placement, alpha, leaderProgress, contentAlpha);
}
if (entry.variant === 'card') {
return paintCard(ctx, entry, placement, alpha, leaderProgress, contentAlpha);
}
if (entry.variant === 'track') return paintTrack(ctx, entry, placement, alpha);
return paintLabel(ctx, entry, placement, alpha);
}
+4
View File
@@ -24,6 +24,10 @@ export const WORLD_OVERLAY_STYLE = Object.freeze({
fontTrackedTitle: '600 13px "JetBrains Mono", monospace',
fontTrackedDetail: '500 11px "JetBrains Mono", monospace',
radius: 4,
anchorDotRadius: 3.2,
anchorDotStrokeWidth: 1,
anchorDotStroke: 'rgba(4, 12, 16, 0.96)',
leaderWidth: 1.35,
});
/** CCTV's field-tested thumbnail-card overrides on top of shared card chrome. */