From 88484e4301fe13b1e0e249900204edda52d00bad Mon Sep 17 00:00:00 2001 From: Occumed79 Date: Mon, 27 Jul 2026 15:26:55 -0700 Subject: [PATCH 1/8] Keep parent tiles during continuous zoom --- src/occumed-map.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/occumed-map.js b/src/occumed-map.js index 37d78681..ab131692 100644 --- a/src/occumed-map.js +++ b/src/occumed-map.js @@ -91,7 +91,13 @@ export async function createOccumedMap({ hash: false, pixelRatio: resolveOccumedPixelRatio(), antialias: true, - fadeDuration: 0, + // The virtual gateway can take longer than a CDN-hosted static tile source. + // Keep already-requested parent tiles alive while child tiles arrive so zoom + // gestures never expose blank map frames. + cancelPendingTileRequestsWhileZooming: false, + maxTileCacheZoomLevels: 8, + refreshExpiredTiles: false, + fadeDuration: 300, renderWorldCopies: false, attributionControl: false, cooperativeGestures: false, From 59b1545afce33b5dd8d89388e1b54d799f0b878a Mon Sep 17 00:00:00 2001 From: Occumed79 Date: Mon, 27 Jul 2026 15:29:34 -0700 Subject: [PATCH 2/8] Use the surface archive only for the land mask --- src/server/world-tile-gateway.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/server/world-tile-gateway.js b/src/server/world-tile-gateway.js index 5a76b2d5..cf7f2e06 100644 --- a/src/server/world-tile-gateway.js +++ b/src/server/world-tile-gateway.js @@ -188,7 +188,7 @@ export class WorldTileGateway { if (zoom <= surfaceMaxZoom) { const payload = await this.readArchiveTile(surfaceAsset, zoom, x, y); return payload - ? mergeVectorTiles([payload], { includeLayers: ['land', 'landcover', 'depth'] }) + ? mergeVectorTiles([payload], { includeLayers: ['land'] }) : EMPTY_MVT; } @@ -219,8 +219,6 @@ export class WorldTileGateway { } = manifest.virtualTiles; if (zoom <= overviewMaxZoom) { const payload = await this.readArchiveTile(overviewAsset, zoom, x, y); - // The physical surface is resolved independently and must remain visible - // even when an optional overview enrichment tile is absent. return payload || EMPTY_MVT; } From 445082a19d171092814f6f7a25b9ea19fb36abc7 Mon Sep 17 00:00:00 2001 From: Occumed79 Date: Mon, 27 Jul 2026 15:30:39 -0700 Subject: [PATCH 3/8] Add true continuous zoom validation --- scripts/validate-continuous-zoom.mjs | 262 +++++++++++++++++++++++++++ 1 file changed, 262 insertions(+) create mode 100644 scripts/validate-continuous-zoom.mjs diff --git a/scripts/validate-continuous-zoom.mjs b/scripts/validate-continuous-zoom.mjs new file mode 100644 index 00000000..f20c2da6 --- /dev/null +++ b/scripts/validate-continuous-zoom.mjs @@ -0,0 +1,262 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { chromium } from 'playwright'; + +const origin = (process.env.OCCUMED_PREVIEW_ORIGIN || 'http://127.0.0.1:4173').replace(/\/$/, ''); +const outputDir = path.resolve( + process.env.OCCUMED_PREVIEW_OUTPUT || 'visual-validation/continuous-motion' +); +await fs.mkdir(outputDir, { recursive: true }); + +const expectedTemplate = `${origin}/tiles/{z}/{x}/{y}.pbf`; +const browser = await chromium.launch({ headless: true }); + +try { + const context = await browser.newContext({ + viewport: { width: 1440, height: 1000 }, + deviceScaleFactor: 2, + colorScheme: 'dark' + }); + const page = await context.newPage(); + const pageErrors = []; + const networkFailures = []; + const externalVectorRequests = []; + const tileStartedAt = new Map(); + const tileDurations = []; + + page.on('pageerror', (error) => pageErrors.push(error.message)); + page.on('request', (request) => { + const url = request.url(); + if (/\.pbf(?:$|\?)/i.test(url)) { + tileStartedAt.set(request, performance.now()); + if (!url.startsWith(`${origin}/tiles/`)) externalVectorRequests.push(url); + } + }); + page.on('requestfailed', (request) => { + networkFailures.push({ + type: 'requestfailed', + url: request.url(), + error: request.failure()?.errorText || 'unknown request failure' + }); + }); + page.on('response', (response) => { + if (response.status() >= 400) { + networkFailures.push({ + type: 'http', + url: response.url(), + status: response.status() + }); + } + }); + page.on('requestfinished', (request) => { + const started = tileStartedAt.get(request); + if (started === undefined) return; + tileDurations.push({ + url: request.url(), + durationMs: performance.now() - started + }); + tileStartedAt.delete(request); + }); + + await page.goto(origin, { waitUntil: 'domcontentloaded', timeout: 90_000 }); + await page.waitForFunction( + () => globalThis.__OCCUMED_MAP__?.isStyleLoaded(), + null, + { timeout: 90_000 } + ); + + async function waitForStableView(center, zoom) { + await page.evaluate(({ center, zoom }) => { + const map = globalThis.__OCCUMED_MAP__; + map.jumpTo({ center, zoom, pitch: 0, bearing: 0 }); + map.triggerRepaint(); + }, { center, zoom }); + + await page.waitForFunction( + () => { + const map = globalThis.__OCCUMED_MAP__; + return map?.isStyleLoaded() && map.areTilesLoaded() && + map.queryRenderedFeatures().some((feature) => feature.source === 'occumed-open'); + }, + null, + { timeout: 90_000 } + ); + } + + async function runMotion(name, start, end, durationMs) { + await waitForStableView(start.center, start.zoom); + + const result = await page.evaluate(async ({ name, end, durationMs, expectedTemplate }) => { + const map = globalThis.__OCCUMED_MAP__; + const samples = []; + let lastSampleAt = -Infinity; + let sourceChanged = false; + + const sourceSignature = () => { + const source = map.getStyle().sources?.['occumed-open'] || null; + return JSON.stringify({ url: source?.url || null, tiles: source?.tiles || [] }); + }; + const expectedSignature = JSON.stringify({ url: null, tiles: [expectedTemplate] }); + + const sample = (timestamp) => { + if (timestamp - lastSampleAt < 80) return; + lastSampleAt = timestamp; + const signature = sourceSignature(); + sourceChanged ||= signature !== expectedSignature; + const vectorFeatureCount = map + .queryRenderedFeatures() + .filter((feature) => feature.source === 'occumed-open') + .length; + samples.push({ + timestamp, + zoom: map.getZoom(), + center: map.getCenter().toArray(), + vectorFeatureCount, + tilesLoaded: map.areTilesLoaded(), + sourceSignature: signature + }); + }; + + return await new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + map.off('render', sample); + reject(new Error(`${name} motion timed out.`)); + }, durationMs + 30_000); + + const finish = () => { + clearTimeout(timeout); + map.off('render', sample); + sample(performance.now()); + const blankSamples = samples.filter((entry) => entry.vectorFeatureCount === 0); + let longestBlankRun = 0; + let currentBlankRun = 0; + for (const entry of samples) { + if (entry.vectorFeatureCount === 0) { + currentBlankRun += 1; + longestBlankRun = Math.max(longestBlankRun, currentBlankRun); + } else { + currentBlankRun = 0; + } + } + resolve({ + name, + sampleCount: samples.length, + sourceChanged, + blankSampleCount: blankSamples.length, + longestBlankRun, + minimumFeatureCount: Math.min(...samples.map((entry) => entry.vectorFeatureCount)), + maximumFeatureCount: Math.max(...samples.map((entry) => entry.vectorFeatureCount)), + samples + }); + }; + + map.on('render', sample); + map.once('moveend', finish); + map.easeTo({ + center: end.center, + zoom: end.zoom, + pitch: 0, + bearing: 0, + duration: durationMs, + easing: (value) => value, + essential: true + }); + }); + }, { name, end, durationMs, expectedTemplate }); + + await page.screenshot({ + path: path.join(outputDir, `${name}-final.png`), + fullPage: false + }); + return result; + } + + const motions = [ + await runMotion( + 'world-to-fresno', + { center: [-98.5, 25], zoom: 2.43 }, + { center: [-119.7871, 36.7378], zoom: 14 }, + 12_000 + ), + await runMotion( + 'fresno-to-world', + { center: [-119.7871, 36.7378], zoom: 14 }, + { center: [-98.5, 25], zoom: 2.43 }, + 12_000 + ), + await runMotion( + 'cross-border-pan', + { center: [-112.5, 31.8], zoom: 7 }, + { center: [-101.5, 31.8], zoom: 7 }, + 8_000 + ), + await runMotion( + 'europe-shard-pan', + { center: [-4, 50], zoom: 6.5 }, + { center: [24, 50], zoom: 6.5 }, + 9_000 + ) + ]; + + const sortedDurations = tileDurations + .map((entry) => entry.durationMs) + .sort((left, right) => left - right); + const percentile = (fraction) => sortedDurations.length + ? sortedDurations[Math.min(sortedDurations.length - 1, Math.floor(sortedDurations.length * fraction))] + : null; + + const report = { + generatedAt: new Date().toISOString(), + origin, + expectedTemplate, + motions, + tileRequests: { + count: tileDurations.length, + p50Ms: percentile(0.5), + p95Ms: percentile(0.95), + maximumMs: sortedDurations.at(-1) || null, + slowest: [...tileDurations] + .sort((left, right) => right.durationMs - left.durationMs) + .slice(0, 20) + }, + pageErrors, + networkFailures, + externalVectorRequests: [...new Set(externalVectorRequests)] + }; + + await fs.writeFile( + path.join(outputDir, 'continuous-motion-report.json'), + `${JSON.stringify(report, null, 2)}\n` + ); + + const failedMotions = motions.filter( + (motion) => motion.sourceChanged || motion.blankSampleCount > 0 || motion.sampleCount < 20 + ); + if ( + failedMotions.length || + pageErrors.length || + networkFailures.length || + externalVectorRequests.length + ) { + throw new Error(`Continuous motion validation failed: ${JSON.stringify({ + failedMotions: failedMotions.map((motion) => ({ + name: motion.name, + sampleCount: motion.sampleCount, + sourceChanged: motion.sourceChanged, + blankSampleCount: motion.blankSampleCount, + longestBlankRun: motion.longestBlankRun, + minimumFeatureCount: motion.minimumFeatureCount + })), + pageErrors, + networkFailures, + externalVectorRequests + })}`); + } + + console.log( + `Validated ${motions.length} continuous motions with zero blank vector frames; ` + + `tile p95 ${Math.round(report.tileRequests.p95Ms || 0)}ms.` + ); +} finally { + await browser.close(); +} From cca6de0fbe403f962aae753af18948d1ec310597 Mon Sep 17 00:00:00 2001 From: Occumed79 Date: Mon, 27 Jul 2026 15:31:02 -0700 Subject: [PATCH 4/8] Gate map changes on continuous zoom rendering --- .../workflows/validate-continuous-zoom.yml | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 .github/workflows/validate-continuous-zoom.yml diff --git a/.github/workflows/validate-continuous-zoom.yml b/.github/workflows/validate-continuous-zoom.yml new file mode 100644 index 00000000..7ca04f83 --- /dev/null +++ b/.github/workflows/validate-continuous-zoom.yml @@ -0,0 +1,92 @@ +name: Validate Continuous Worldwide Zoom + +on: + workflow_dispatch: + pull_request: + branches: + - main + paths: + - .github/workflows/validate-continuous-zoom.yml + - package-lock.json + - package.json + - style.json + - scripts/** + - src/** + - server.mjs + +permissions: + contents: read + +concurrency: + group: occumed-continuous-zoom-${{ github.ref }} + cancel-in-progress: true + +env: + SURFACE_ASSET: occumed-world-surface.pmtiles + +jobs: + continuous-motion: + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 24 + cache: npm + + - run: npm ci + + - name: Build and validate the production map + run: npm run build + + - name: Install Chromium + run: npx playwright install --with-deps chromium + + - name: Build candidate worldwide physical surface + run: | + set -euo pipefail + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + build-essential libsqlite3-dev zlib1g-dev + bash scripts/build-world-surface.sh \ + "dist/virtual-assets/${SURFACE_ASSET}" + + - name: Run continuous zoom and pan gate + run: | + set -euo pipefail + mkdir -p continuous-motion + OCCUMED_WORLD_SURFACE_URL="http://127.0.0.1:4173/virtual-assets/${SURFACE_ASSET}" \ + node server.mjs > continuous-motion/server.log 2>&1 & + server_pid="$!" + cleanup() { + status="$?" + if [ "$status" -ne 0 ]; then + tail -300 continuous-motion/server.log || true + fi + kill "$server_pid" 2>/dev/null || true + exit "$status" + } + trap cleanup EXIT + + for attempt in {1..90}; do + if curl --fail --silent http://127.0.0.1:4173/healthz > /dev/null; then + break + fi + sleep 1 + done + curl --fail http://127.0.0.1:4173/healthz + + OCCUMED_PREVIEW_OUTPUT=continuous-motion/results \ + node scripts/validate-continuous-zoom.mjs + + - uses: actions/upload-artifact@v4 + if: always() + with: + name: continuous-zoom-${{ github.sha }} + path: | + continuous-motion/results + continuous-motion/server.log + if-no-files-found: error + retention-days: 14 From afec12e1ec3e83ffe23aa1f4df9f4e1d21750338 Mon Sep 17 00:00:00 2001 From: Occumed79 Date: Mon, 27 Jul 2026 15:35:23 -0700 Subject: [PATCH 5/8] Align gateway guard with continuous zoom behavior --- scripts/check-world-tile-gateway.mjs | 35 ++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/scripts/check-world-tile-gateway.mjs b/scripts/check-world-tile-gateway.mjs index eff863f1..178dde55 100644 --- a/scripts/check-world-tile-gateway.mjs +++ b/scripts/check-world-tile-gateway.mjs @@ -104,12 +104,12 @@ const overscaled = inspectVectorTile(overscaleVectorLayer(surface, { targetY: 32768 })); expect(overscaled.land?.featureCount === 1, 'The worldwide land surface cannot overscale through max zoom.'); -const physicalSurface = inspectVectorTile( - mergeVectorTiles([surface], { includeLayers: ['land', 'landcover', 'depth'] }) +const physicalLandMask = inspectVectorTile( + mergeVectorTiles([surface], { includeLayers: ['land'] }) ); -expect(physicalSurface.land?.featureCount === 1, 'The physical surface lost its land layer.'); -expect(physicalSurface.landcover?.featureCount === 1, 'The physical surface lost generalized landcover.'); -expect(physicalSurface.depth?.featureCount === 1, 'The physical surface lost its bathymetry layer.'); +expect(physicalLandMask.land?.featureCount === 1, 'The physical surface lost its land layer.'); +expect(!physicalLandMask.landcover, 'The physical surface still overlays generalized landcover on regional detail.'); +expect(!physicalLandMask.depth, 'The physical surface still overlays generalized bathymetry on regional detail.'); const normalizedProperties = normalizeMvtProperties({ safeRank: 4, unsafePositive: 2 ** 65, @@ -179,7 +179,18 @@ expect(!JSON.stringify(runtime).includes('pmtiles://'), 'A storage archive is st expect(!helper.includes('setUrl('), 'The browser helper can still replace the vector source URL.'); expect(!helper.includes("addProtocol('pmtiles'"), 'The browser still reads PMTiles storage shards directly.'); expect(!helper.includes('WorldPmtilesRouter'), 'The browser still installs the removed regional router.'); -expect(helper.includes('fadeDuration: 0'), 'Tile fading can still expose stale cartography during zoom.'); +expect( + helper.includes('cancelPendingTileRequestsWhileZooming: false'), + 'MapLibre can still cancel parent tiles while child zoom tiles are loading.' +); +expect( + helper.includes('maxTileCacheZoomLevels: 8'), + 'The browser does not retain enough parent zoom levels for continuous motion.' +); +expect( + helper.includes('fadeDuration: 300'), + 'Normal symbol collision fading is not enabled during continuous zoom.' +); expect(server.includes('/tiles\\/(\\d+)\\/(\\d+)\\/(\\d+)\\.pbf'), 'The server does not expose the permanent worldwide tile route.'); expect(server.includes("'world-virtual-manifest.json'"), 'The gateway does not use the isolated server-only manifest.'); expect(!server.includes('/world-tiles/'), 'The server still exposes regional storage URLs to the browser.'); @@ -194,13 +205,17 @@ expect( 'A missing overview enrichment can still reject the complete worldwide tile.' ); expect( - gateway.includes("includeLayers: ['land', 'landcover', 'depth']"), - 'The gateway does not expose land, landcover, and bathymetry as one continuous physical surface.' + gateway.includes("includeLayers: ['land']"), + 'The gateway does not isolate the continuous physical surface to the land mask.' +); +expect( + !gateway.includes("includeLayers: ['land', 'landcover', 'depth']"), + 'The gateway still overlays generalized landcover and bathymetry on regional detail.' ); expect(manifestBuilder.includes('version: 2'), 'The server-only routing manifest is not version 2.'); expect( manifestBuilder.includes("surfaceLayers: ['land', 'landcover', 'depth']"), - 'The routing manifest does not declare the complete physical surface schema.' + 'The routing manifest does not document the surface archive schema.' ); expect(!manifestBuilder.includes('switchZoom'), 'The obsolete browser switch zoom remains in the manifest.'); @@ -217,4 +232,4 @@ if (failures.length) { process.exit(1); } -console.log('Virtual worldwide tileset validated: one permanent source, boundary merging, antimeridian routing, land, landcover, and bathymetry continuity, surface overscaling, caching, and no browser-visible shards.'); +console.log('Virtual worldwide tileset validated: one permanent source, boundary merging, antimeridian routing, land-mask continuity, parent-tile retention, caching, and no browser-visible shards.'); From 75d7a16c352754104583d2d9aa172c9965fdc473 Mon Sep 17 00:00:00 2001 From: Occumed79 Date: Mon, 27 Jul 2026 15:37:36 -0700 Subject: [PATCH 6/8] Guard parent-tile retention instead of zero fade --- scripts/check-pmtiles-integration.mjs | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/scripts/check-pmtiles-integration.mjs b/scripts/check-pmtiles-integration.mjs index b34597ec..ae35a6e4 100644 --- a/scripts/check-pmtiles-integration.mjs +++ b/scripts/check-pmtiles-integration.mjs @@ -52,7 +52,18 @@ if (styleBuilder.toLowerCase().includes('openfreemap')) { if (helper.includes('Protocol') || helper.includes('setUrl(') || helper.includes('world-pmtiles-router')) { fail('The browser helper still contains PMTiles shard routing.'); } -if (!helper.includes('fadeDuration: 0')) fail('The browser can crossfade stale basemap tiles.'); +if (!helper.includes('cancelPendingTileRequestsWhileZooming: false')) { + fail('The browser can cancel still-loading parent tiles during zoom.'); +} +if (!helper.includes('maxTileCacheZoomLevels: 8')) { + fail('The browser does not retain enough parent zoom levels for seamless motion.'); +} +if (!helper.includes('refreshExpiredTiles: false')) { + fail('The browser can replace visible tiles through in-session expiry refreshes.'); +} +if (!helper.includes('fadeDuration: 300')) { + fail('Normal symbol collision fading is not enabled during zoom.'); +} if (!helper.includes("source.tiles = source.tiles.map")) { fail('Permanent vector tile templates are not resolved to the style origin.'); } @@ -66,6 +77,12 @@ if (!gateway.includes('Promise.all')) fail('The gateway cannot resolve intersect if (!gateway.includes('mergeVectorTiles')) fail('The gateway cannot merge MVT layers at shard boundaries.'); if (!gateway.includes('MemoryTileCache')) fail('Resolved worldwide tiles are not cached.'); if (!gateway.includes('overscaleVectorLayer')) fail('The land surface cannot remain continuous above its generalized zoom.'); +if (!gateway.includes("includeLayers: ['land']")) { + fail('The gateway does not isolate the worldwide surface to a non-overlapping land mask.'); +} +if (gateway.includes("includeLayers: ['land', 'landcover', 'depth']")) { + fail('The gateway still overlays generalized surface detail on regional geometry.'); +} if (!server.includes('OCCUMED_WORLD_SURFACE_URL')) { fail('Read-only visual validation cannot serve its candidate physical surface.'); } @@ -77,7 +94,7 @@ if (!manifestBuilder.includes('version: 2')) fail('The server-only routing manif if (!manifestBuilder.includes('overviewAsset')) fail('The manifest does not declare the consolidated overview archive.'); if (!manifestBuilder.includes('surfaceAsset')) fail('The manifest does not declare the worldwide land surface.'); if (!manifestBuilder.includes("surfaceLayers: ['land', 'landcover', 'depth']")) { - fail('The manifest does not declare continuous vector land, landcover, and bathymetry.'); + fail('The manifest does not document the physical surface archive schema.'); } if (manifestBuilder.includes('switchZoom')) fail('The obsolete browser switch zoom remains in the manifest.'); if (manifestBuilder.includes('archiveProxyTemplate')) fail('The manifest still advertises regional archives to browsers.'); @@ -121,4 +138,4 @@ if (failures.length) { process.exit(1); } -console.log('PMTiles storage integration validated behind one permanent worldwide vector endpoint.'); +console.log('PMTiles storage integration validated behind one permanent worldwide vector endpoint with parent-tile retention and non-overlapping surface geometry.'); From fee7bd81e3f98d3d71d1fd1a28ec261daed2eb1a Mon Sep 17 00:00:00 2001 From: Occumed79 Date: Mon, 27 Jul 2026 15:49:04 -0700 Subject: [PATCH 7/8] Constrain atmosphere to a narrow white-blue globe rim --- scripts/lock-reference-atmosphere.mjs | 30 ++++++++++++++------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/scripts/lock-reference-atmosphere.mjs b/scripts/lock-reference-atmosphere.mjs index b4a3ee47..471ccb6f 100644 --- a/scripts/lock-reference-atmosphere.mjs +++ b/scripts/lock-reference-atmosphere.mjs @@ -8,31 +8,32 @@ const runtime = JSON.parse(await fs.readFile(runtimePath, 'utf8')); runtime.projection = { type: 'globe' }; -// Match the supplied Studio globe: dark space, a narrow white horizon edge, -// and a broader cool-blue atmospheric bloom. Ground and horizon fog remain -// disabled so the glow stays outside the globe instead of washing out the map. +// MapLibre's atmosphere is a screen-space globe effect, not a directional sun. +// Keep the map surface neutral and confine the white-blue light to a thin, +// even rim at the globe limb. High blend values push the atmosphere far across +// the visible hemisphere and create the incorrect "sunlit half-planet" wash. runtime.sky = { 'sky-color': '#181A1D', - 'horizon-color': '#F5FDFF', - 'fog-color': '#B8E6FF', + 'horizon-color': 'rgba(245, 253, 255, 0.98)', + 'fog-color': 'rgba(184, 230, 255, 0.14)', 'sky-horizon-blend': [ 'interpolate', ['linear'], ['zoom'], - 0, 0.24, - 2.5, 0.2, - 4.5, 0.1, + 0, 0.052, + 2.5, 0.042, + 4.5, 0.018, 6.25, 0 ], - 'horizon-fog-blend': 0, + 'horizon-fog-blend': 0.08, 'fog-ground-blend': 0, 'atmosphere-blend': [ 'interpolate', ['linear'], ['zoom'], - 0, 0.84, - 2.5, 0.76, - 4.5, 0.38, + 0, 0.18, + 2.5, 0.145, + 4.5, 0.06, 6.25, 0 ] }; @@ -43,10 +44,11 @@ delete runtime.light; runtime.metadata = { ...(runtime.metadata || {}), 'occumed:reference-atmosphere': true, - 'occumed:reference-atmosphere-pass': 2, + 'occumed:reference-atmosphere-pass': 3, 'occumed:atmosphere-surface-wash-disabled': true, + 'occumed:atmosphere-edge-only': true, 'occumed:atmosphere-fades-before-detail': true }; await fs.writeFile(runtimePath, `${JSON.stringify(runtime, null, 2)}\n`); -console.log('Locked the luminous white-blue reference atmosphere without adding surface fog.'); +console.log('Locked a narrow, even white-blue globe rim without surface wash.'); From 61c42f05333b41ddce1459db9f278f87e9244bde Mon Sep 17 00:00:00 2001 From: Occumed79 Date: Mon, 27 Jul 2026 15:49:45 -0700 Subject: [PATCH 8/8] Reject broad atmospheric surface wash --- scripts/check-globe-parity.mjs | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/scripts/check-globe-parity.mjs b/scripts/check-globe-parity.mjs index 309845a8..1beabf6c 100644 --- a/scripts/check-globe-parity.mjs +++ b/scripts/check-globe-parity.mjs @@ -35,25 +35,37 @@ if (runtime.projection?.type !== 'globe') fail('The reusable style must use glob if (runtime.fog) fail('Mapbox fog must be translated instead of shipped to MapLibre unchanged.'); if (!runtime.sky) fail('The MapLibre sky/atmosphere configuration is missing.'); if (runtime.sky?.['sky-color'] !== '#181A1D') fail('The fixed reference-space hex changed.'); -if (runtime.sky?.['horizon-color'] !== '#F5FDFF') fail('The atmospheric horizon hex changed.'); -if (runtime.sky?.['fog-color'] !== '#B8E6FF') fail('The cool outer-atmosphere hex changed.'); +if (runtime.sky?.['horizon-color'] !== 'rgba(245, 253, 255, 0.98)') { + fail('The narrow white horizon color changed.'); +} +if (runtime.sky?.['fog-color'] !== 'rgba(184, 230, 255, 0.14)') { + fail('The translucent cool-blue outer glow changed.'); +} if (!runtime.sky?.['atmosphere-blend']) fail('The globe atmosphere blend is missing.'); if (runtime.light) fail('Directional global light must remain disabled to prevent rotation-dependent washout.'); -if (runtime.sky?.['horizon-fog-blend'] !== 0) fail('Horizon fog must remain disabled to prevent surface washout.'); +if ((runtime.sky?.['horizon-fog-blend'] ?? 1) > 0.1) { + fail('Horizon fog is broad enough to wash over the visible hemisphere.'); +} if (runtime.sky?.['fog-ground-blend'] !== 0) fail('Ground fog must remain disabled to preserve surface contrast.'); const atmosphereOutputs = expressionOutputs(runtime.sky?.['atmosphere-blend']); -if (!atmosphereOutputs.some((value) => value >= 0.65)) { - fail('The white-blue atmosphere is too weak to match the supplied glowing globe reference.'); +if (!atmosphereOutputs.some((value) => value >= 0.12)) { + fail('The white-blue atmospheric edge is too weak to remain visible.'); +} +if (atmosphereOutputs.some((value) => value > 0.2)) { + fail('The atmosphere extends too far across the globe surface and reads as directional sunlight.'); } if (atmosphereOutputs.at(-1) !== 0) { fail('The globe atmosphere does not disappear before detailed regional and city zooms.'); } const horizonOutputs = expressionOutputs(runtime.sky?.['sky-horizon-blend']); -if (!horizonOutputs.some((value) => value >= 0.2)) { +if (!horizonOutputs.some((value) => value >= 0.03)) { fail('The narrow luminous horizon rim is too weak.'); } +if (horizonOutputs.some((value) => value > 0.06)) { + fail('The horizon blend is too broad to remain an edge-only bloom.'); +} if (horizonOutputs.at(-1) !== 0) { fail('The horizon rim does not fade out before detailed zooms.'); } @@ -105,6 +117,7 @@ if (runtime.metadata?.['occumed:layer-specific-palette'] !== true) fail('Layer-s if (runtime.metadata?.['occumed:raster-relief-disabled'] !== true) fail('Raster relief protection is missing.'); if (runtime.metadata?.['occumed:reference-atmosphere'] !== true) fail('The reference atmosphere pass did not run.'); if (runtime.metadata?.['occumed:atmosphere-surface-wash-disabled'] !== true) fail('Atmosphere surface-wash protection is missing.'); +if (runtime.metadata?.['occumed:atmosphere-edge-only'] !== true) fail('The edge-only atmosphere protection is missing.'); if (failures.length) { console.error('Occu-Med globe parity validation failed:'); @@ -112,4 +125,4 @@ if (failures.length) { process.exit(1); } -console.log(`Globe parity validated: dark space, strong white-blue atmosphere, layered land, clear blue water, and ${allColors.size} structure-specific colors.`); +console.log(`Globe parity validated: dark space, narrow white-blue edge bloom, neutral surface lighting, layered land, clear blue water, and ${allColors.size} structure-specific colors.`);