@tangram.gl/tangram-layers 0.0.0-stage → 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -2
- package/dist/experimental/projected-basemaps.js +1310 -0
- package/dist/experimental/projected-basemaps.js.map +6 -0
- package/dist/experimental/terrain.js +379 -0
- package/dist/experimental/terrain.js.map +6 -0
- package/dist/experimental/webxr.js +1423 -0
- package/dist/experimental/webxr.js.map +6 -0
- package/dist/index.js +756 -0
- package/dist/index.js.map +6 -0
- package/dist/types/experimental/projected-basemaps.d.ts +120 -0
- package/dist/types/experimental/projected-detail.d.ts +33 -0
- package/dist/types/experimental/projected-navigation.d.ts +117 -0
- package/dist/types/experimental/terrain-display.d.ts +124 -0
- package/dist/types/experimental/terrain.d.ts +131 -0
- package/dist/types/first_person_view_adapter.d.ts +29 -0
- package/dist/types/globe_view_adapter.d.ts +33 -0
- package/dist/types/index.d.ts +84 -0
- package/dist/types/tangram-layer-lifecycle.d.ts +132 -0
- package/dist/types/tangram-layer.d.ts +157 -0
- package/dist/types/view_adapter_types.d.ts +42 -0
- package/dist/types/web_mercator_view_adapter.d.ts +77 -0
- package/package.json +80 -3
- package/src/experimental/projected-basemaps.ts +301 -0
- package/src/experimental/projected-detail.ts +116 -0
- package/src/experimental/projected-navigation.ts +323 -0
- package/src/experimental/terrain-display.ts +243 -0
- package/src/experimental/terrain.ts +240 -0
- package/src/experimental/webxr/grabbing.ts +166 -0
- package/src/experimental/webxr/index.d.ts +77 -0
- package/src/experimental/webxr/index.ts +72 -0
- package/src/experimental/webxr/interaction.ts +221 -0
- package/src/experimental/webxr/picking.ts +119 -0
- package/src/experimental/webxr/presentation.ts +457 -0
- package/src/experimental/webxr/projection.ts +371 -0
- package/src/experimental/webxr/types.ts +221 -0
- package/src/experimental/webxr/views.ts +190 -0
- package/src/first_person_view_adapter.ts +107 -0
- package/src/globe_view_adapter.ts +103 -0
- package/src/index.ts +64 -0
- package/src/tangram-layer-lifecycle.ts +130 -0
- package/src/tangram-layer.ts +679 -0
- package/src/view_adapter_types.ts +40 -0
- package/src/web_mercator_view_adapter.ts +185 -0
|
@@ -0,0 +1,679 @@
|
|
|
1
|
+
// tangram-layers
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import WebMercatorViewAdapter from './web_mercator_view_adapter.js';
|
|
6
|
+
import FirstPersonViewAdapter from './first_person_view_adapter.js';
|
|
7
|
+
import GlobeViewAdapter from './globe_view_adapter.js';
|
|
8
|
+
import type {GlobeViewAdapterOptions} from './globe_view_adapter.js';
|
|
9
|
+
import type {FirstPersonViewAdapterOptions} from './first_person_view_adapter.js';
|
|
10
|
+
import type {FirstPersonViewport, GlobeViewport} from './view_adapter_types.js';
|
|
11
|
+
import type {HostFrameOptions} from '@tangram.gl/tangram-renderer/core';
|
|
12
|
+
import type {ProjectionEngine, ProjectionExecutionOptions} from '@tangram.gl/tangram-renderer/core';
|
|
13
|
+
import type {LayerProps} from '@deck.gl/core';
|
|
14
|
+
import type {Scene} from '@tangram.gl/tangram-renderer';
|
|
15
|
+
import type {SceneDefinition, FeatureSelectionOptions, FeatureSelectionResult} from '@tangram.gl/tangram-renderer/core';
|
|
16
|
+
import type {TerrainSurface, TerrainSurfaceHit} from '@tangram.gl/tangram-renderer/core';
|
|
17
|
+
import type {RendererOptions, RenderOptions, HostTileResourceOptions} from '@tangram.gl/tangram-renderer/core';
|
|
18
|
+
import type {TangramRecord, PendingTangramRecord, TangramRendererFactory, TangramLayerViewport, TangramLayerContext, TangramBridgeScene} from './tangram-layer-lifecycle';
|
|
19
|
+
|
|
20
|
+
/** Tangram-specific properties in addition to ordinary deck.gl layer properties. */
|
|
21
|
+
export type TangramLayerProps = LayerProps & {
|
|
22
|
+
/** Inline scene or URL loaded by the renderer. */
|
|
23
|
+
scene: SceneDefinition;
|
|
24
|
+
/** Opt-in physical-height draping; host owns DEM acquisition and supplies immutable grids. */
|
|
25
|
+
terrain?: import('@tangram.gl/tangram-renderer/core').RendererOptions['terrain'];
|
|
26
|
+
/** Borrowed application admission pool, shared with terrain acquisition; changing it recreates the renderer. */
|
|
27
|
+
terrainResourceBudget?: import('@tangram.gl/tangram-renderer/core').RendererOptions['terrainResourceBudget'];
|
|
28
|
+
/** Visual terrain relief multiplier; changing it does not refetch or rebuild meshes. */
|
|
29
|
+
terrainExaggeration?: number;
|
|
30
|
+
/** Caller-owned CRS factory for CPU-projected basemaps; omitted keeps worker-local math.gl transforms. */
|
|
31
|
+
projectionEngine?: ProjectionEngine;
|
|
32
|
+
/** Immutable host kernel chunking policy; changing it recreates the scene, like changing the engine. */
|
|
33
|
+
projectionEngineExecution?: ProjectionExecutionOptions;
|
|
34
|
+
/** Base URL used by imported scenes and assets. */
|
|
35
|
+
sceneBasePath?: string | null;
|
|
36
|
+
/** Optional Nextzen source key injected on load. */
|
|
37
|
+
apiKey?: string | null;
|
|
38
|
+
/** Conservative globe visibility settings. */
|
|
39
|
+
globeMaxElevation?: number;
|
|
40
|
+
globeVisibleBounds?: readonly [number, number, number, number];
|
|
41
|
+
globePreloadZoom?: number;
|
|
42
|
+
/** Shared styled mesh-resource and decoded-acquisition limits. */
|
|
43
|
+
tileResources?: HostTileResourceOptions;
|
|
44
|
+
maxConcurrentTileLoadsPerWorker?: number | null;
|
|
45
|
+
/** Bounded first-person ground/slab footprint. */
|
|
46
|
+
firstPersonMaxGroundExtent?: number;
|
|
47
|
+
firstPersonElevationRange?: readonly [number, number];
|
|
48
|
+
/** Scene and attribution lifecycle callbacks, resolved by deck defaults. */
|
|
49
|
+
onSceneLoad?: (scene: Scene) => void;
|
|
50
|
+
onSceneError?: (error: Error, scene: Scene) => void;
|
|
51
|
+
onAttributionChange?: (credits: string[], scene: Scene) => void;
|
|
52
|
+
/** Optional CPU-projected subclass's existing callback and projection policy. */
|
|
53
|
+
projectedProjection?: import('@tangram.gl/tangram-renderer/core').ProjectedBasemapOptions;
|
|
54
|
+
onProjectionChange?: (projection: import('@tangram.gl/tangram-renderer/core').ProjectedBasemapOptions) => void;
|
|
55
|
+
/** Remaining existing Tangram bridge properties. */
|
|
56
|
+
[property: string]: unknown;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/** Opt-in host adapter, kept independent of the ordinary geographic view dispatch. */
|
|
60
|
+
export interface TangramHostViewAdapter {
|
|
61
|
+
/** Validate and adapt a host viewport before modifying renderer state. */
|
|
62
|
+
getFrame(viewport: unknown, properties: unknown, dimensions: {width: number; height: number}): HostFrameOptions;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const VIEW_EPSILON = 1e-7;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Injects an API key into every Nextzen source in a Tangram scene.
|
|
69
|
+
*
|
|
70
|
+
* @param {object} config Tangram scene configuration.
|
|
71
|
+
* @param {string|null|undefined} apiKey Nextzen API key.
|
|
72
|
+
* @returns {boolean} `true` when at least one source was updated.
|
|
73
|
+
*/
|
|
74
|
+
export function injectNextzenApiKey(config: {sources?: Record<string, {url?: unknown; url_params?: Record<string, unknown>} | null>} | null | undefined, apiKey?: string | null) {
|
|
75
|
+
if (!apiKey || !config || !config.sources) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let updated = false;
|
|
80
|
+
for (const source of Object.values(config.sources)) {
|
|
81
|
+
if (source && typeof source.url === 'string' && source.url.includes('nextzen.org')) {
|
|
82
|
+
source.url_params = source.url_params || {};
|
|
83
|
+
source.url_params.api_key = apiKey;
|
|
84
|
+
updated = true;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return updated;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Converts a deck.gl Web Mercator viewport into Tangram camera matrices.
|
|
92
|
+
*
|
|
93
|
+
* Tangram tile models use absolute EPSG:3857 meters while deck matrices consume
|
|
94
|
+
* zoom-zero common coordinates. Altitude remains in physical meters and uses
|
|
95
|
+
* deck's latitude-dependent distance scale.
|
|
96
|
+
*
|
|
97
|
+
* @param {object} viewport deck.gl WebMercatorViewport.
|
|
98
|
+
* @returns {{view: Float64Array, projection: Float32Array, position: number[]}}
|
|
99
|
+
*/
|
|
100
|
+
export function getExternalCameraFrame(viewport: Parameters<typeof WebMercatorViewAdapter.getCameraFrame>[0]): {view: Float64Array; projection: Float32Array; position: [number, number, number]} {
|
|
101
|
+
return WebMercatorViewAdapter.getCameraFrame(viewport);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Converts a finite first-person camera into a bounded flat-ground host frame. */
|
|
105
|
+
export function getFirstPersonViewFrame(viewport: FirstPersonViewport, options: FirstPersonViewAdapterOptions = {}) {
|
|
106
|
+
return FirstPersonViewAdapter.getFrame(viewport, options);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function getGlobeViewFrame(viewport: GlobeViewport, options: GlobeViewAdapterOptions = {}) {
|
|
110
|
+
return GlobeViewAdapter.getFrame(viewport, options);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Creates an experimental deck.gl layer class that renders Tangram through
|
|
115
|
+
* Tangram's luma.gl device renderer.
|
|
116
|
+
*
|
|
117
|
+
* The class is dependency-injected so this demo does not add deck.gl as a Tangram
|
|
118
|
+
* package dependency.
|
|
119
|
+
*
|
|
120
|
+
* @param {object} dependencies Bridge dependencies.
|
|
121
|
+
* @param {typeof import('@deck.gl/core').Layer} dependencies.Layer deck.gl Layer class.
|
|
122
|
+
* @param {object} dependencies.ClassicWebGLRenderer Embeddable Tangram renderer class.
|
|
123
|
+
* @param {object} dependencies.Renderer Legacy alias for ClassicWebGLRenderer.
|
|
124
|
+
* @returns {typeof import('@deck.gl/core').Layer} TangramLayer class.
|
|
125
|
+
*/
|
|
126
|
+
export function createTangramLayerClass({Layer, ClassicWebGLRenderer, Renderer}: {Layer: abstract new (...properties: Partial<TangramLayerProps>[]) => object; ClassicWebGLRenderer?: TangramRendererFactory; Renderer?: TangramRendererFactory}, viewAdapter?: TangramHostViewAdapter) {
|
|
127
|
+
const rendererClass = ClassicWebGLRenderer || Renderer;
|
|
128
|
+
if (!Layer || !rendererClass) {
|
|
129
|
+
throw new Error('createTangramLayerClass requires Layer and ClassicWebGLRenderer');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
class TangramLayer extends Layer {
|
|
133
|
+
/** Inherited host capabilities are supplied by the injected deck superclass. */
|
|
134
|
+
declare props: Partial<TangramLayerProps>;
|
|
135
|
+
declare context: TangramLayerContext;
|
|
136
|
+
declare state: {tangramRecord?: TangramRecord | null};
|
|
137
|
+
declare setState: (state: Record<string, unknown>) => void;
|
|
138
|
+
declare setNeedsRedraw: () => void;
|
|
139
|
+
declare raiseError: (error: Error, message: string) => void;
|
|
140
|
+
/** Existing deck metadata assigned below without changing constructor output. */
|
|
141
|
+
declare static layerName: string;
|
|
142
|
+
declare static defaultProps: Record<string, unknown>;
|
|
143
|
+
/** Forward deck.gl's property objects without changing its constructor semantics. */
|
|
144
|
+
constructor(...properties: Partial<TangramLayerProps>[]) {
|
|
145
|
+
super(...properties);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
initializeState(_context?: import('@deck.gl/core').LayerContext) {
|
|
149
|
+
this.setState({tangramRecord: null});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
updateState({props}: {props: Partial<TangramLayerProps>}) {
|
|
153
|
+
let record = this.state.tangramRecord;
|
|
154
|
+
const shouldCreateScene =
|
|
155
|
+
!record ||
|
|
156
|
+
record.sceneSource !== props.scene ||
|
|
157
|
+
record.sceneBasePath !== props.sceneBasePath ||
|
|
158
|
+
record.apiKey !== props.apiKey ||
|
|
159
|
+
record.projectionEngine !== (props.projectionEngine ?? undefined) ||
|
|
160
|
+
record.projectionEngineExecution !== (props.projectionEngineExecution ?? undefined) ||
|
|
161
|
+
Boolean(record.terrain) !== Boolean(props.terrain) ||
|
|
162
|
+
record.terrainResourceBudget !== (props.terrainResourceBudget ?? undefined) ||
|
|
163
|
+
record.maxConcurrentTileLoadsPerWorker !== (props.maxConcurrentTileLoadsPerWorker ?? undefined);
|
|
164
|
+
|
|
165
|
+
if (shouldCreateScene) {
|
|
166
|
+
this._disposeTangramRecord(record);
|
|
167
|
+
record = this._createTangramRecord(props);
|
|
168
|
+
this.setState({tangramRecord: record});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (record) {
|
|
172
|
+
record.owner = this;
|
|
173
|
+
if (record.terrain !== props.terrain) {
|
|
174
|
+
record.terrain = props.terrain;
|
|
175
|
+
const terrain = props.terrain;
|
|
176
|
+
const revision = record.terrainRevision = (record.terrainRevision || 0) + 1;
|
|
177
|
+
record.terrainPendingRevision = revision;
|
|
178
|
+
Promise.resolve(record.loadPromise).then(async () => {
|
|
179
|
+
try {
|
|
180
|
+
if (!record.disposed && revision === record.terrainRevision && terrain) {
|
|
181
|
+
await record.renderer.setTerrain(terrain);
|
|
182
|
+
if (!record.disposed && revision === record.terrainRevision) {
|
|
183
|
+
record.terrainPendingRevision = 0;
|
|
184
|
+
record.renderer.setTerrainExaggeration(record.owner.props.terrainExaggeration ?? 1);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
} finally {
|
|
188
|
+
if (record.terrainPendingRevision === revision) record.terrainPendingRevision = 0;
|
|
189
|
+
}
|
|
190
|
+
}).catch(error => {if (!record.disposed && revision === record.terrainRevision) this._raiseBridgeError(normalizeError(error));});
|
|
191
|
+
}
|
|
192
|
+
if (props.terrain && !record.terrainPendingRevision) {
|
|
193
|
+
try {record.renderer.setTerrainExaggeration(props.terrainExaggeration ?? 1);}
|
|
194
|
+
catch (error) {this._raiseBridgeError(normalizeError(error));}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
draw({renderPass}: {renderPass?: import('@luma.gl/core').RenderPass} = {}) {
|
|
200
|
+
const record = this.state.tangramRecord;
|
|
201
|
+
if (!record || record.disposed) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
record.owner = this;
|
|
206
|
+
this._synchronizeTangramScene(record);
|
|
207
|
+
if (!this._canRender(record, this.props)) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const renderTangram = () => {
|
|
212
|
+
const update_options: RenderOptions = {force: true};
|
|
213
|
+
renderPass = renderPass || this.context.renderPass;
|
|
214
|
+
if (renderPass) {
|
|
215
|
+
update_options.renderPass = renderPass;
|
|
216
|
+
}
|
|
217
|
+
const rendered = record.renderer.render(update_options);
|
|
218
|
+
if (
|
|
219
|
+
record.scene.config &&
|
|
220
|
+
record.scene.config.scene &&
|
|
221
|
+
record.scene.config.scene.animated === true
|
|
222
|
+
) {
|
|
223
|
+
// Keep host-driven scenes moving even while deck's view is idle.
|
|
224
|
+
// Tangram's active-style list can lag tile/style activation by a frame.
|
|
225
|
+
this.setNeedsRedraw();
|
|
226
|
+
}
|
|
227
|
+
if (rendered && record.gl) {
|
|
228
|
+
// Tangram's depth and stencil buffers are internal implementation
|
|
229
|
+
// details. Preserve its color output but leave a clean depth buffer
|
|
230
|
+
// for deck layers that follow this basemap.
|
|
231
|
+
record.gl.depthMask(true);
|
|
232
|
+
record.gl.clear(record.gl.DEPTH_BUFFER_BIT | record.gl.STENCIL_BUFFER_BIT);
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
if (record.device.type === 'webgl') {
|
|
237
|
+
record.scene.withWebGLContext!(renderTangram);
|
|
238
|
+
} else {
|
|
239
|
+
renderTangram();
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
finalizeState(_context?: import('@deck.gl/core').LayerContext) {
|
|
244
|
+
this._disposeTangramRecord(this.state && this.state.tangramRecord);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** Asynchronously query a view-local or canvas CSS pixel; the host must keep drawing. */
|
|
248
|
+
getFeatureAt(pixel: {x: number; y: number}, options: FeatureSelectionOptions = {}): Promise<FeatureSelectionResult | undefined> {
|
|
249
|
+
const record = this.state.tangramRecord;
|
|
250
|
+
if (!record || record.disposed || record.loadFailed) return Promise.resolve(undefined);
|
|
251
|
+
const pending = record.renderer.getFeatureAt(pixel, options);
|
|
252
|
+
record.owner.setNeedsRedraw();
|
|
253
|
+
return pending;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Intersect the actual rendered eye with a host-owned terrain mesh in content coordinates. */
|
|
257
|
+
getTerrainAt(pixel: {x: number; y: number}, surface?: TerrainSurface, options: FeatureSelectionOptions = {}): TerrainSurfaceHit | null {
|
|
258
|
+
const record = this.state && this.state.tangramRecord;
|
|
259
|
+
if (!record || record.disposed || record.loadFailed || !record.loaded) return null;
|
|
260
|
+
return record.renderer.getTerrainAt(pixel, surface, options);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Exact admitted ground triangles at the last frame, including temporal relief and globe projection. */
|
|
264
|
+
getDrapedTerrainSurface(): import('@tangram.gl/tangram-renderer/core').DrapedTerrainSurfaceSnapshot | null {
|
|
265
|
+
const record = this.state?.tangramRecord;
|
|
266
|
+
return record && !record.disposed && record.loaded && !record.loadFailed ? record.renderer.getDrapedTerrainSurface() : null;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** Resident worker preparation costs; GPU, source and scratch allocations are separate. */
|
|
270
|
+
getTerrainDrapingStatistics(): import('@tangram.gl/tangram-renderer/core').TerrainDrapingStatistics | undefined {
|
|
271
|
+
const record = this.state?.tangramRecord;
|
|
272
|
+
return record && !record.disposed ? record.renderer.getTerrainDrapingStatistics() : undefined;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
get isLoaded() {
|
|
276
|
+
const record = this.state && this.state.tangramRecord;
|
|
277
|
+
return Boolean(record && record.loaded && !record.loadFailed && !record.disposed);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
_createTangramRecord(props: Partial<TangramLayerProps>) {
|
|
281
|
+
if (!props.scene) {
|
|
282
|
+
this._raiseBridgeError(new Error('scene is required'));
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const deckCanvas = this.context.deck && this.context.deck.getCanvas();
|
|
287
|
+
const device = this.context.device;
|
|
288
|
+
if (!deckCanvas) {
|
|
289
|
+
this._raiseBridgeError(new Error('deck canvas is required'));
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
if (
|
|
293
|
+
!device ||
|
|
294
|
+
(device.type !== 'webgl' && device.type !== 'webgpu') ||
|
|
295
|
+
typeof device.createBuffer !== 'function' ||
|
|
296
|
+
typeof device.createShader !== 'function' ||
|
|
297
|
+
typeof device.createTexture !== 'function' ||
|
|
298
|
+
typeof device.createRenderPipeline !== 'function' ||
|
|
299
|
+
typeof device.createVertexArray !== 'function'
|
|
300
|
+
) {
|
|
301
|
+
this._raiseBridgeError(new Error('a deck.gl luma.gl Device is required'));
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
let gl: TangramRecord['gl'] = null;
|
|
306
|
+
if (device.type === 'webgl') {
|
|
307
|
+
gl = device.handle as TangramRecord['gl']; // Device type is WebGL; the existing handle/canvas guard follows.
|
|
308
|
+
if (
|
|
309
|
+
!gl ||
|
|
310
|
+
typeof device.pushState !== 'function' ||
|
|
311
|
+
typeof device.popState !== 'function'
|
|
312
|
+
) {
|
|
313
|
+
this._raiseBridgeError(new Error('a deck.gl WebGLDevice is required'));
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
if (gl.canvas !== deckCanvas) {
|
|
317
|
+
this._raiseBridgeError(
|
|
318
|
+
new Error('deck canvas and WebGLDevice handle must share a context')
|
|
319
|
+
);
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const record: PendingTangramRecord = {
|
|
325
|
+
owner: this,
|
|
326
|
+
renderer: null,
|
|
327
|
+
scene: null,
|
|
328
|
+
device,
|
|
329
|
+
gl,
|
|
330
|
+
deckCanvas,
|
|
331
|
+
sceneSource: props.scene,
|
|
332
|
+
sceneBasePath: props.sceneBasePath,
|
|
333
|
+
apiKey: props.apiKey,
|
|
334
|
+
projectionEngine: props.projectionEngine ?? undefined,
|
|
335
|
+
terrain: props.terrain,
|
|
336
|
+
terrainResourceBudget: props.terrainResourceBudget ?? undefined,
|
|
337
|
+
terrainPendingRevision: 0,
|
|
338
|
+
projectionEngineExecution: props.projectionEngineExecution ?? undefined,
|
|
339
|
+
maxConcurrentTileLoadsPerWorker: props.maxConcurrentTileLoadsPerWorker ?? undefined,
|
|
340
|
+
canvasWidth: null,
|
|
341
|
+
canvasHeight: null,
|
|
342
|
+
disposed: false,
|
|
343
|
+
destroyed: false,
|
|
344
|
+
loadSettled: false,
|
|
345
|
+
loadFailed: false,
|
|
346
|
+
loaded: false,
|
|
347
|
+
lastSceneError: null,
|
|
348
|
+
lastViewportError: null,
|
|
349
|
+
reportedViewportError: null,
|
|
350
|
+
loadPromise: null,
|
|
351
|
+
webglScopeDepth: 0
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
let renderer;
|
|
355
|
+
try {
|
|
356
|
+
const renderer_options: RendererOptions & {requestRedraw: () => void} = {
|
|
357
|
+
device,
|
|
358
|
+
canvas: deckCanvas,
|
|
359
|
+
projectionEngine: record.projectionEngine,
|
|
360
|
+
terrain: record.terrain,
|
|
361
|
+
terrainResourceBudget: record.terrainResourceBudget,
|
|
362
|
+
projectionEngineExecution: record.projectionEngineExecution,
|
|
363
|
+
maxConcurrentTileLoadsPerWorker: record.maxConcurrentTileLoadsPerWorker,
|
|
364
|
+
requestRedraw: () => {
|
|
365
|
+
if (!record.disposed && record.owner.setNeedsRedraw) {
|
|
366
|
+
record.owner.setNeedsRedraw();
|
|
367
|
+
}
|
|
368
|
+
},
|
|
369
|
+
continuousZoom: true,
|
|
370
|
+
highDensityDisplay: true,
|
|
371
|
+
logLevel: 'warn'
|
|
372
|
+
};
|
|
373
|
+
if (gl) {
|
|
374
|
+
renderer_options.webGLContext = gl;
|
|
375
|
+
renderer_options.webGLContextScope = (callback: () => unknown) =>
|
|
376
|
+
this._withDeviceState(record, callback);
|
|
377
|
+
}
|
|
378
|
+
renderer = rendererClass!.create!(props.scene, renderer_options);
|
|
379
|
+
} catch (error) {
|
|
380
|
+
this._raiseBridgeError(normalizeError(error));
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
383
|
+
record.renderer = renderer;
|
|
384
|
+
record.scene = renderer.scene;
|
|
385
|
+
// These two factory-assigned fields establish the live phase; the callback
|
|
386
|
+
// owner remains the exact same object created before factory invocation.
|
|
387
|
+
const liveRecord = record as TangramRecord;
|
|
388
|
+
|
|
389
|
+
renderer.subscribe({
|
|
390
|
+
load: (message) => {
|
|
391
|
+
injectNextzenApiKey(message.config, record.apiKey);
|
|
392
|
+
},
|
|
393
|
+
update: () => this._updateAttributions(liveRecord),
|
|
394
|
+
error: (message) =>
|
|
395
|
+
this._reportSceneError(liveRecord, normalizeError(message), message.type !== 'scene_import')
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
this._synchronizeTangramScene(liveRecord);
|
|
399
|
+
record.loadPromise = Promise.resolve()
|
|
400
|
+
.then(() => {
|
|
401
|
+
if (record.disposed) {
|
|
402
|
+
return null;
|
|
403
|
+
}
|
|
404
|
+
return renderer.load(props.scene!, { // Creation already rejected missing scene input; this closure preserves that identity.
|
|
405
|
+
base_path: props.sceneBasePath as string | undefined,
|
|
406
|
+
blocking: false
|
|
407
|
+
});
|
|
408
|
+
})
|
|
409
|
+
.then((result) => {
|
|
410
|
+
if (!record.disposed) {
|
|
411
|
+
record.loaded = true;
|
|
412
|
+
record.owner.setNeedsRedraw && record.owner.setNeedsRedraw();
|
|
413
|
+
record.owner.props.onSceneLoad!(liveRecord.scene);
|
|
414
|
+
this._updateAttributions(liveRecord);
|
|
415
|
+
}
|
|
416
|
+
return result;
|
|
417
|
+
})
|
|
418
|
+
.catch((error) => {
|
|
419
|
+
if (!record.disposed) {
|
|
420
|
+
record.loadFailed = true;
|
|
421
|
+
this._reportSceneError(liveRecord, normalizeError(error));
|
|
422
|
+
}
|
|
423
|
+
})
|
|
424
|
+
.finally(() => {
|
|
425
|
+
record.loadSettled = true;
|
|
426
|
+
if (record.disposed) {
|
|
427
|
+
this._destroyTangramRecord(liveRecord);
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
return liveRecord;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/** Refresh credits after source recreation; ignore stale metadata and disposed layers. */
|
|
435
|
+
_updateAttributions(record: TangramRecord) {
|
|
436
|
+
if (record.disposed || typeof record.renderer.getAttributions !== 'function') return;
|
|
437
|
+
const generation = record.attributionGeneration = (record.attributionGeneration || 0) + 1;
|
|
438
|
+
Promise.resolve().then(() => record.disposed ? undefined : record.renderer.getAttributions()).then(credits => {
|
|
439
|
+
if (credits && !record.disposed && generation === record.attributionGeneration) {
|
|
440
|
+
record.owner.props.onAttributionChange?.(credits, record.scene);
|
|
441
|
+
}
|
|
442
|
+
}).catch(error => {
|
|
443
|
+
if (!record.disposed && generation === record.attributionGeneration) {
|
|
444
|
+
this._reportSceneError(record, normalizeError(error), false);
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
_synchronizeTangramScene(record: TangramRecord) {
|
|
450
|
+
const viewport = this.context.viewport;
|
|
451
|
+
const viewports = this.context.deck!.getViewports
|
|
452
|
+
? this.context.deck!.getViewports()
|
|
453
|
+
: [viewport];
|
|
454
|
+
if (viewAdapter) {
|
|
455
|
+
try {
|
|
456
|
+
if (viewports.length !== 1) throw new Error('only one deck.gl viewport is supported');
|
|
457
|
+
const width = record.deckCanvas.clientWidth || viewport.width;
|
|
458
|
+
const height = record.deckCanvas.clientHeight || viewport.height;
|
|
459
|
+
const frame = viewAdapter.getFrame(viewport, this.props, {width, height});
|
|
460
|
+
record.renderer.setFrame({...frame, tileResources: this.props.tileResources ?? undefined});
|
|
461
|
+
record.lastViewportError = null;
|
|
462
|
+
record.reportedViewportError = null;
|
|
463
|
+
} catch (error) {
|
|
464
|
+
record.lastViewportError = (error as Error).message;
|
|
465
|
+
this._raiseViewportError(record, normalizeError(error));
|
|
466
|
+
}
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
const globeOptions = {
|
|
470
|
+
maxElevation: this.props.globeMaxElevation ?? undefined,
|
|
471
|
+
visibleBounds: this.props.globeVisibleBounds ?? undefined,
|
|
472
|
+
preloadZoom: this.props.globePreloadZoom ?? undefined
|
|
473
|
+
};
|
|
474
|
+
const firstPersonOptions = {maxGroundExtent: this.props.firstPersonMaxGroundExtent ?? undefined,
|
|
475
|
+
elevationRange: this.props.firstPersonElevationRange ?? undefined};
|
|
476
|
+
const viewportError = validateViewport(viewport, viewports, globeOptions, firstPersonOptions);
|
|
477
|
+
|
|
478
|
+
if (viewportError) {
|
|
479
|
+
record.lastViewportError = viewportError.message;
|
|
480
|
+
this._raiseViewportError(record, viewportError);
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
record.lastViewportError = null;
|
|
485
|
+
record.reportedViewportError = null;
|
|
486
|
+
const width = record.deckCanvas.clientWidth || viewport.width;
|
|
487
|
+
const height = record.deckCanvas.clientHeight || viewport.height;
|
|
488
|
+
|
|
489
|
+
record.canvasWidth = width;
|
|
490
|
+
record.canvasHeight = height;
|
|
491
|
+
const frame = isGlobeViewport(viewport)
|
|
492
|
+
? {...getGlobeViewFrame(viewport, globeOptions), viewport: {width, height}}
|
|
493
|
+
: isFirstPersonViewport(viewport)
|
|
494
|
+
? getFirstPersonViewFrame(viewport, {width, height, ...firstPersonOptions})
|
|
495
|
+
: getMapViewFrame(viewport, {width, height});
|
|
496
|
+
record.renderer.setFrame({...frame, tileResources: this.props.tileResources ?? undefined});
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
_canRender(record: TangramRecord, props: Partial<TangramLayerProps>) {
|
|
500
|
+
return (
|
|
501
|
+
props.visible !== false &&
|
|
502
|
+
props.opacity !== 0 &&
|
|
503
|
+
record.loaded &&
|
|
504
|
+
!record.loadFailed &&
|
|
505
|
+
!record.lastViewportError
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
_raiseViewportError(record: TangramRecord, error: Error) {
|
|
510
|
+
if (record.reportedViewportError === error.message) {
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
record.reportedViewportError = error.message;
|
|
514
|
+
this._raiseBridgeError(error);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
_reportSceneError(record: TangramRecord, error: Error, isFatal = true) {
|
|
518
|
+
if (record.disposed || record.lastSceneError === error.message) {
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
record.lastSceneError = error.message;
|
|
522
|
+
if (isFatal) {
|
|
523
|
+
record.loadFailed = true;
|
|
524
|
+
}
|
|
525
|
+
record.owner.props.onSceneError!(error, record.scene);
|
|
526
|
+
record.owner.raiseError(error, 'TangramLayer scene');
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
_raiseBridgeError(error: Error) {
|
|
530
|
+
this.raiseError(error, 'TangramLayer bridge');
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
_disposeTangramRecord(record: TangramRecord | null | undefined) {
|
|
534
|
+
if (!record || record.disposed) {
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
record.disposed = true;
|
|
538
|
+
if (!record.loadPromise || record.loadSettled) {
|
|
539
|
+
this._destroyTangramRecord(record);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
_destroyTangramRecord(record: TangramRecord) {
|
|
544
|
+
if (!record.destroyed) {
|
|
545
|
+
record.destroyed = true;
|
|
546
|
+
record.renderer.destroy();
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
_withDeviceState<Value>(record: PendingTangramRecord, callback: () => Value): Value {
|
|
551
|
+
if (record.webglScopeDepth > 0) {
|
|
552
|
+
return callback();
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const {device, gl} = record;
|
|
556
|
+
const lumaState = gl!.lumaState;
|
|
557
|
+
const hasTrackedProgram = Boolean(lumaState && 'program' in lumaState);
|
|
558
|
+
const previousProgram = hasTrackedProgram
|
|
559
|
+
? lumaState!.program
|
|
560
|
+
: gl!.getParameter(gl!.CURRENT_PROGRAM);
|
|
561
|
+
|
|
562
|
+
record.webglScopeDepth++;
|
|
563
|
+
device.pushState!();
|
|
564
|
+
try {
|
|
565
|
+
return callback();
|
|
566
|
+
} finally {
|
|
567
|
+
try {
|
|
568
|
+
device.popState!();
|
|
569
|
+
gl!.useProgram(previousProgram);
|
|
570
|
+
} finally {
|
|
571
|
+
record.webglScopeDepth--;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
TangramLayer.layerName = 'TangramLayer';
|
|
578
|
+
TangramLayer.defaultProps = {
|
|
579
|
+
projectionEngine: null,
|
|
580
|
+
projectionEngineExecution: null,
|
|
581
|
+
scene: null,
|
|
582
|
+
sceneBasePath: null,
|
|
583
|
+
apiKey: null,
|
|
584
|
+
globeMaxElevation: null,
|
|
585
|
+
globeVisibleBounds: null,
|
|
586
|
+
globePreloadZoom: null,
|
|
587
|
+
tileResources: null,
|
|
588
|
+
maxConcurrentTileLoadsPerWorker: null,
|
|
589
|
+
firstPersonMaxGroundExtent: 20000,
|
|
590
|
+
terrain: {type: 'object', value: null, compare: false},
|
|
591
|
+
terrainResourceBudget: {type: 'object', value: null, compare: false},
|
|
592
|
+
terrainExaggeration: {type: 'number', value: 1, min: 0, max: 1000},
|
|
593
|
+
firstPersonElevationRange: {type: 'array', value: [0, 0], compare: true},
|
|
594
|
+
onSceneLoad: () => {},
|
|
595
|
+
onAttributionChange: () => {},
|
|
596
|
+
onSceneError: () => {}
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
return TangramLayer;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function validateViewport(viewport: TangramLayerViewport, viewports: readonly TangramLayerViewport[], globeOptions: GlobeViewAdapterOptions = {}, firstPersonOptions: FirstPersonViewAdapterOptions = {}): Error | null {
|
|
603
|
+
if (viewports.length !== 1) {
|
|
604
|
+
return new Error('only one deck.gl viewport is supported');
|
|
605
|
+
}
|
|
606
|
+
const globe = isGlobeViewport(viewport);
|
|
607
|
+
const firstPerson = isFirstPersonViewport(viewport);
|
|
608
|
+
const geographicAnchorError = WebMercatorViewAdapter.validateGeographicAnchor(
|
|
609
|
+
viewport,
|
|
610
|
+
globe ? 'GlobeViewport' : 'Web Mercator viewport'
|
|
611
|
+
);
|
|
612
|
+
if (geographicAnchorError) {
|
|
613
|
+
return globe ? geographicAnchorError : new Error('a Web Mercator viewport is required');
|
|
614
|
+
}
|
|
615
|
+
// deck.gl uses both WEB_MERCATOR (1) and its high-zoom
|
|
616
|
+
// WEB_MERCATOR_AUTO_OFFSET (4) internally. The numeric projection mode is
|
|
617
|
+
// not part of the public viewport contract, so validate the public
|
|
618
|
+
// geospatial capability instead of rejecting high-zoom MapView instances.
|
|
619
|
+
if (!globe) {
|
|
620
|
+
const webMercatorError = WebMercatorViewAdapter.validateViewport(viewport);
|
|
621
|
+
if (webMercatorError) {
|
|
622
|
+
return webMercatorError;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
if (
|
|
626
|
+
!Number.isFinite(viewport.bearing || 0) ||
|
|
627
|
+
!Number.isFinite(viewport.pitch || 0) ||
|
|
628
|
+
(viewport.pitch || 0) < (firstPerson ? -90 : -VIEW_EPSILON) ||
|
|
629
|
+
(firstPerson ? (viewport.pitch || 0) > 90 : (viewport.pitch || 0) >= 90)
|
|
630
|
+
) {
|
|
631
|
+
return new Error('bearing and pitch must describe a finite deck.gl camera');
|
|
632
|
+
}
|
|
633
|
+
try {
|
|
634
|
+
if (globe) {
|
|
635
|
+
getGlobeViewFrame(viewport, globeOptions);
|
|
636
|
+
} else if (isFirstPersonViewport(viewport)) {
|
|
637
|
+
getFirstPersonViewFrame(viewport, firstPersonOptions);
|
|
638
|
+
} else {
|
|
639
|
+
WebMercatorViewAdapter.getCameraFrame(viewport);
|
|
640
|
+
}
|
|
641
|
+
} catch (error) {
|
|
642
|
+
return error as Error;
|
|
643
|
+
}
|
|
644
|
+
return null;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function getMapViewFrame(viewport: FirstPersonViewport, {width, height}: {width: number; height: number}) {
|
|
648
|
+
return WebMercatorViewAdapter.getFrame(viewport, {width, height});
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function isFirstPersonViewport(viewport: TangramLayerViewport): boolean {
|
|
652
|
+
return Boolean(
|
|
653
|
+
viewport && viewport.constructor && viewport.constructor.displayName === 'FirstPersonViewport'
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function isGlobeViewport(viewport: TangramLayerViewport): viewport is TangramLayerViewport & GlobeViewport {
|
|
658
|
+
return Boolean(
|
|
659
|
+
viewport &&
|
|
660
|
+
(viewport.constructor?.displayName === 'GlobeViewport' ||
|
|
661
|
+
viewport.constructor?.name === 'GlobeViewport')
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function normalizeError(value: unknown): Error {
|
|
666
|
+
const fields = value as {error?: unknown; message?: string} | null | undefined;
|
|
667
|
+
if (value instanceof Error) {
|
|
668
|
+
return value;
|
|
669
|
+
}
|
|
670
|
+
if (value && fields!.error instanceof Error) {
|
|
671
|
+
return fields!.error;
|
|
672
|
+
}
|
|
673
|
+
if (value && fields!.message) {
|
|
674
|
+
return new Error(fields!.message);
|
|
675
|
+
}
|
|
676
|
+
return new Error(String(value));
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
export default createTangramLayerClass;
|