feat(weather): add globe wind, observed weather and cyclone context (#617)

Wind (NOAA GFS and ECMWF IFS: animated particles, colour field, a reading at map centre), observed weather (NOAA MRMS radar, GOES and NESDIS infrared clouds, lightning density) on one shared history timeline, NHC and CPHC cyclone advisories with tracks and cones, and a WEATHER panel in the right rail. On 3D Tiles the observed products render as raised shells with a sharper window around the view.

Co-authored-by: Gustavo Beneduzi <[email protected]>
Co-authored-by: Daniel Slay <[email protected]>
Co-authored-by: Rehaan Delmotra <[email protected]>
Co-authored-by: Cursor Agent <[email protected]>
This commit is contained in:
authored and GitHub committed 2026-09-22 17:45:39 -05:00
1 parent f01b6a5d84
commit 0251c74109
180 files changed
+30645 -434

No files matched your search

+63 -43
View File
@@ -7,6 +7,8 @@ import {
shouldHideCollapsedRightPanels,
} from './panelRailGeometry.js';
const pendingCollapseRetries = new WeakSet();
/**
* Measure and place the right panel rail for one synchronous layout pass.
* The caller owns scheduling, obstacle selection, disclosure preferences and
@@ -40,9 +42,11 @@ export function layoutRightPanelRail({
getComputedStyle = (element) => windowRef.getComputedStyle(element),
}) {
if (!stack) return;
// A collapse may schedule one follow-up, which only measures and allocates.
const isCollapseRetry = pendingCollapseRetries.delete(stack);
const panels = [...stack.children].filter((panel) =>
panel.matches('[data-panel-id]'),
const panels = [...stack.children].filter(
(panel) => panel.matches('[data-panel-id]') && !panel.hidden,
);
if (!hud.visible || hud.variant !== 'tactical') {
for (const panel of panels.filter((item) =>
@@ -113,33 +117,53 @@ export function layoutRightPanelRail({
});
}
// Auto-collapsed headers contribute the same need even when exclusive
// presentation hides them. Manually collapsed launchers keep their policy.
const measuredPanels = panels.filter(
(panel) =>
!exclusive ||
!panel.classList.contains('collapsed') ||
panel.classList.contains('layout-auto-collapsed'),
);
const visiblePanels = panels.filter(
(panel) => !exclusive || !panel.classList.contains('collapsed'),
);
const displayScrollTop = readDisplayScrollTop();
// Measure intrinsic content, not the allocation written by the previous
// layout pass. Display is the exception: its own scrollHeight already
// exposes every control, and removing its live allocation can reset the
// user's scroll position while HUD or preset content is settling.
for (const panel of visiblePanels) {
if (!panel.classList.contains('collapsed') && panel !== displayPanel) {
panel.style.removeProperty('--right-panel-allocated-height');
}
}
const gap = parseFloat(getComputedStyle(stack).rowGap) || 0;
const naturalHeight =
visiblePanels.reduce(
(total, panel) =>
total +
// Measuring lifts each opted-in scroller's max-height, which clamps its
// scroll offset; remember the offsets to put back afterwards.
const scrollers = [
...(stack.querySelectorAll?.('[data-rail-scroller]') || []),
]
.map((node) => [node, node.scrollTop || 0])
.filter(([, top]) => top > 0);
const naturalHeights = new Map();
// Remove live height allocations through a synchronous CSS override. Keeping
// the stored properties intact avoids REMOVE/SET churn on unchanged panels.
// Do not toggle layout classes here: the class observer schedules new passes.
stack.setAttribute('data-rail-measuring', '');
try {
for (const panel of measuredPanels) {
naturalHeights.set(
panel,
Math.max(
panel.getBoundingClientRect().height,
panel.scrollHeight || 0,
panel.classList.contains('collapsed') ? 42 : 0,
),
);
}
} finally {
stack.removeAttribute('data-rail-measuring');
for (const [node, top] of scrollers)
if (node.scrollTop !== top) node.scrollTop = top;
}
const gap = parseFloat(getComputedStyle(stack).rowGap) || 0;
const naturalHeight =
measuredPanels.reduce(
(total, panel) => total + naturalHeights.get(panel),
0,
) +
gap * Math.max(0, visiblePanels.length - 1);
gap * Math.max(0, measuredPanels.length - 1);
const layout = resolveHudRailLayout({
viewportHeight,
panelHeight: naturalHeight,
@@ -153,16 +177,16 @@ export function layoutRightPanelRail({
});
if (!layout) return;
const { safeTop, safeBottom, maxHeight: availableHeight } = layout;
const stabilityBand = viewportHeight * 0.01;
const stabilityBand = Math.max(48, viewportHeight * 0.06);
const wasFocused = stack.classList.contains('layout-focus');
const shouldFocus = wasFocused
? naturalHeight > availableHeight - stabilityBand * 2
: naturalHeight > availableHeight - stabilityBand;
? naturalHeight >= availableHeight - stabilityBand
: naturalHeight > availableHeight;
const layoutTop = shouldFocus ? safeTop : layout.top;
const collapsedHeight = visiblePanels.reduce(
(total, panel) =>
panel.classList.contains('collapsed')
? total + panel.getBoundingClientRect().height
? total + naturalHeights.get(panel)
: total,
0,
);
@@ -194,38 +218,34 @@ export function layoutRightPanelRail({
gap * Math.max(0, visiblePanels.length - 1),
);
const expandedHeights = allocatePanelStackHeights({
naturalHeights: expandedPanels.map((panel) =>
Math.max(panel.getBoundingClientRect().height, panel.scrollHeight || 0),
),
naturalHeights: expandedPanels.map((panel) => naturalHeights.get(panel)),
availableHeight: expandedAvailableHeight,
});
const autoCollapseIndices = hud.visible
? panelStackAutoCollapseIndices({
naturalHeights: expandedPanels.map((panel) =>
Math.max(
panel.getBoundingClientRect().height,
panel.scrollHeight || 0,
// Other HUD variants restore automatic collapse at the start of each pass.
// Do not immediately collapse those panels again and schedule a loop.
const autoCollapseIndices =
hud.visible && hud.variant === 'tactical'
? panelStackAutoCollapseIndices({
naturalHeights: expandedPanels.map((panel) =>
naturalHeights.get(panel),
),
),
allocatedHeights: expandedHeights,
collapseLaterPanels: shouldFocus && hud.variant === 'tactical',
})
: [];
if (autoCollapseIndices.length) {
allocatedHeights: expandedHeights,
collapseLaterPanels: shouldFocus && hud.variant === 'tactical',
})
: [];
if (!isCollapseRetry && autoCollapseIndices.length) {
for (const index of autoCollapseIndices) {
const panel = expandedPanels[index];
panel.classList.add('collapsed', 'layout-auto-collapsed');
onCollapse(panel);
}
pendingCollapseRetries.add(stack);
onRetry();
return;
}
// Write-if-changed. This pass runs on the 500 ms stats cadence, and an
// unconditional REMOVE-then-SET of an unchanged allocation is two style
// mutations per tick on `#pp-toggles` (the one panel the measure-strip
// above deliberately skips) — churn that reads as a genuine panel move to
// the world-overlay host's occluder observer and defeats parked-idle
// render savings. Only a real allocation change may touch the attribute.
// Write-if-changed. This pass runs on the 500 ms stats cadence. Rewriting
// an unchanged allocation wakes the world-overlay occluder observer and
// defeats parked-idle render savings. Measurement leaves these values intact.
expandedPanels.forEach((panel, index) => {
const next = `${expandedHeights[index].toFixed(1)}px`;
if (