From 811bcaff850bce9987aa5142013138b123f62a82 Mon Sep 17 00:00:00 2001 From: German Khokhlov Date: Tue, 3 Nov 2026 03:49:39 +0400 Subject: [PATCH] Retain the Nepal locator presentation and draw it on either globe --- scripts/package-boundaries.json | 1 + scripts/qa-nepal-locator-switch.mjs | 145 ++++++++++++++++ src/app/constructCatalog.js | 5 +- src/app/navaraLayers.test.mjs | 1 + src/app/navaraMode.js | 5 + src/data/bhoteKoshiLocator.js | 118 +++++++------ src/data/dataCredits.js | 10 +- src/data/dataCredits.test.mjs | 3 + src/globe/navara/index.js | 2 + src/layers/shared/render/navaraStory.js | 173 ++++++++++++++++++++ src/layers/submarineCables/render/navara.js | 3 +- 11 files changed, 409 insertions(+), 57 deletions(-) create mode 100644 scripts/qa-nepal-locator-switch.mjs create mode 100644 src/layers/shared/render/navaraStory.js diff --git a/scripts/package-boundaries.json b/scripts/package-boundaries.json index 696145e57..cdadd9b6a 100644 --- a/scripts/package-boundaries.json +++ b/scripts/package-boundaries.json @@ -3679,6 +3679,7 @@ "src/layers/shared/render/navaraStems.js", "src/layers/submarineCables/render/navara.js", "src/layers/directions/render/navara.js", + "src/layers/shared/render/navaraStory.js", "src/layers/weather/render/navara.js", "src/layers/weather/render/navaraSurface.js", "src/layers/wind/render/navara.js", diff --git a/scripts/qa-nepal-locator-switch.mjs b/scripts/qa-nepal-locator-switch.mjs new file mode 100644 index 000000000..6f3209fb2 --- /dev/null +++ b/scripts/qa-nepal-locator-switch.mjs @@ -0,0 +1,145 @@ +import { fromDegrees } from '../src/geo/cartesian3.js'; +import sharp from 'sharp'; +import puppeteer from 'puppeteer'; +import assert from 'node:assert/strict'; +import { blockPaidQaMaps } from './qa-map-policy.mjs'; + +const base = new URL(process.argv[2]); +if ( + !['localhost', '127.0.0.1'].includes(base.hostname) || + ['', '4173', '4185', '4186', '4187', '4188', '4189', '4194'].includes( + base.port, + ) +) + throw new Error('Supply an unreserved local keyless preview URL'); +base.search = '?engine=navara&welcome=0&look=cinematic'; +base.hash = + 'v=2&lat=28.3326&lon=85.4848&alt=18000&heading=0&pitch=-90&roll=0&map=sentinel2&l='; +const b = await puppeteer.launch({ + headless: true, + args: ['--use-angle=metal', '--enable-gpu'], +}); +try { + const p = await b.newPage(); + await blockPaidQaMaps(p); + await p.setViewport({ width: 1024, height: 768 }); + const errors = []; + p.on('pageerror', (e) => { + errors.push(e.message); + console.log('error', e.message.split('?')[0]); + }); + p.on('console', (msg) => { + if (msg.type() === 'error' && /Shader|WebGLProgram/i.test(msg.text())) + errors.push(msg.text()); + }); + await p.goto(base.href); + const ready = async (kind) => { + await p.waitForFunction( + (k) => + window.__godsEyeView?.engine.kind === k && + !document.querySelector('#globe-engine')?.disabled && + !document.querySelector('.engine-switch-frame') && + document.querySelector('#loading-screen.hidden'), + { timeout: 90000 }, + kind, + ); + await p.evaluate( + () => window.__godsEyeView.styleManager.initialRestorePromise, + ); + }; + await ready('navara'); + await p.evaluate(async () => { + const { tileset, dataManager } = window.__godsEyeView; + if (tileset || window.__GOOGLE_MAPS_API_KEY__) + throw Error('Keyless preview required'); + window.qaLocator = dataManager.layers.get('bhote-koshi-locator').module; + window.qaLocator.setParams({ presentation: 'bhote-koshi-path-overview' }); + await dataManager.setEnabled('bhote-koshi-locator', true, { + origin: 'user', + }); + }); + for (const kind of ['navara', 'cesium', 'navara']) { + if ((await p.evaluate(() => window.__godsEyeView.engine.kind)) !== kind) { + await p.select('#globe-engine', kind); + await ready(kind); + } + const { same, stats, params, at } = await p.evaluate( + (world) => { + const { engine, dataManager } = window.__godsEyeView, + mod = dataManager.layers.get('bhote-koshi-locator').module; + const pos = engine.projector.worldToWindow(world, {}), + box = engine.host.canvas.getBoundingClientRect(); + return { + same: mod === window.qaLocator, + stats: mod.getStats(), + params: mod.getParams(), + at: { x: pos.x + box.left, y: pos.y + box.top }, + }; + }, + fromDegrees(85.47741, 28.33299, 1600), + ); + assert.equal(same, true); + assert.equal(stats.count, 1); + assert.equal(params.presentation, 'bhote-koshi-path-overview'); + // Look for the cyan river in a scene crop away from the UI rails. + await new Promise((r) => setTimeout(r, 1000)); + const crop = async () => + sharp(await p.screenshot()) + .extract({ left: 400, top: 320, width: 230, height: 180 }) + .removeAlpha() + .raw() + .toBuffer(); + const rendered = await crop(); + await p.evaluate(() => + window.qaLocator.setParams({ presentation: 'nepal-context' }), + ); + await new Promise((resolve) => setTimeout(resolve, 500)); + const baseline = await crop(); + let added = 0; + for (let i = 0; i < rendered.length; i += 3) { + const contrast = rendered[i + 1] - rendered[i], + previous = baseline[i + 1] - baseline[i]; + if ( + rendered[i + 1] > 140 && + rendered[i + 2] > 140 && + contrast > 35 && + contrast - previous > 18 + ) + added++; + } + console.log( + kind, + stats.coverage, + 'cyan pixels added by the path', + added, + at, + ); + assert.ok( + added > 50, + 'visible river differs from the same scene without a path', + ); + await p.evaluate(() => + window.qaLocator.setParams({ presentation: 'bhote-koshi-path-overview' }), + ); + } + await p.evaluate(() => { + window.qaLocator.setParams({ presentation: 'bhote-koshi-incident-places' }); + window.qaLocator.setParams({ presentation: 'bhote-koshi-city-context' }); + window.qaLocator.setParams({ + presentation: 'bhote-koshi-trigger-record', + preview: true, + }); + }); + await new Promise((resolve) => setTimeout(resolve, 1000)); + await p.evaluate(() => + window.__godsEyeView.dataManager.setEnabled('bhote-koshi-locator', false, { + origin: 'user', + }), + ); + assert.deepEqual(errors, []); + console.log( + 'PASS Nepal locator flood path and presentation retained, trigger and places update; both engines', + ); +} finally { + await b.close(); +} diff --git a/src/app/constructCatalog.js b/src/app/constructCatalog.js index 51f04cab7..0fd82b3cc 100644 --- a/src/app/constructCatalog.js +++ b/src/app/constructCatalog.js @@ -164,9 +164,10 @@ export function createApplicationCatalog({ [ 'bhote-koshi-locator', () => - createBhoteKoshiLocatorLayer({ + sessionModel('bhote-koshi-locator', () => createBhoteKoshiLocatorLayer({ boundaryResolver: nepalBoundaryResolver, - }), + createNativeRenderer: (engine, data) => rendererFor('bhote-koshi-locator', engine, { data }), + })), ], ['flights', flights], ['military', military], diff --git a/src/app/navaraLayers.test.mjs b/src/app/navaraLayers.test.mjs index fa4effc71..78eb43c03 100644 --- a/src/app/navaraLayers.test.mjs +++ b/src/app/navaraLayers.test.mjs @@ -43,6 +43,7 @@ test('the Navara mode registers migrated renderers once', async () => { .available() .map(({ id }) => id), [ + 'bhote-koshi-locator', 'flights', 'military', 'local-adsb', diff --git a/src/app/navaraMode.js b/src/app/navaraMode.js index 96d6df97a..7578506c0 100644 --- a/src/app/navaraMode.js +++ b/src/app/navaraMode.js @@ -120,6 +120,10 @@ export async function registerNavaraLayerRenderers() { return (engine, options) => createWeatherNavaraRenderer(engine, navaraViews.get(engine), options); }, ])), + 'bhote-koshi-locator': async () => { + const { drawStorySource } = await import('../layers/shared/render/navaraStory.js'); + return (engine, { data }) => drawStorySource(engine, navaraViews.get(engine), data); + }, directions: async () => { const { createDirectionsNavaraRenderer } = await import('../layers/directions/render/navara.js'); return (engine) => createDirectionsNavaraRenderer(engine, navaraViews.get(engine)); @@ -258,6 +262,7 @@ export async function createNavaraScene({ view: globe.view, alignDaylightToRestoredView: globe.alignDaylightToRestoredView, MeshDesc: globe.MeshDesc, + NavaraColor: globe.NavaraColor, EffectDesc: globe.EffectDesc, NavaraEffect: globe.NavaraEffect, DrapedMesh: globe.DrapedMesh, diff --git a/src/data/bhoteKoshiLocator.js b/src/data/bhoteKoshiLocator.js index 43b685a54..9c169446f 100644 --- a/src/data/bhoteKoshiLocator.js +++ b/src/data/bhoteKoshiLocator.js @@ -276,8 +276,9 @@ export function largestGeoJsonRing(geojson) { */ export function createBhoteKoshiLocatorLayer({ boundaryResolver = DEFAULT_BOUNDARY_RESOLVER, + createNativeRenderer, overlayHost = DEFAULT_OVERLAY_HOST, - requestRender = governorRequestRender, + requestRender: requestSceneRender = governorRequestRender, now = () => performance.now(), scheduleFrame = (callback) => requestAnimationFrame(callback), cancelFrame = (handle) => cancelAnimationFrame(handle), @@ -285,6 +286,12 @@ export function createBhoteKoshiLocatorLayer({ cancelDelay = (handle) => clearTimeout(handle), } = {}) { let _viewer = null; + let _engine = null; + let _native = null; + let _pausedApproach = null; + const cameraForScene = () => _engine?.camera ?? _viewer?.camera; + const creditTarget = () => _engine ?? _viewer; + const requestRender = reason => _engine?.kind === 'navara' ? _engine.render.request(reason) : requestSceneRender(reason); let _dataSource = null; let _enabled = false; let _boundaryReady = false; @@ -332,7 +339,7 @@ export function createBhoteKoshiLocatorLayer({ _removeTriggerMoveEnd = null; const wasInFlight = _triggerApproachInFlight; _triggerApproachInFlight = false; - if (wasInFlight) _viewer?.camera?.cancelFlight?.(); + if (wasInFlight) cameraForScene()?.cancelFlight?.(); if (!preserveCallout) _triggerCalloutStartedAt = Number.POSITIVE_INFINITY; } @@ -349,6 +356,8 @@ export function createBhoteKoshiLocatorLayer({ function removeDataSource() { stopAnimations(); overlayHost.clearSource(BHOTE_KOSHI_LOCATOR_OVERLAY_SOURCE_ID); + _native?.destroy(); + _native = null; if (_dataSource && _viewer?.dataSources) _viewer.dataSources.remove(_dataSource, true); _dataSource = null; @@ -618,9 +627,16 @@ export function createBhoteKoshiLocatorLayer({ _removeTriggerMoveEnd = null; _triggerCalloutStartedAt = now(); requestRender('bhote-koshi-trigger-record-reveal'); - const camera = _viewer?.camera; + flyTriggerStage(attempt, 'flying', TRIGGER_APPROACH_DURATION_SEC); + } + + function flyTriggerStage(attempt, phase, duration) { + const camera = cameraForScene(); if (!camera?.flyTo) return; - const generation = _generation; + const orbit = phase === 'orbiting'; + const pose = orbit ? TRIGGER_ORBIT_CAMERA : TRIGGER_APPROACH_CAMERA; + attempt.phase = phase; + attempt.endsAt = now() + duration * 1000; _triggerApproachInFlight = true; const finish = () => { if (_triggerApproachAttempt !== attempt) return; @@ -628,46 +644,18 @@ export function createBhoteKoshiLocatorLayer({ _triggerApproachInFlight = false; }; camera.flyTo({ - destination: Cesium.Cartesian3.fromDegrees( - TRIGGER_APPROACH_CAMERA.lon, - TRIGGER_APPROACH_CAMERA.lat, - TRIGGER_APPROACH_CAMERA.alt, - ), + destination: Cesium.Cartesian3.fromDegrees(pose.lon, pose.lat, pose.alt), orientation: { - heading: Cesium.Math.toRadians(TRIGGER_APPROACH_CAMERA.heading), - pitch: Cesium.Math.toRadians(TRIGGER_APPROACH_CAMERA.pitch), - roll: Cesium.Math.toRadians(TRIGGER_APPROACH_CAMERA.roll), + heading: Cesium.Math.toRadians(pose.heading), + pitch: Cesium.Math.toRadians(pose.pitch), + roll: Cesium.Math.toRadians(pose.roll), }, - duration: TRIGGER_APPROACH_DURATION_SEC, - easingFunction: Cesium.EasingFunction.CUBIC_IN_OUT, + duration, + easingFunction: orbit ? Cesium.EasingFunction.SINUSOIDAL_IN_OUT : Cesium.EasingFunction.CUBIC_IN_OUT, complete: () => { - if (_triggerApproachAttempt !== attempt || attempt.phase !== 'flying') - return; - if ( - !_enabled || - generation !== _generation || - _presentation !== BHOTE_KOSHI_TRIGGER_RECORD_PRESENTATION - ) { - finish(); - return; - } - attempt.phase = 'orbiting'; - camera.flyTo({ - destination: Cesium.Cartesian3.fromDegrees( - TRIGGER_ORBIT_CAMERA.lon, - TRIGGER_ORBIT_CAMERA.lat, - TRIGGER_ORBIT_CAMERA.alt, - ), - orientation: { - heading: Cesium.Math.toRadians(TRIGGER_ORBIT_CAMERA.heading), - pitch: Cesium.Math.toRadians(TRIGGER_ORBIT_CAMERA.pitch), - roll: Cesium.Math.toRadians(TRIGGER_ORBIT_CAMERA.roll), - }, - duration: TRIGGER_ORBIT_DURATION_SEC, - easingFunction: Cesium.EasingFunction.SINUSOIDAL_IN_OUT, - complete: finish, - cancel: finish, - }); + if (_triggerApproachAttempt !== attempt) return; + if (orbit || !_enabled || _presentation !== BHOTE_KOSHI_TRIGGER_RECORD_PRESENTATION) finish(); + else flyTriggerStage(attempt, 'orbiting', TRIGGER_ORBIT_DURATION_SEC); }, cancel: finish, }); @@ -681,7 +669,7 @@ export function createBhoteKoshiLocatorLayer({ const attempt = { phase: 'armed' }; _triggerApproachAttempt = attempt; const start = () => startTriggerApproach(attempt); - const camera = _viewer?.camera; + const camera = cameraForScene(); if ( !camera?.moveStart?.addEventListener || !camera?.moveEnd?.addEventListener @@ -1046,11 +1034,42 @@ export function createBhoteKoshiLocatorLayer({ requestRender('bhote-koshi-locator-presentation'); } - async function init(viewer) { + function connectDisplay() { + if (!_dataSource) return; + if (_viewer) _viewer.dataSources.add(_dataSource); + else if (_engine) _native = createNativeRenderer(_engine, _dataSource); + registerDynamicCredit(creditTarget(), BHOTE_KOSHI_LOCATOR_CREDIT); + showOsmCredit(creditTarget(), BHOTE_KOSHI_LOCATOR_LAYER_ID); + } + + function attachRenderer(viewer, {engine = null} = {}) { _viewer = viewer; + _engine = engine; + if (!_enabled) return true; + connectDisplay(); + publishOverlayEntries({moving: _placeRevealMoving}); + if (_pausedApproach) { + const {phase, remaining} = _pausedApproach; + _pausedApproach = null; + if (phase === 'armed') armTriggerApproach(); + else flyTriggerStage((_triggerApproachAttempt = {}), phase, remaining); + } return true; } + function detachRenderer() { + const phase = _triggerApproachAttempt?.phase; + if (phase && phase !== 'finished') { + _pausedApproach = {phase, remaining: Math.max(0, (_triggerApproachAttempt.endsAt - now()) / 1000)}; + clearTriggerApproach({preserveCallout: true}); + } + _native?.destroy(); + _native = null; + if (_viewer && _dataSource) _viewer.dataSources.remove(_dataSource, false); + hideOsmCredit(creditTarget(), BHOTE_KOSHI_LOCATOR_LAYER_ID); + _engine = _viewer = null; + } + async function enable(viewer, { signal } = {}) { _viewer = viewer; _enabled = true; @@ -1062,9 +1081,7 @@ export function createBhoteKoshiLocatorLayer({ const dataSource = new Cesium.CustomDataSource('bhote-koshi-locator'); _dataSource = dataSource; renderPresentation(); - viewer.dataSources.add(dataSource); - registerDynamicCredit(viewer, BHOTE_KOSHI_LOCATOR_CREDIT); - showOsmCredit(viewer, BHOTE_KOSHI_LOCATOR_LAYER_ID); + connectDisplay(); requestRender('bhote-koshi-locator-callout'); try { @@ -1100,7 +1117,8 @@ export function createBhoteKoshiLocatorLayer({ } async function disable() { - hideOsmCredit(_viewer, BHOTE_KOSHI_LOCATOR_LAYER_ID); + hideOsmCredit(creditTarget(), BHOTE_KOSHI_LOCATOR_LAYER_ID); + _pausedApproach = null; _enabled = false; _boundaryReady = false; _boundaryResolved = false; @@ -1113,7 +1131,7 @@ export function createBhoteKoshiLocatorLayer({ async function destroy() { await disable(); - _viewer = null; + _viewer = _engine = null; } return { @@ -1124,7 +1142,9 @@ export function createBhoteKoshiLocatorLayer({ icon: '◎', source: 'OpenStreetMap + GeoPera', updateInterval: 0, - init, + init: attachRenderer, + attachRenderer, + detachRenderer, enable, disable, update: async () => true, diff --git a/src/data/dataCredits.js b/src/data/dataCredits.js index 2094609dc..d51a8dc1a 100644 --- a/src/data/dataCredits.js +++ b/src/data/dataCredits.js @@ -645,8 +645,8 @@ export const BHOTE_KOSHI_LOCATOR_CREDIT = { '(CC BY-NC 4.0)', }; -/** @type {Set} Keys of dynamic credits already registered this session. */ -const _dynamicCreditKeys = new Set(); +/** Keys registered in each globe's attribution display. */ +const dynamicCreditKeys = new WeakMap(); /** * Register a conditional credit at the moment its data source activates. @@ -660,9 +660,11 @@ export function registerDynamicCredit(target, credit) { const credits = creditsOf(target); if (!credits) return false; if (!credit?.key || !credit?.html) return false; - if (_dynamicCreditKeys.has(credit.key)) return true; + let keys = dynamicCreditKeys.get(target); + if (!keys) dynamicCreditKeys.set(target, (keys = new Set())); + if (keys.has(credit.key)) return true; credits.addStatic(credit.html, { onScreen: false }); - _dynamicCreditKeys.add(credit.key); + keys.add(credit.key); return true; } diff --git a/src/data/dataCredits.test.mjs b/src/data/dataCredits.test.mjs index c520ce41b..09645e75e 100644 --- a/src/data/dataCredits.test.mjs +++ b/src/data/dataCredits.test.mjs @@ -147,6 +147,9 @@ test('data credits go through the globe engine credits', async () => { engine.fake.credits.filter(({ html }) => html === 'D').length, 1, ); + const other = createFakeEngine(); + registerDynamicCredit(other, { key: 'engine-dyn', html: 'D' }); + assert.equal(other.fake.credits.filter(({ html }) => html === 'D').length, 1); engine.fake.resetCalls(); assert.equal(showOsmCredit(engine, 'alpr', { openMapTiles: true }), true); const inline = engine.fake.credits.filter(({ onScreen }) => onScreen); diff --git a/src/globe/navara/index.js b/src/globe/navara/index.js index 49f7dd4d3..4b733ec45 100644 --- a/src/globe/navara/index.js +++ b/src/globe/navara/index.js @@ -3,6 +3,7 @@ // dynamic import() so Navara stays out of the Cesium-mode bundle. import ThreeView, { + Color as NavaraColor, MeshDesc, EffectDesc, Effect as NavaraEffect, @@ -205,6 +206,7 @@ export async function createNavaraGlobe({ return { view, engine, + NavaraColor, MeshDesc, EffectDesc, NavaraEffect, diff --git a/src/layers/shared/render/navaraStory.js b/src/layers/shared/render/navaraStory.js new file mode 100644 index 000000000..ba1b99eac --- /dev/null +++ b/src/layers/shared/render/navaraStory.js @@ -0,0 +1,173 @@ +import * as geo from '../../../geo/index.js'; + +const val = (p) => (p?.getValue ? p.getValue() : p); +const rgba = (c) => [c.red, c.green, c.blue, c.alpha]; +const tint = (c) => + (Math.round(c.red * 255) << 16) | + (Math.round(c.green * 255) << 8) | + Math.round(c.blue * 255); +function samePath(old, points) { + return ( + old?.length === points.length && + points.every( + (p, i) => p.x === old[i].x && p.y === old[i].y && p.z === old[i].z, + ) + ); +} +function circleAtlas() { + const canvas = document.createElement('canvas'); + canvas.width = 128; + canvas.height = 64; + const ctx = canvas.getContext('2d'); + ctx.fillStyle = 'white'; + ctx.beginPath(); + ctx.arc(32, 32, 29, 0, Math.PI * 2); + ctx.fill(); + ctx.lineWidth = 5; + ctx.strokeStyle = 'white'; + ctx.beginPath(); + ctx.arc(96, 32, 27, 0, Math.PI * 2); + ctx.stroke(); + return { canvas, cells: { fill: [0, 0, 0.5, 1], rim: [0.5, 0, 1, 1] } }; +} + +// The authored story owns the clock and geographic graphics. This adapter only samples them +// while its scene is attached. Neither a Cesium viewer nor an additional globe is needed. +export function drawStorySource(engine, { view, NavaraColor }, data) { + const entries = new Map(); + let atlas; + // Story graphics encode map colors. Compensate the planet's photographic + // exposure before its tone-mapping pass so a cyan overlay stays readable. + function displayColor(hex) { + const c = new NavaraColor().setHex(hex); + const exposure = Math.max(1, view.toneMappingExposure ?? 1); + const [r, g, b] = c.toArray(); + return c.setRGBLinear(r / exposure, g / exposure, b / exposure); + } + function point(entity) { + const p = entity.point, + nearFar = val(p.scaleByDistance), + depth = val(p.disableDepthTestDistance); + const set = engine.movers.createSet({ + id: data.name + '-' + entity.id, + kind: 'sprite', + capacity: 2, + pickable: false, + depthTest: depth === Infinity ? 'always' : 'normal', + sprite: { + sizePx: 64, + atlas: (atlas ??= circleAtlas()), + scaleByDistance: nearFar + ? [nearFar.near, nearFar.nearValue, nearFar.far, nearFar.farValue] + : null, + }, + }); + return { + paint(show) { + const position = val(entity.position); + if (!show || !position) { + set.setCount(0); + set.commit(); + return; + } + let at = position; + if (val(p.heightReference) === 1) { + const carto = geo.cartographic.fromCartesian(position); + const h = engine.surface.terrainHeight(carto); + if (Number.isFinite(h)) + at = geo.cartesian3.fromRadians(carto.longitude, carto.latitude, h); + } + const scale = Number(val(p.pixelSize)) / 58; + const color = val(p.color), + outline = val(p.outlineColor); + set.set(0, { + position: at, + show: true, + cell: 'fill', + color: rgba(color), + scale, + }); + const hasOutline = Boolean(outline && val(p.outlineWidth)); + if (hasOutline) + set.set(1, { + position: at, + show: true, + cell: 'rim', + color: rgba(outline), + scale, + }); + set.setCount(hasOutline ? 2 : 1); + set.commit(); + }, + destroy: () => set.destroy(), + }; + } + function geometry(entity) { + const key = entity.polyline ? 'polyline' : 'polygon', + g = entity[key]; + let handle, + previous = null, + visible = null; + return { + paint(show) { + const path = + key === 'polygon' ? val(g.hierarchy)?.positions : val(g.positions); + show = Boolean(show && path?.length >= (key === 'polygon' ? 3 : 2)); + if (!show) { + if (handle && visible) handle.update({ visible: false }); + visible = false; + return; + } + if (!handle || !samePath(previous, path)) { + const color = val(g.material)?.color ?? val(g.material?.color); + previous = path.map(({ x, y, z }) => ({ x, y, z })); + const geom = { + points: previous, + clampToGround: true, + color: displayColor(color ? tint(color) : 0x20e7f2), + opacity: color?.alpha ?? 1, + transparent: true, + ...(key === 'polyline' ? { width: val(g.width) ?? 2 } : {}), + }; + if (handle) handle.update({ [key]: geom, visible: true }); + else + handle = view.addMesh({ + [key]: geom, + visible: true, + lit: false, + pickable: false, + }); + } else if (!visible) handle.update({ visible: true }); + visible = true; + }, + destroy: () => handle?.delete(), + }; + } + function frame() { + const live = new Set(data.entities.values); + for (const [e, item] of entries) + if (!live.has(e)) { + item.destroy(); + entries.delete(e); + } + for (const e of live) { + let item = entries.get(e); + if (!item) { + if (!e.point && !e.polyline && !e.polygon) continue; + item = e.point ? point(e) : geometry(e); + entries.set(e, item); + } + item.paint(data.show !== false && val(e.show) !== false); + } + } + const off = engine.frames.preRender.addEventListener(frame); + engine.render.request(data.name); + return { + destroy() { + off(); + for (const item of entries.values()) item.destroy(); + entries.clear(); + engine.render.request(data.name); + }, + }; +} diff --git a/src/layers/submarineCables/render/navara.js b/src/layers/submarineCables/render/navara.js index a7d33d162..bc0a186f9 100644 --- a/src/layers/submarineCables/render/navara.js +++ b/src/layers/submarineCables/render/navara.js @@ -1,5 +1,4 @@ import * as geo from '../../../geo/index.js'; -import { Color } from 'three'; import { isPointerFree } from '../../../data/inputOwnership.js'; import { featureLabel, @@ -201,7 +200,7 @@ export function createNavaraCables(engine, native, host) { layer.on('featureUpdated', ({ evaluator }) => evaluator.evaluate(({ properties }) => { const css = properties?.color ?? BASE_CABLE_COLOR; - if (!colors.has(css)) colors.set(css, new Color(css)); + if (!colors.has(css)) colors.set(css, new native.NavaraColor().setStyle(css)); return { color: colors.get(css) }; }), );