mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 03:14:00 +00:00
Retain the Nepal locator presentation and draw it on either globe
This commit is contained in:
1 parent
605b9490f3
commit
811bcaff85
11 files changed
+409
-57
No files matched your search
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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],
|
||||
|
||||
@@ -43,6 +43,7 @@ test('the Navara mode registers migrated renderers once', async () => {
|
||||
.available()
|
||||
.map(({ id }) => id),
|
||||
[
|
||||
'bhote-koshi-locator',
|
||||
'flights',
|
||||
'military',
|
||||
'local-adsb',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -645,8 +645,8 @@ export const BHOTE_KOSHI_LOCATOR_CREDIT = {
|
||||
'(CC BY-NC 4.0)',
|
||||
};
|
||||
|
||||
/** @type {Set<string>} 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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) };
|
||||
}),
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user