From 16f16ec629fb6d1d07708e403e08b9de7dca4345 Mon Sep 17 00:00:00 2001 From: n30nex Date: Sun, 13 Sep 2026 01:20:53 -0400 Subject: [PATCH] Keep empty focus layers out of terrain redraws --- web/e2e/terrain-scale.spec.ts | 38 ----------------------------------- web/src/map.ts | 7 ++++--- 2 files changed, 4 insertions(+), 41 deletions(-) diff --git a/web/e2e/terrain-scale.spec.ts b/web/e2e/terrain-scale.spec.ts index 7d3980e..1dd5714 100644 --- a/web/e2e/terrain-scale.spec.ts +++ b/web/e2e/terrain-scale.spec.ts @@ -73,39 +73,6 @@ test('keeps controls responsive while preparing a dense 7000-route terrain mesh' await expect(map).toHaveAttribute('data-buildings-loaded', 'true'); await page.evaluate(() => new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(() => resolve())))); expect(Date.now() - terrainStarted, 'the cold terrain scene must meet the startup budget').toBeLessThan(10000); - await page.evaluate(() => { - const canvas = document.querySelector('#map .maplibregl-canvas')!; - const gl = canvas.getContext('webgl2')!; - const records: Array<{ call: string; kind: string; count: number; before: number; draw: number; viewport: number[]; screen: boolean }> = []; - const restore: Array<() => void> = []; - const kinds = new WeakMap(); - const target = gl as unknown as Record unknown>; - for (const name of ['drawArrays', 'drawElements', 'drawArraysInstanced', 'drawElementsInstanced']) { - const original = target[name]!; - target[name] = (...args: number[]): unknown => { - if (records.length >= 3) return original.apply(gl, args); - const program = gl.getParameter(gl.CURRENT_PROGRAM) as WebGLProgram; - let kind = kinds.get(program); - if (!kind) { - const shader = (gl.getAttachedShaders(program) ?? []).map(s => gl.getShaderSource(s) ?? '').join('\n'); - kind = shader.includes('u_maximum_band') ? 'history' : /circle_radius|u_circle/.test(shader) ? 'circles' - : /hillshade/.test(shader) ? 'hillshade' : /extrusion/.test(shader) ? 'buildings' - : /u_dem|u_depth/.test(shader) ? 'terrain' : 'map'; - kinds.set(program, kind); - } - if (kind !== 'history') return original.apply(gl, args); - const pixel = new Uint8Array(4); - const start = performance.now(); gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixel); const ready = performance.now(); - const result = original.apply(gl, args); gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixel); - records.push({ call: name, kind, count: name.includes('Elements') ? args[1]! : args[2]!, - before: ready - start, draw: performance.now() - ready, - viewport: Array.from(gl.getParameter(gl.VIEWPORT) as Int32Array), screen: gl.getParameter(gl.FRAMEBUFFER_BINDING) === null }); - return result; - }; - restore.push(() => { target[name] = original; }); - } - (window as unknown as { denseGpuProbe: { records: typeof records; restore(): void } }).denseGpuProbe = { records, restore: () => restore.forEach(fn => fn()) }; - }); await openMapOptions(page); await page.locator('#routes-button').click(); await page.keyboard.press('Escape'); @@ -124,11 +91,6 @@ test('keeps controls responsive while preparing a dense 7000-route terrain mesh' }; setTimeout(tick, 0); })); - const gpuDraws = await page.evaluate(() => { - const probe = (window as unknown as { denseGpuProbe: { records: unknown[]; restore(): void } }).denseGpuProbe; - probe.restore(); return probe.records; - }); - await info.attach('dense-gpu-draws', { body: JSON.stringify(gpuDraws), contentType: 'application/json' }); await info.attach('dense-terrain-timing', { body: JSON.stringify({ ...loopTiming, mesh: await map.evaluate(el => ({ ...el.dataset })) }), contentType: 'application/json' }); expect(loopTiming.duration, 'terrain preparation must leave the event loop responsive').toBeLessThan(2000); await expect.poll(() => map.getAttribute('data-route-terrain-samples').then(Number), { timeout: 15000 }).toBeGreaterThan(100); diff --git a/web/src/map.ts b/web/src/map.ts index ccb5db5..70c1872 100644 --- a/web/src/map.ts +++ b/web/src/map.ts @@ -772,7 +772,7 @@ export class LiveMap { this.updateTerrainRoutes(); const maxAge = this.effectiveRouteAgeMS(); const visualApplied = detailSource - ? applyRouteVisibilityForZoom(this.map, visible, maxAge, this.map.getZoom()) + ? applyRouteVisibilityForZoom(this.map, visible && this.selectedNodeID !== null, maxAge, this.map.getZoom()) : false; const hitApplied = this.selectedNodeID !== null && applyRouteHitLayerVisibility(this.map, visible); const neighborsApplied = this.selectedNodeID !== null && applyNeighborRingVisibility(this.map, visible); @@ -982,7 +982,7 @@ export class LiveMap { } const visibilityApplied = applyRouteVisibilityForZoom( this.map, - this.routesVisible, + this.routesVisible && this.selectedNodeID !== null, this.effectiveRouteAgeMS(), this.map.getZoom() ); @@ -1073,7 +1073,7 @@ export class LiveMap { this.map.addSource(ROUTE_DETAIL_SOURCE_ID, { type: 'geojson', data: EMPTY_LINES, maxzoom: 16 }); this.map.addSource(ROUTE_FOCUS_SOURCE_ID, { type: 'geojson', data: EMPTY_LINES, maxzoom: 16 }); this.applyRouteTimeState(Date.now(), true); - const exactVisibility = this.routesVisible ? 'visible' : 'none'; + const exactVisibility = this.routesVisible && this.selectedNodeID !== null ? 'visible' : 'none'; this.historicalRouteLayer.setVisible(this.routesVisible); this.historicalRouteLayer.setMaximumBand(routeWindowBand(this.effectiveRouteAgeMS())); this.map.addLayer(this.historicalRouteLayer); @@ -1516,6 +1516,7 @@ export class LiveMap { private applyFocusState(updateRouteFilter = true): void { const focusIDs = this.selectedNodeID ? [this.selectedNodeID, ...this.neighborNodeIDs] : []; + applyRouteVisibilityForZoom(this.map, this.routesVisible && this.selectedNodeID !== null, this.effectiveRouteAgeMS(), this.map.getZoom()); if (updateRouteFilter) applyRouteSelectionFilter(this.map, this.selectedNodeID); applySelectedNodeFilter(this.map, this.selectedNodeID); applyNodeFocus(this.map, this.selectedNodeID, focusIDs, this.neighborNodeIDs);