@tangram.gl/tangram-renderer 0.0.0-stage → 0.0.0
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 +59 -2
- package/dist/core.js +36923 -0
- package/dist/core.js.map +6 -0
- package/dist/index.js +32 -0
- package/dist/loaders-gl-worker.js +82 -0
- package/dist/loaders-gl-worker.js.map +6 -0
- package/dist/map-logic.js +2 -0
- package/dist/map-logic.js.map +6 -0
- package/dist/projected-basemaps-worker.js +3 -0
- package/dist/projected-basemaps-worker.js.map +6 -0
- package/dist/shader-modules.js +141 -0
- package/dist/shader-modules.js.map +6 -0
- package/dist/style-schema.js +126 -0
- package/dist/style-schema.js.map +6 -0
- package/dist/tangram-style.schema.json +882 -0
- package/dist/tangram.debug.mjs +37139 -0
- package/dist/tangram.debug.mjs.map +6 -0
- package/dist/tangram.min.mjs +4950 -0
- package/dist/types/builders/earcut.d.ts +15 -0
- package/dist/types/core.d.ts +29 -0
- package/dist/types/gl/constants.d.ts +2 -0
- package/dist/types/gl/globe_mesh.d.ts +48 -0
- package/dist/types/gl/glsl.d.ts +17 -0
- package/dist/types/gl/vertex-constants.d.ts +4 -0
- package/dist/types/gl/vertex-types.d.ts +34 -0
- package/dist/types/gl/vertex_data.d.ts +32 -0
- package/dist/types/gl/vertex_elements.d.ts +8 -0
- package/dist/types/gl/vertex_layout.d.ts +32 -0
- package/dist/types/glsl.d.ts +14 -0
- package/dist/types/gpu/luma_device_renderer.d.ts +66 -0
- package/dist/types/gpu/resource_lifecycle.d.ts +4 -0
- package/dist/types/gpu/tangram_gpu_backend.d.ts +151 -0
- package/dist/types/index.d.ts +155 -0
- package/dist/types/lights/light-definitions.d.ts +121 -0
- package/dist/types/map-logic/draped-terrain-bounds.d.ts +41 -0
- package/dist/types/map-logic/globe-terrain-preparation.d.ts +85 -0
- package/dist/types/map-logic/globe-terrain.d.ts +54 -0
- package/dist/types/map-logic/index.d.ts +33 -0
- package/dist/types/map-logic/label-identity.d.ts +22 -0
- package/dist/types/map-logic/label-placement.d.ts +32 -0
- package/dist/types/map-logic/screen-bounds.d.ts +15 -0
- package/dist/types/map-logic/terrain-data.d.ts +55 -0
- package/dist/types/map-logic/terrain-error.d.ts +51 -0
- package/dist/types/map-logic/terrain-ground-topology.d.ts +26 -0
- package/dist/types/map-logic/terrain-height-field.d.ts +59 -0
- package/dist/types/map-logic/terrain-navigation.d.ts +27 -0
- package/dist/types/map-logic/terrain-neighborhood.d.ts +5 -0
- package/dist/types/map-logic/terrain-query.d.ts +33 -0
- package/dist/types/map-logic/terrain-resource-budget.d.ts +51 -0
- package/dist/types/map-logic/terrain-transition-topology.d.ts +23 -0
- package/dist/types/map-logic/tile-build-queue.d.ts +51 -0
- package/dist/types/map-logic/tile-cache-policy.d.ts +46 -0
- package/dist/types/map-logic/tile-residency.d.ts +17 -0
- package/dist/types/map-logic/triangle-query-index.d.ts +87 -0
- package/dist/types/procedures/js-yaml.d.ts +15 -0
- package/dist/types/procedures/mesh-projector.d.ts +107 -0
- package/dist/types/procedures/mvt-dependencies.d.ts +20 -0
- package/dist/types/procedures/projected-coordinate-transform.d.ts +27 -0
- package/dist/types/procedures/projection-batch-executor.d.ts +53 -0
- package/dist/types/procedures/scene-yaml-legacy.d.ts +6 -0
- package/dist/types/procedures/terrain-draping.d.ts +165 -0
- package/dist/types/procedures/terrain-globe.d.ts +6 -0
- package/dist/types/procedures/terrain-styled-topology.d.ts +34 -0
- package/dist/types/procedures/web-mercator-legacy.d.ts +4 -0
- package/dist/types/procedures/web-mercator-math.d.ts +4 -0
- package/dist/types/scene/globals.d.ts +11 -0
- package/dist/types/scene/ground_footprint.d.ts +16 -0
- package/dist/types/scene/host_frame.d.ts +54 -0
- package/dist/types/scene/projection_math.d.ts +52 -0
- package/dist/types/scene/scene-resource-types.d.ts +177 -0
- package/dist/types/scene/scene_bundle.d.ts +69 -0
- package/dist/types/scene/visibility_adapter.d.ts +106 -0
- package/dist/types/selection/draped-terrain-surface.d.ts +78 -0
- package/dist/types/selection/terrain_surface.d.ts +149 -0
- package/dist/types/shader-modules/globe-horizon.d.ts +15 -0
- package/dist/types/shader-modules/height-decode.d.ts +13 -0
- package/dist/types/shader-modules/hillshade.d.ts +13 -0
- package/dist/types/shader-modules/index.d.ts +6 -0
- package/dist/types/shader-modules/planar.d.ts +12 -0
- package/dist/types/shader-modules/sphere-map.d.ts +13 -0
- package/dist/types/shader-modules/triplanar.d.ts +15 -0
- package/dist/types/sources/decoded_tile_store.d.ts +95 -0
- package/dist/types/sources/tile_source_metadata.d.ts +32 -0
- package/dist/types/styles/text/fontfaceobserver.d.ts +10 -0
- package/dist/types/tile/tile_id.d.ts +37 -0
- package/dist/types/tile/tile_traversal_adapter.d.ts +63 -0
- package/dist/types/types.d.ts +293 -0
- package/dist/types/utils/geo.d.ts +57 -0
- package/dist/types/utils/hash.d.ts +1 -0
- package/dist/types/utils/log.d.ts +27 -0
- package/dist/types/utils/props.d.ts +6 -0
- package/dist/types/utils/subscribe.d.ts +10 -0
- package/dist/types/utils/thread.d.ts +6 -0
- package/dist/types/utils/urls.d.ts +26 -0
- package/dist/types/utils/utils.d.ts +26 -0
- package/dist/types/utils/version.d.ts +2 -0
- package/dist/types/utils/worker-types.d.ts +68 -0
- package/dist/types/utils/worker_broker.d.ts +3 -0
- package/package.json +103 -3
- package/src/builders/common.ts +41 -0
- package/src/builders/earcut.d.ts +15 -0
- package/src/builders/geometry-types.ts +60 -0
- package/src/builders/points.ts +115 -0
- package/src/builders/polygons.ts +225 -0
- package/src/builders/polylines.ts +682 -0
- package/src/builders/wireframe.ts +31 -0
- package/src/core.d.ts +29 -0
- package/src/core.ts +33 -0
- package/src/experimental/loaders-gl-worker.ts +19 -0
- package/src/experimental/projected-basemaps-worker.ts +13 -0
- package/src/experimental/projected-mesh-cache.ts +137 -0
- package/src/experimental/projected-mesh-domain.ts +110 -0
- package/src/experimental/projected-mesh-refinement.ts +193 -0
- package/src/experimental/projected-mesh.ts +399 -0
- package/src/experimental/projected-normal.ts +26 -0
- package/src/experimental/projected-symbol-mesh.ts +74 -0
- package/src/gl/constants.ts +18 -0
- package/src/gl/context.ts +103 -0
- package/src/gl/extensions.ts +30 -0
- package/src/gl/globe_mesh.ts +285 -0
- package/src/gl/glsl.ts +257 -0
- package/src/gl/mesh-types.ts +56 -0
- package/src/gl/render_state.ts +111 -0
- package/src/gl/shader_program.ts +1310 -0
- package/src/gl/texture.ts +814 -0
- package/src/gl/uniform_buffer.ts +340 -0
- package/src/gl/vao.ts +154 -0
- package/src/gl/vbo_mesh.ts +349 -0
- package/src/gl/vertex-constants.ts +19 -0
- package/src/gl/vertex-types.ts +35 -0
- package/src/gl/vertex_data.ts +126 -0
- package/src/gl/vertex_elements.ts +54 -0
- package/src/gl/vertex_layout.ts +241 -0
- package/src/glsl.d.ts +14 -0
- package/src/gpu/luma_device_renderer.ts +579 -0
- package/src/gpu/pixel_readback.ts +51 -0
- package/src/gpu/resource_lifecycle.ts +32 -0
- package/src/gpu/tangram_gpu_backend.ts +195 -0
- package/src/index.d.ts +155 -0
- package/src/index.ts +105 -0
- package/src/labels/collision-types.ts +94 -0
- package/src/labels/collision.ts +273 -0
- package/src/labels/collision_grid.ts +61 -0
- package/src/labels/intersect.ts +30 -0
- package/src/labels/label-types.ts +91 -0
- package/src/labels/label.ts +207 -0
- package/src/labels/label_line.ts +801 -0
- package/src/labels/label_point.ts +131 -0
- package/src/labels/main-pass-types.ts +73 -0
- package/src/labels/main_pass.ts +221 -0
- package/src/labels/point_anchor.ts +100 -0
- package/src/labels/point_placement.ts +150 -0
- package/src/labels/projected-pass.ts +259 -0
- package/src/labels/repeat_group.ts +72 -0
- package/src/lights/ambient_light.glsl +18 -0
- package/src/lights/directional_light.glsl +39 -0
- package/src/lights/geographic-lights.ts +21 -0
- package/src/lights/light-definitions.ts +285 -0
- package/src/lights/light.ts +590 -0
- package/src/lights/lighting-uniforms.ts +80 -0
- package/src/lights/lighting-wgsl.ts +116 -0
- package/src/lights/material.glsl +237 -0
- package/src/lights/material.ts +145 -0
- package/src/lights/native-falloff.ts +37 -0
- package/src/lights/point_light.glsl +96 -0
- package/src/lights/spot_light.glsl +111 -0
- package/src/map-logic/draped-terrain-bounds.ts +71 -0
- package/src/map-logic/globe-terrain-preparation.ts +217 -0
- package/src/map-logic/globe-terrain.ts +296 -0
- package/src/map-logic/index.ts +37 -0
- package/src/map-logic/label-identity.ts +36 -0
- package/src/map-logic/label-placement.ts +117 -0
- package/src/map-logic/screen-bounds.ts +34 -0
- package/src/map-logic/terrain-data.ts +107 -0
- package/src/map-logic/terrain-error.ts +134 -0
- package/src/map-logic/terrain-ground-topology.ts +164 -0
- package/src/map-logic/terrain-height-field.ts +254 -0
- package/src/map-logic/terrain-navigation.ts +50 -0
- package/src/map-logic/terrain-neighborhood.ts +43 -0
- package/src/map-logic/terrain-query.ts +51 -0
- package/src/map-logic/terrain-resource-budget.ts +124 -0
- package/src/map-logic/terrain-transition-topology.ts +164 -0
- package/src/map-logic/tile-build-queue.ts +110 -0
- package/src/map-logic/tile-cache-policy.ts +89 -0
- package/src/map-logic/tile-residency.ts +50 -0
- package/src/map-logic/triangle-query-index.ts +265 -0
- package/src/procedures/js-yaml.d.ts +15 -0
- package/src/procedures/mesh-projector.ts +191 -0
- package/src/procedures/mlt-loaders.ts +55 -0
- package/src/procedures/mvt-dependencies.d.ts +20 -0
- package/src/procedures/mvt-legacy.ts +150 -0
- package/src/procedures/mvt-loaders.ts +53 -0
- package/src/procedures/mvt-parser.ts +48 -0
- package/src/procedures/mvt-properties.ts +36 -0
- package/src/procedures/mvt-tile-provider.ts +72 -0
- package/src/procedures/pmtiles-loader.ts +75 -0
- package/src/procedures/projected-coordinate-transform.ts +79 -0
- package/src/procedures/projection-batch-executor.ts +140 -0
- package/src/procedures/scene-yaml-legacy.ts +16 -0
- package/src/procedures/scene-yaml-loaders.ts +10 -0
- package/src/procedures/terrain-draping.ts +713 -0
- package/src/procedures/terrain-globe.ts +32 -0
- package/src/procedures/terrain-styled-topology.ts +184 -0
- package/src/procedures/web-mercator-legacy.ts +24 -0
- package/src/procedures/web-mercator-math.ts +50 -0
- package/src/scene/camera.ts +298 -0
- package/src/scene/camera_base.ts +116 -0
- package/src/scene/classic_scene.ts +20 -0
- package/src/scene/external_camera.ts +101 -0
- package/src/scene/globals.ts +120 -0
- package/src/scene/ground_footprint.ts +139 -0
- package/src/scene/host_frame.ts +326 -0
- package/src/scene/projected_tile_lod.ts +125 -0
- package/src/scene/projection_math.ts +142 -0
- package/src/scene/projection_shaders.ts +183 -0
- package/src/scene/renderer.ts +516 -0
- package/src/scene/scene-resource-types.ts +174 -0
- package/src/scene/scene.ts +2378 -0
- package/src/scene/scene_bundle.ts +304 -0
- package/src/scene/scene_debug.ts +129 -0
- package/src/scene/scene_loader.ts +402 -0
- package/src/scene/scene_worker.ts +528 -0
- package/src/scene/view.ts +683 -0
- package/src/scene/visibility_adapter.ts +319 -0
- package/src/scene/worker_url.ts +15 -0
- package/src/selection/draped-terrain-surface.ts +377 -0
- package/src/selection/selection-types.ts +79 -0
- package/src/selection/selection.ts +542 -0
- package/src/selection/selection_fragment.glsl +18 -0
- package/src/selection/selection_globals.glsl +9 -0
- package/src/selection/selection_pixels.ts +59 -0
- package/src/selection/selection_vertex.glsl +15 -0
- package/src/selection/terrain_surface.ts +298 -0
- package/src/shader-modules/globe-horizon.ts +52 -0
- package/src/shader-modules/height-decode.ts +41 -0
- package/src/shader-modules/hillshade.ts +70 -0
- package/src/shader-modules/index.ts +10 -0
- package/src/shader-modules/planar.ts +24 -0
- package/src/shader-modules/sphere-map.ts +38 -0
- package/src/shader-modules/triplanar.ts +45 -0
- package/src/sources/aligned_tile_source.ts +122 -0
- package/src/sources/data_source.ts +655 -0
- package/src/sources/decoded_tile_store.ts +245 -0
- package/src/sources/geojson.ts +300 -0
- package/src/sources/mvt.ts +247 -0
- package/src/sources/raster.ts +341 -0
- package/src/sources/shared_tile_source_adapter.ts +117 -0
- package/src/sources/sources.ts +10 -0
- package/src/sources/tile_source_adapter.ts +84 -0
- package/src/sources/tile_source_metadata.ts +65 -0
- package/src/sources/tile_source_state.ts +112 -0
- package/src/sources/topojson.ts +90 -0
- package/src/styles/feature_annotations.ts +16 -0
- package/src/styles/filter.ts +190 -0
- package/src/styles/geometry-style-types.ts +209 -0
- package/src/styles/globe_visibility_wgsl.ts +22 -0
- package/src/styles/layer-types.ts +45 -0
- package/src/styles/layer.ts +586 -0
- package/src/styles/lines/dasharray.ts +49 -0
- package/src/styles/lines/lines.ts +707 -0
- package/src/styles/lines/lines_wgsl.ts +224 -0
- package/src/styles/points/point-runtime-types.ts +125 -0
- package/src/styles/points/points.ts +1079 -0
- package/src/styles/points/points_fragment.glsl +126 -0
- package/src/styles/points/points_vertex.glsl +230 -0
- package/src/styles/points/points_wgsl.ts +189 -0
- package/src/styles/polygons/polygons.ts +268 -0
- package/src/styles/polygons/polygons_fragment.glsl +173 -0
- package/src/styles/polygons/polygons_vertex.glsl +213 -0
- package/src/styles/polygons/polygons_wgsl.ts +127 -0
- package/src/styles/property-types.ts +125 -0
- package/src/styles/raster/raster.ts +33 -0
- package/src/styles/raster/raster_globals.glsl +46 -0
- package/src/styles/style-mixing-types.ts +101 -0
- package/src/styles/style-runtime-types.ts +220 -0
- package/src/styles/style-schema.ts +133 -0
- package/src/styles/style.ts +1033 -0
- package/src/styles/style_globals.glsl +69 -0
- package/src/styles/style_manager.ts +464 -0
- package/src/styles/style_parser.ts +620 -0
- package/src/styles/text/font_manager.ts +180 -0
- package/src/styles/text/fontfaceobserver.d.ts +10 -0
- package/src/styles/text/text-types.ts +220 -0
- package/src/styles/text/text.ts +378 -0
- package/src/styles/text/text_canvas.ts +773 -0
- package/src/styles/text/text_labels.ts +436 -0
- package/src/styles/text/text_segments.ts +142 -0
- package/src/styles/text/text_settings.ts +228 -0
- package/src/styles/text/text_wgsl.ts +159 -0
- package/src/styles/text/text_wrap.ts +194 -0
- package/src/tile/globe_tile_preload.ts +40 -0
- package/src/tile/tangram_tileset_2d.ts +108 -0
- package/src/tile/tile-build-types.ts +155 -0
- package/src/tile/tile.ts +807 -0
- package/src/tile/tile_id.ts +172 -0
- package/src/tile/tile_manager.ts +889 -0
- package/src/tile/tile_pyramid.ts +119 -0
- package/src/tile/tile_resource_cache.ts +91 -0
- package/src/tile/tile_traversal_adapter.ts +132 -0
- package/src/types.ts +332 -0
- package/src/utils/debounce.ts +17 -0
- package/src/utils/debug_settings.ts +43 -0
- package/src/utils/errors.ts +12 -0
- package/src/utils/functions.ts +95 -0
- package/src/utils/geo.ts +364 -0
- package/src/utils/hash.ts +14 -0
- package/src/utils/log.ts +108 -0
- package/src/utils/media_capture.ts +193 -0
- package/src/utils/merge.ts +44 -0
- package/src/utils/obb.ts +134 -0
- package/src/utils/props.ts +43 -0
- package/src/utils/slice.ts +14 -0
- package/src/utils/subscribe.ts +58 -0
- package/src/utils/task.ts +154 -0
- package/src/utils/thread.ts +26 -0
- package/src/utils/urls.ts +223 -0
- package/src/utils/utils.ts +310 -0
- package/src/utils/vector.ts +197 -0
- package/src/utils/version.ts +8 -0
- package/src/utils/worker-types.ts +79 -0
- package/src/utils/worker_broker.ts +523 -0
|
@@ -0,0 +1,2378 @@
|
|
|
1
|
+
// Tangram
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) 2013-2016 Brett Camper and Mapzen
|
|
4
|
+
// Copyright (c) 2026 vis.gl contributors
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
import log from '../utils/log';
|
|
8
|
+
import Utils from '../utils/utils';
|
|
9
|
+
import debugSettings from '../utils/debug_settings';
|
|
10
|
+
import * as URLs from '../utils/urls';
|
|
11
|
+
import WorkerBroker from '../utils/worker_broker';
|
|
12
|
+
import Task from '../utils/task';
|
|
13
|
+
import subscribeMixin from '../utils/subscribe';
|
|
14
|
+
import sliceObject from '../utils/slice';
|
|
15
|
+
import Context from '../gl/context';
|
|
16
|
+
import Texture from '../gl/texture';
|
|
17
|
+
import ShaderProgram from '../gl/shader_program';
|
|
18
|
+
import UniformBuffer from '../gl/uniform_buffer';
|
|
19
|
+
import VertexArrayObject from '../gl/vao';
|
|
20
|
+
import {StyleManager} from '../styles/style_manager';
|
|
21
|
+
import {Style} from '../styles/style';
|
|
22
|
+
import StyleParser from '../styles/style_parser';
|
|
23
|
+
import SceneLoader from './scene_loader';
|
|
24
|
+
import View from './view';
|
|
25
|
+
import Light from '../lights/light';
|
|
26
|
+
import {normalizeSceneLights} from '../lights/light-definitions';
|
|
27
|
+
import {PORTABLE_LIGHT_UNIFORMS, getPortableLightUniforms} from '../lights/lighting-uniforms';
|
|
28
|
+
import TileManager from '../tile/tile_manager';
|
|
29
|
+
import {isGlobeFallbackStyle} from '../tile/globe_tile_preload';
|
|
30
|
+
import DataSource from '../sources/data_source';
|
|
31
|
+
import '../sources/sources';
|
|
32
|
+
import FeatureSelection from '../selection/selection';
|
|
33
|
+
import RenderStateManager from '../gl/render_state';
|
|
34
|
+
import TextCanvas from '../styles/text/text_canvas';
|
|
35
|
+
import FontManager from '../styles/text/font_manager';
|
|
36
|
+
import MediaCapture from '../utils/media_capture';
|
|
37
|
+
import setupSceneDebug from './scene_debug';
|
|
38
|
+
import {getWorkerURL} from './worker_url';
|
|
39
|
+
import type {ViewScene} from './view';
|
|
40
|
+
import type {ViewOptions} from './view';
|
|
41
|
+
import type {SubscriptionMethods} from '../utils/subscribe';
|
|
42
|
+
import type {RendererOptions} from '../types';
|
|
43
|
+
import type {TangramGPUSceneOptions} from '../gpu/tangram_gpu_backend';
|
|
44
|
+
import type {UniformBufferOptions} from '../gl/uniform_buffer';
|
|
45
|
+
import type {SceneBundle} from './scene_bundle';
|
|
46
|
+
import type {FinalizedSceneDefinition, SceneTextureNodes} from './scene-resource-types';
|
|
47
|
+
import type {LogLevel} from '../utils/log';
|
|
48
|
+
import type {Device, RenderPipelineParameters} from '@luma.gl/core';
|
|
49
|
+
|
|
50
|
+
/** Scene-owned compatibility options, independent of authored scene configuration. */
|
|
51
|
+
export interface SceneOptions extends RendererOptions, ViewOptions, Partial<TangramGPUSceneOptions> {
|
|
52
|
+
/** Optional parent for a scene-owned classic canvas. */
|
|
53
|
+
container?: HTMLElement;
|
|
54
|
+
/** Callback before each owned-loop update. */
|
|
55
|
+
preUpdate?: () => void;
|
|
56
|
+
/** Callback after each owned-loop update. */
|
|
57
|
+
postUpdate?: () => void;
|
|
58
|
+
/** Leave frame scheduling to the host. */
|
|
59
|
+
disableRenderLoop?: boolean;
|
|
60
|
+
/** Retain the classic vertex-binding compatibility path. */
|
|
61
|
+
disableVertexArrayObjects?: boolean;
|
|
62
|
+
/** Attributes for a newly allocated classic context. */
|
|
63
|
+
webGLContextOptions?: WebGLContextAttributes;
|
|
64
|
+
/** Caller-owned classic context; never destroyed by this scene. */
|
|
65
|
+
webGLContext?: WebGLRenderingContext;
|
|
66
|
+
/** Host state guard around classic GL operations. */
|
|
67
|
+
webGLContextScope?: <Result>(callback: () => Result) => Result;
|
|
68
|
+
/** Logging threshold applied during scene construction. */
|
|
69
|
+
logLevel?: LogLevel;
|
|
70
|
+
}
|
|
71
|
+
/** Normalized scene registries after URL and global resolution; custom values remain unknown. */
|
|
72
|
+
type SceneRuntimeConfiguration = FinalizedSceneDefinition & {
|
|
73
|
+
sources: Record<string, import('./scene-resource-types').SceneSourceDefinition>;
|
|
74
|
+
textures: Record<string, import('../gl/texture').TextureOptions>;
|
|
75
|
+
cameras: NonNullable<NonNullable<ViewScene['config']>['cameras']>;
|
|
76
|
+
styles: Record<string, import('../styles/style-mixing-types').StyleDefinition>;
|
|
77
|
+
lights: Record<string, import('../lights/light').LightConfig>;
|
|
78
|
+
layers: Record<string, {enabled?: boolean; data?: {source: string; layer?: unknown}; [name: string]: unknown}>;
|
|
79
|
+
scene: {animated?: boolean; cpu_projection?: unknown; lighting?: unknown; scripts?: Record<string, string> | string[]; background?: {color?: unknown}; [name: string]: unknown};
|
|
80
|
+
}
|
|
81
|
+
/** Rebuild policy captured when a concurrent request is queued. */
|
|
82
|
+
interface RebuildOptions {
|
|
83
|
+
initial?: boolean;
|
|
84
|
+
new_generation?: boolean;
|
|
85
|
+
sources?: string[] | null;
|
|
86
|
+
serialize_funcs?: boolean;
|
|
87
|
+
profile?: boolean;
|
|
88
|
+
fade_in?: boolean;
|
|
89
|
+
preserveTileCache?: boolean;
|
|
90
|
+
preserveTime?: boolean;
|
|
91
|
+
/** Lightweight canonical ribbon refinement; null restores per-tile style zoom. */
|
|
92
|
+
terrainRibbonZoom?: number | null;
|
|
93
|
+
}
|
|
94
|
+
/** Promise ownership for the active generation and its queued replacement. */
|
|
95
|
+
interface SceneBuildState {
|
|
96
|
+
resolve(value: boolean | void): void;
|
|
97
|
+
reject(error: unknown): void;
|
|
98
|
+
initial: boolean;
|
|
99
|
+
error?: Error;
|
|
100
|
+
/** Worker configuration acknowledgements must precede terrain generation settlement. */
|
|
101
|
+
terrainConfigPending?: boolean;
|
|
102
|
+
queued?: {resolve(value: void | boolean): void; reject(error: unknown): void; options: RebuildOptions};
|
|
103
|
+
}
|
|
104
|
+
import type {SceneListeners, SceneDefinition, SceneLoadOptions, TileSourceStatistics, FeatureSelectionResult} from '../types';
|
|
105
|
+
import {validateConcurrentTileLoads} from '../sources/decoded_tile_store';
|
|
106
|
+
import type {RenderPass} from '@luma.gl/core';
|
|
107
|
+
import type {TangramTileSourceMetadata} from '../sources/tile_source_metadata';
|
|
108
|
+
import {normalizeTerrainDraping} from '../procedures/terrain-draping';
|
|
109
|
+
import {HostProjectionEngineAdapter, validateProjectionEngine} from '../procedures/projected-coordinate-transform';
|
|
110
|
+
import {normalizeProjectionExecutionOptions} from '../procedures/projection-batch-executor';
|
|
111
|
+
|
|
112
|
+
// Load scene definition: pass an object directly, or a URL as string to load remotely
|
|
113
|
+
export default class Scene {
|
|
114
|
+
/** Next scene identity used by broker targets. */
|
|
115
|
+
declare static id: number;
|
|
116
|
+
/** Next globally unique style/source generation. */
|
|
117
|
+
declare static generation: number;
|
|
118
|
+
/** Active initialization promise, cleared after settling. */
|
|
119
|
+
declare initializing: Promise<void> | null;
|
|
120
|
+
/** Requested worker-pool size. */
|
|
121
|
+
declare num_workers: number;
|
|
122
|
+
/** Optional caller-supplied worker bundle URL. */
|
|
123
|
+
declare worker_url: string | undefined;
|
|
124
|
+
/** Authored URL or object retained for reloads. */
|
|
125
|
+
declare config_source: SceneDefinition | null;
|
|
126
|
+
/** Root resolver for normalized scene resource paths. */
|
|
127
|
+
declare config_bundle: SceneBundle | null;
|
|
128
|
+
/** Most recent successfully loaded authored input. */
|
|
129
|
+
declare last_valid_config_source: SceneDefinition | null;
|
|
130
|
+
/** Reload options belonging to the last valid input. */
|
|
131
|
+
declare last_valid_options: SceneLoadOptions | undefined;
|
|
132
|
+
/** Compatibility reload state retained after a failed load. */
|
|
133
|
+
declare last_valid_base_path: SceneLoadOptions | undefined;
|
|
134
|
+
/** Active geometry generation and its optional queued replacement. */
|
|
135
|
+
declare building: SceneBuildState | null;
|
|
136
|
+
/** Whether this scene owns an automatic animation loop. */
|
|
137
|
+
declare render_loop: boolean;
|
|
138
|
+
/** Whether the owned animation loop has been started. */
|
|
139
|
+
declare render_loop_active: boolean;
|
|
140
|
+
/** Deferred stop request consumed by the animation loop. */
|
|
141
|
+
declare render_loop_stop: boolean;
|
|
142
|
+
/** Number of submitted render passes. */
|
|
143
|
+
declare render_count: number;
|
|
144
|
+
/** Previous frame's count used to detect redraw changes. */
|
|
145
|
+
declare last_render_count: number;
|
|
146
|
+
/** Whether this frame changed the submitted geometry count. */
|
|
147
|
+
declare render_count_changed: boolean;
|
|
148
|
+
/** Monotonic scene frame counter. */
|
|
149
|
+
declare frame: number;
|
|
150
|
+
/** Last frame with a completed color render. */
|
|
151
|
+
declare last_main_render: number;
|
|
152
|
+
/** Screenshot/video task owner shared by render callbacks. */
|
|
153
|
+
declare media_capture: MediaCapture;
|
|
154
|
+
/** Most recently fetched worker selection-map size. */
|
|
155
|
+
declare selection_feature_count: number;
|
|
156
|
+
/** Shared in-flight selection-map count request. */
|
|
157
|
+
declare fetching_selection_map: Promise<number> | null;
|
|
158
|
+
/** Previous scene's texture names awaiting ownership release. */
|
|
159
|
+
declare prev_textures: string[] | null;
|
|
160
|
+
/** Whether all eligible features receive selection IDs. */
|
|
161
|
+
declare introspection: boolean;
|
|
162
|
+
/** Lifecycle timestamps used by loading diagnostics. */
|
|
163
|
+
declare times: Record<string, number | null>;
|
|
164
|
+
/** Current shader time in seconds, including host overrides. */
|
|
165
|
+
declare animation_time: number;
|
|
166
|
+
/** Classic DOM canvas parent, absent for host-owned canvases. */
|
|
167
|
+
declare container: HTMLElement | null | undefined;
|
|
168
|
+
/** Classic or host canvas, cleared during destruction. */
|
|
169
|
+
declare canvas: HTMLCanvasElement | OffscreenCanvas | null;
|
|
170
|
+
/** Attributes used only when allocating a classic WebGL context. */
|
|
171
|
+
declare contextOptions: WebGLContextAttributes | undefined;
|
|
172
|
+
/** Caller-owned classic context, if supplied. */
|
|
173
|
+
declare external_gl: WebGLRenderingContext | null;
|
|
174
|
+
/** Active classic context; portable rendering can leave it absent. */
|
|
175
|
+
declare gl: WebGLRenderingContext | null;
|
|
176
|
+
/** Caller-owned luma device used by portable drawing and readback. */
|
|
177
|
+
declare device: Device | null;
|
|
178
|
+
/** Selected source language for the active backend. */
|
|
179
|
+
declare shader_language: 'glsl' | 'wgsl';
|
|
180
|
+
/** Whether drawing is delegated to the device backend. */
|
|
181
|
+
declare portable_rendering: boolean;
|
|
182
|
+
/** Opaque scene resource identity, not a raw GPU owner. */
|
|
183
|
+
declare resource_context: object | null;
|
|
184
|
+
/** Whether the current resource context is configured. */
|
|
185
|
+
declare resources_initialized: boolean;
|
|
186
|
+
/** Whether scene teardown must release its canvas/context. */
|
|
187
|
+
declare owns_gl: boolean;
|
|
188
|
+
/** Optional host callback that brackets classic context access. */
|
|
189
|
+
declare webgl_context_scope: SceneOptions['webGLContextScope'];
|
|
190
|
+
/** Host notification used instead of an owned render loop. */
|
|
191
|
+
declare redraw_callback: (() => void) | undefined;
|
|
192
|
+
/** Whether classic shader state uses uniform blocks. */
|
|
193
|
+
declare enable_uniform_buffers: boolean;
|
|
194
|
+
/** Whether shader allocation is delegated to the device factory. */
|
|
195
|
+
declare device_shader_compilation: boolean;
|
|
196
|
+
/** Scene-owned uniform blocks shared by tile draws. */
|
|
197
|
+
declare uniform_buffers: Record<string, UniformBuffer>;
|
|
198
|
+
/** Initialized light instances, cleared during scene teardown. */
|
|
199
|
+
declare lights: Record<string, Light> | null;
|
|
200
|
+
/** Cached authored background and its last computed RGBA value. */
|
|
201
|
+
declare background: {color?: import('../styles/property-types').PropertyCache; computed_color?: number[]} | null;
|
|
202
|
+
/** Nested configuration-update lock count. */
|
|
203
|
+
declare updating: number;
|
|
204
|
+
/** Most recent generation whose geometry work drained. */
|
|
205
|
+
declare last_complete_generation: number | null;
|
|
206
|
+
/** Selected logging threshold for this scene. */
|
|
207
|
+
declare log_level: LogLevel;
|
|
208
|
+
/** Whether the current view has finished loading and building. */
|
|
209
|
+
declare view_complete: boolean;
|
|
210
|
+
/** Deduplicated scripts shared by scene and source configuration. */
|
|
211
|
+
declare external_scripts: string[] | undefined;
|
|
212
|
+
/** Round-robin worker index for new tile builds. */
|
|
213
|
+
declare next_worker: number;
|
|
214
|
+
/** Authored root directory used when reloading edited scenes. */
|
|
215
|
+
declare base_path: string | undefined;
|
|
216
|
+
/** Deprecated alias of the root resource directory. */
|
|
217
|
+
declare config_path: string | undefined;
|
|
218
|
+
/** Related-object subscriptions released during teardown. */
|
|
219
|
+
declare listeners: Record<string, import('../utils/subscribe').EventListeners> | null;
|
|
220
|
+
/** Classic cached GL render-state owner. */
|
|
221
|
+
declare render_states: RenderStateManager | undefined;
|
|
222
|
+
/** Portable pipeline policy captured for the current pass. */
|
|
223
|
+
declare mesh_render_state: RenderPipelineParameters | null;
|
|
224
|
+
/** Profiling callbacks installed by the scene debug adapter. */
|
|
225
|
+
declare debug: {profile(name: string): void; profileEnd(name: string): void};
|
|
226
|
+
/** Scene event dispatcher installed by the subscription mixin. */
|
|
227
|
+
declare trigger: SubscriptionMethods['trigger'];
|
|
228
|
+
/** Subscription lookup used before emitting expensive diagnostics. */
|
|
229
|
+
declare hasSubscribersFor: SubscriptionMethods['hasSubscribersFor'];
|
|
230
|
+
/** Subscription teardown installed by the shared event mixin. */
|
|
231
|
+
declare unsubscribeAll: SubscriptionMethods['unsubscribeAll'];
|
|
232
|
+
/** Caller allocation hook for scene-owned uniform blocks. */
|
|
233
|
+
declare uniform_buffer_factory: TangramGPUSceneOptions['uniformBufferFactory'] | undefined;
|
|
234
|
+
/** Caller allocation hook for owned shader resources. */
|
|
235
|
+
declare shader_factory: TangramGPUSceneOptions['shaderFactory'] | undefined;
|
|
236
|
+
/** Optional backend shader validation hook. */
|
|
237
|
+
declare shader_program_validator: TangramGPUSceneOptions['shaderProgramValidator'] | undefined;
|
|
238
|
+
/** Caller allocation hook for owned mesh buffers. */
|
|
239
|
+
declare mesh_buffer_factory: TangramGPUSceneOptions['meshBufferFactory'] | undefined;
|
|
240
|
+
/** Backend draw dispatcher; the scene does not own it. */
|
|
241
|
+
declare mesh_renderer: TangramGPUSceneOptions['meshRenderer'] | undefined;
|
|
242
|
+
/** Caller allocation hook for scene texture resources. */
|
|
243
|
+
declare texture_factory: TangramGPUSceneOptions['textureFactory'] | undefined;
|
|
244
|
+
/** Backend texture limit used by glyph atlas packing. */
|
|
245
|
+
declare max_texture_size: number | undefined;
|
|
246
|
+
/** Numeric broker identity assigned before the manager is constructed. */
|
|
247
|
+
declare readonly id: number;
|
|
248
|
+
/** Source/style generation consumed by serialized tile callbacks. */
|
|
249
|
+
declare generation: number;
|
|
250
|
+
/** Worker pool is absent before initialization and released during teardown. */
|
|
251
|
+
declare workers: import('../utils/worker-types').BrokerWorker[] | null | undefined;
|
|
252
|
+
/** Main-thread upload styles are not available before scene configuration loads. */
|
|
253
|
+
declare styles: Record<string, import('../styles/style-mixing-types').ManagedStyle> | null;
|
|
254
|
+
/** Detached optional worker terrain input; never a deck view or a provider handle. */
|
|
255
|
+
declare terrain: import('../procedures/terrain-draping').TerrainDrapingSnapshot | undefined;
|
|
256
|
+
/** Borrowed main-thread admission pool, never serialized into worker configuration. */
|
|
257
|
+
declare terrainResourceBudget: import('../map-logic/terrain-resource-budget').TerrainResourceBudget | undefined;
|
|
258
|
+
/** Last requested worker ribbon zoom, independent of physical field/morph identity. */
|
|
259
|
+
declare terrainRibbonZoom: number | undefined;
|
|
260
|
+
/** Authored line draw groups remain refinable even when no-data suppresses every current road mesh. */
|
|
261
|
+
terrainRibbonStyles = false;
|
|
262
|
+
/** Main/worker input copies and internal query-capacity reservation, independent of mesh owners. */
|
|
263
|
+
private terrainInputLease?: import('../map-logic/terrain-resource-budget').TerrainResourceLease;
|
|
264
|
+
/** Replacement inputs cannot spend the still-admitted generation's reservation. */
|
|
265
|
+
private terrainStagedInputLease?: import('../map-logic/terrain-resource-budget').TerrainResourceLease;
|
|
266
|
+
/** Input identity already admitted for the current workers/query capacities. */
|
|
267
|
+
private terrainInputSnapshot?: import('../procedures/terrain-draping').TerrainDrapingSnapshot;
|
|
268
|
+
/** Input identity whose replacement resources are still staged. */
|
|
269
|
+
private terrainStagedInputSnapshot?: import('../procedures/terrain-draping').TerrainDrapingSnapshot;
|
|
270
|
+
/** Failed worker snapshots remain owned until a complete successful replacement or worker disposal. */
|
|
271
|
+
private terrainFailedInputs: {lease: import('../map-logic/terrain-resource-budget').TerrainResourceLease; snapshot: import('../procedures/terrain-draping').TerrainDrapingSnapshot}[] = [];
|
|
272
|
+
/** Visual multiplier independent of source heights and cache keys. */
|
|
273
|
+
declare terrainExaggeration: number;
|
|
274
|
+
/** Monotonic timestamp shared by color, collision and every submitted eye. */
|
|
275
|
+
declare terrainFrameTime: number | undefined;
|
|
276
|
+
/** Terrain-only worker rebuilds stage mesh owners until every batch has completed. */
|
|
277
|
+
declare terrainTransaction: boolean | undefined;
|
|
278
|
+
/** Last terrain draw generation admitted after complete worker replacement. */
|
|
279
|
+
declare terrainDisplayGeneration: number | undefined;
|
|
280
|
+
/** Whether scene resources and workers are ready for queries/draws. */
|
|
281
|
+
declare initialized: boolean;
|
|
282
|
+
/** Active selection owner; loading clears this while a Renderer may retain its frame. */
|
|
283
|
+
declare selection_view_id: string | null;
|
|
284
|
+
/** Scene-local style activation and blend-order ownership. */
|
|
285
|
+
declare style_manager: StyleManager;
|
|
286
|
+
/** Active eye's independently owned selection target. */
|
|
287
|
+
declare selection: FeatureSelection | null;
|
|
288
|
+
/** Retained readback contexts; source feature maps remain shared in workers. */
|
|
289
|
+
declare selection_views: Map<string, {selection: FeatureSelection; lastRender: number; pending: boolean}>;
|
|
290
|
+
/** Current eye's pipeline-readiness lock, saved when another eye becomes active. */
|
|
291
|
+
declare selection_render_pending: boolean;
|
|
292
|
+
/** Last completed selection frame for the active eye. */
|
|
293
|
+
declare last_selection_render: number;
|
|
294
|
+
declare subscribe: (listeners: SceneListeners) => void;
|
|
295
|
+
declare view: View;
|
|
296
|
+
/** Tile worker and mesh ownership for the active scene. */
|
|
297
|
+
declare tile_manager: TileManager | null;
|
|
298
|
+
declare config: SceneRuntimeConfiguration | null;
|
|
299
|
+
/** Current source instances, recreated when scene configuration changes. */
|
|
300
|
+
declare sources: Record<string, DataSource>;
|
|
301
|
+
declare dirty: boolean;
|
|
302
|
+
declare start_time: number;
|
|
303
|
+
declare host_animation_time: number | null;
|
|
304
|
+
/** Shared source-procedure limit applies independently to each worker, not to mesh builds. */
|
|
305
|
+
readonly maxConcurrentTileLoadsPerWorker: number | undefined;
|
|
306
|
+
/** Scene-local broker endpoint; only this string crosses into workers. */
|
|
307
|
+
private readonly projectionTarget: string | undefined;
|
|
308
|
+
/** Scene owns compiled transforms, not the caller's reusable engine. */
|
|
309
|
+
private readonly projectionAdapter: HostProjectionEngineAdapter | undefined;
|
|
310
|
+
|
|
311
|
+
/** Configuration-dependent operations run after scene loading; teardown/reload keep the optional field truthful. */
|
|
312
|
+
private get liveConfig(): SceneRuntimeConfiguration {
|
|
313
|
+
if (!this.config) throw new Error('Scene configuration is not loaded');
|
|
314
|
+
return this.config;
|
|
315
|
+
}
|
|
316
|
+
/** Draw operations on the classic path require a live context, unlike device-only submission. */
|
|
317
|
+
private get activeGL(): WebGLRenderingContext {
|
|
318
|
+
if (!this.gl) throw new Error('Scene has no active WebGL context');
|
|
319
|
+
return this.gl;
|
|
320
|
+
}
|
|
321
|
+
/** Worker calls follow initialization and must not cross the destroyed pool boundary. */
|
|
322
|
+
private get activeWorkers(): import('../utils/worker-types').BrokerWorker[] {
|
|
323
|
+
if (!this.workers) throw new Error('Scene worker pool is not initialized');
|
|
324
|
+
return this.workers;
|
|
325
|
+
}
|
|
326
|
+
/** Styles are initialized before the main draw path and retired when a scene is destroyed. */
|
|
327
|
+
private get liveStyles(): Record<string, import('../styles/style-mixing-types').ManagedStyle> {
|
|
328
|
+
if (!this.styles) throw new Error('Scene styles are not initialized');
|
|
329
|
+
return this.styles;
|
|
330
|
+
}
|
|
331
|
+
/** Tile ownership is unavailable after teardown rather than a permanently live façade. */
|
|
332
|
+
private get activeTileManager(): TileManager {
|
|
333
|
+
if (!this.tile_manager) throw new Error('Scene tile manager has been destroyed');
|
|
334
|
+
return this.tile_manager;
|
|
335
|
+
}
|
|
336
|
+
/** Rendering and query code use a concrete active-eye target after readiness checks. */
|
|
337
|
+
private get activeSelection(): FeatureSelection {
|
|
338
|
+
if (!this.selection) throw new Error('Scene feature selection is not initialized');
|
|
339
|
+
return this.selection;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
constructor(config_source: SceneDefinition | null, options: SceneOptions = {}) {
|
|
343
|
+
options = options || {};
|
|
344
|
+
this.terrain = options.terrain ? normalizeTerrainDraping(options.terrain) : undefined;
|
|
345
|
+
this.terrainResourceBudget = options.terrainResourceBudget;
|
|
346
|
+
this.terrainInputLease = this.terrainResourceBudget?.reserve('retained', 0) ?? undefined;
|
|
347
|
+
this.terrainExaggeration = 1;
|
|
348
|
+
validateProjectionEngine(options.projectionEngine);
|
|
349
|
+
normalizeProjectionExecutionOptions(options.projectionEngineExecution);
|
|
350
|
+
this.maxConcurrentTileLoadsPerWorker = validateConcurrentTileLoads(options.maxConcurrentTileLoadsPerWorker);
|
|
351
|
+
subscribeMixin(this);
|
|
352
|
+
|
|
353
|
+
this.id = Scene.id++;
|
|
354
|
+
this.initialized = false;
|
|
355
|
+
this.initializing = null; // will be a promise that resolves when scene is loaded
|
|
356
|
+
this.sources = {};
|
|
357
|
+
|
|
358
|
+
this.view = new View(this, options);
|
|
359
|
+
this.tile_manager = new TileManager({ scene: this });
|
|
360
|
+
this.num_workers = options.numWorkers || 2;
|
|
361
|
+
this.worker_url = options.workerURL;
|
|
362
|
+
if (options.disableVertexArrayObjects === true) {
|
|
363
|
+
VertexArrayObject.disabled = true;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
Utils.use_high_density_display = options.highDensityDisplay !== undefined ? options.highDensityDisplay : true;
|
|
367
|
+
Utils.updateDevicePixelRatio();
|
|
368
|
+
|
|
369
|
+
this.config = null;
|
|
370
|
+
this.config_source = config_source;
|
|
371
|
+
this.config_bundle = null;
|
|
372
|
+
this.last_valid_config_source = null;
|
|
373
|
+
|
|
374
|
+
this.styles = null;
|
|
375
|
+
this.style_manager = new StyleManager();
|
|
376
|
+
|
|
377
|
+
this.building = null; // tracks current scene building state (tiles being built, etc.)
|
|
378
|
+
this.dirty = true; // request a redraw
|
|
379
|
+
|
|
380
|
+
if (options.preUpdate){
|
|
381
|
+
// optional pre-render loop hook
|
|
382
|
+
this.subscribe({'pre_update': options.preUpdate});
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (options.postUpdate){
|
|
386
|
+
// optional post-render loop hook
|
|
387
|
+
this.subscribe({'post_update': options.postUpdate});
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
this.render_loop = !options.disableRenderLoop; // disable render loop - app will have to manually call Scene.render() per frame
|
|
391
|
+
this.render_loop_active = false;
|
|
392
|
+
this.render_loop_stop = false;
|
|
393
|
+
this.render_count = 0;
|
|
394
|
+
this.last_render_count = 0;
|
|
395
|
+
this.render_count_changed = false;
|
|
396
|
+
this.frame = 0;
|
|
397
|
+
this.last_main_render = -1; // frame counter for last main render pass
|
|
398
|
+
this.last_selection_render = -1; // frame counter for last selection render pass
|
|
399
|
+
this.selection_render_pending = false; // unfinished pipeline draws invalidate the selection target
|
|
400
|
+
this.media_capture = new MediaCapture();
|
|
401
|
+
this.selection = null;
|
|
402
|
+
this.selection_views = new Map();
|
|
403
|
+
this.selection_view_id = null;
|
|
404
|
+
this.selection_feature_count = 0;
|
|
405
|
+
this.fetching_selection_map = null;
|
|
406
|
+
this.prev_textures = null; // textures from previously loaded scene (used for cleanup)
|
|
407
|
+
this.introspection = (options.introspection === true) ? true : false;
|
|
408
|
+
this.times = {}; // internal time logs (mostly for dev/profiling)
|
|
409
|
+
this.resetTime();
|
|
410
|
+
this.host_animation_time = null;
|
|
411
|
+
this.animation_time = 0;
|
|
412
|
+
|
|
413
|
+
this.container = options.container;
|
|
414
|
+
this.canvas = options.canvas || null;
|
|
415
|
+
this.contextOptions = options.webGLContextOptions;
|
|
416
|
+
this.external_gl = options.webGLContext || null;
|
|
417
|
+
this.gl = null;
|
|
418
|
+
this.device = options.device || null;
|
|
419
|
+
this.shader_language = options.shaderLanguage || 'glsl';
|
|
420
|
+
this.portable_rendering = Boolean(this.device && this.shader_language !== 'glsl');
|
|
421
|
+
this.resource_context = this.portable_rendering ? this.device : null;
|
|
422
|
+
this.resources_initialized = false;
|
|
423
|
+
this.owns_gl = !this.external_gl && !this.portable_rendering;
|
|
424
|
+
this.webgl_context_scope = options.webGLContextScope;
|
|
425
|
+
this.redraw_callback = options.requestRedraw;
|
|
426
|
+
this.enable_uniform_buffers = options.enableUniformBuffers === true;
|
|
427
|
+
this.device_shader_compilation = options.deviceShaderCompilation === true;
|
|
428
|
+
this.uniform_buffer_factory = options.uniformBufferFactory;
|
|
429
|
+
this.shader_factory = options.shaderFactory;
|
|
430
|
+
this.shader_program_validator = options.shaderProgramValidator;
|
|
431
|
+
this.mesh_buffer_factory = options.meshBufferFactory;
|
|
432
|
+
this.mesh_renderer = options.meshRenderer;
|
|
433
|
+
this.texture_factory = options.textureFactory;
|
|
434
|
+
this.max_texture_size = options.maxTextureSize;
|
|
435
|
+
this.uniform_buffers = {};
|
|
436
|
+
|
|
437
|
+
this.lights = null;
|
|
438
|
+
this.background = null;
|
|
439
|
+
|
|
440
|
+
this.createListeners();
|
|
441
|
+
this.updating = 0;
|
|
442
|
+
this.generation = Scene.generation; // an id that is incremented each time the scene config is invalidated
|
|
443
|
+
this.last_complete_generation = Scene.generation; // last generation id with a complete view
|
|
444
|
+
setupSceneDebug(this);
|
|
445
|
+
|
|
446
|
+
this.log_level = options.logLevel || 'warn';
|
|
447
|
+
log.setLevel(this.log_level);
|
|
448
|
+
log.reset?.();
|
|
449
|
+
if (options.projectionEngine) {
|
|
450
|
+
const adapter = this.projectionAdapter = new HostProjectionEngineAdapter(options.projectionEngine, options.projectionEngineExecution);
|
|
451
|
+
this.projectionTarget = `ProjectionEngine_${this.id}`;
|
|
452
|
+
WorkerBroker.addTarget(this.projectionTarget, {
|
|
453
|
+
projectPositions: async (...parameters: Parameters<HostProjectionEngineAdapter['projectPositions']>) => WorkerBroker.withTransferables(
|
|
454
|
+
await adapter.projectPositions(...parameters))
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
static create (config: SceneDefinition | null, options: SceneOptions = {}) {
|
|
460
|
+
return new Scene(config, options);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Supply camera matrices from an embedding renderer
|
|
464
|
+
setCameraMatrices (matrices: Parameters<View['setCameraMatrices']>[0]) {
|
|
465
|
+
this.view.setCameraMatrices(matrices);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Load scene (or reload existing scene if no new source specified)
|
|
469
|
+
// Options:
|
|
470
|
+
// `base_path`: base URL against which scene resources should be resolved (useful for Play) (default nulll)
|
|
471
|
+
// `blocking`: should rendering block on scene load completion (default true)
|
|
472
|
+
load (config_source: SceneDefinition | null = null, options: SceneLoadOptions = {}): Promise<void> {
|
|
473
|
+
if (this.initializing) {
|
|
474
|
+
return this.initializing;
|
|
475
|
+
}
|
|
476
|
+
log.reset?.();
|
|
477
|
+
|
|
478
|
+
this.updating++;
|
|
479
|
+
this.initialized = false;
|
|
480
|
+
this.view_complete = false; // track if a view complete event has been triggered yet
|
|
481
|
+
this.times.frame = null; // clear first frame time
|
|
482
|
+
this.times.build = null; // clear first scene build time
|
|
483
|
+
|
|
484
|
+
// Backwards compatibilty for passing `base_path` string as second argument
|
|
485
|
+
// (since transitioned to using options argument to accept more parameters)
|
|
486
|
+
options = (typeof options === 'string') ? { base_path: options } : options;
|
|
487
|
+
|
|
488
|
+
// Should rendering block on load (not desirable for initial load, often desired for live style-switching)
|
|
489
|
+
options.blocking = (options.blocking !== undefined) ? options.blocking : true;
|
|
490
|
+
|
|
491
|
+
if (this.render_loop !== false) {
|
|
492
|
+
this.setupRenderLoop();
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Load scene definition (sources, styles, etc.), then create styles & workers
|
|
496
|
+
this.createCanvas();
|
|
497
|
+
this.prev_textures = this.config && Object.keys(this.liveConfig.textures); // save textures from last scene
|
|
498
|
+
this.initializing = this.loadScene(config_source, options)
|
|
499
|
+
.then(async ({ texture_nodes }) => {
|
|
500
|
+
await this.createWorkers();
|
|
501
|
+
|
|
502
|
+
// Clean up resources from prior scene
|
|
503
|
+
this.destroyFeatureSelection();
|
|
504
|
+
WorkerBroker.postMessage(this.activeWorkers, 'self.clearFunctionStringCache');
|
|
505
|
+
|
|
506
|
+
// Scene loaded from a JS object, or modified by a `load` event, may contain compiled JS functions
|
|
507
|
+
// which need to be serialized, while one loaded only from a URL does not.
|
|
508
|
+
const serialize_funcs = ((typeof this.config_source === 'object') || this.hasSubscribersFor('load'));
|
|
509
|
+
|
|
510
|
+
const updating = this.updateConfig({
|
|
511
|
+
texture_nodes,
|
|
512
|
+
serialize_funcs,
|
|
513
|
+
normalize: false,
|
|
514
|
+
loading: true,
|
|
515
|
+
fade_in: true });
|
|
516
|
+
|
|
517
|
+
if (options.blocking === true) {
|
|
518
|
+
await updating;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
this.freePreviousTextures();
|
|
522
|
+
this.updating--;
|
|
523
|
+
this.initializing = null;
|
|
524
|
+
this.initialized = true;
|
|
525
|
+
this.last_valid_config_source = this.config_source;
|
|
526
|
+
this.last_valid_options = { base_path: options.base_path, file_type: options.file_type };
|
|
527
|
+
|
|
528
|
+
this.requestRedraw();
|
|
529
|
+
}).catch(error => {
|
|
530
|
+
this.initializing = null;
|
|
531
|
+
this.updating = 0;
|
|
532
|
+
|
|
533
|
+
// Report and revert to last valid config if available
|
|
534
|
+
let type, message;
|
|
535
|
+
if (error.name === 'YAMLException') {
|
|
536
|
+
type = 'yaml';
|
|
537
|
+
message = 'Error parsing scene YAML';
|
|
538
|
+
}
|
|
539
|
+
else {
|
|
540
|
+
// TODO: more error types
|
|
541
|
+
message = 'Error initializing scene';
|
|
542
|
+
}
|
|
543
|
+
this.trigger('error', { type, message, error, url: this.config_source });
|
|
544
|
+
|
|
545
|
+
message = `Scene.load() failed to load ${JSON.stringify(this.config_source)}: ${error.message}`;
|
|
546
|
+
if (this.last_valid_config_source) {
|
|
547
|
+
log('warn', message, error);
|
|
548
|
+
log('info', 'Scene.load() reverting to last valid configuration');
|
|
549
|
+
return this.load(this.last_valid_config_source, this.last_valid_base_path);
|
|
550
|
+
}
|
|
551
|
+
log('error', message, error);
|
|
552
|
+
throw error;
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
return this.initializing;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
destroy() {
|
|
559
|
+
return this.withWebGLContext(() => this.destroyScene());
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
destroyScene() {
|
|
563
|
+
this.projectionAdapter?.dispose();
|
|
564
|
+
if (this.projectionTarget) WorkerBroker.removeTarget(this.projectionTarget);
|
|
565
|
+
this.initialized = false;
|
|
566
|
+
this.render_loop_stop = true; // schedule render loop to stop
|
|
567
|
+
|
|
568
|
+
this.destroyListeners();
|
|
569
|
+
this.destroyFeatureSelection();
|
|
570
|
+
|
|
571
|
+
if (this.owns_gl && this.canvas && 'parentNode' in this.canvas && this.canvas.parentNode) {
|
|
572
|
+
this.canvas.parentNode.removeChild(this.canvas);
|
|
573
|
+
}
|
|
574
|
+
this.canvas = null;
|
|
575
|
+
this.container = null;
|
|
576
|
+
|
|
577
|
+
const resource_context = this.portable_rendering ? this.resource_context : this.gl;
|
|
578
|
+
if (resource_context) {
|
|
579
|
+
Texture.destroy(resource_context);
|
|
580
|
+
Texture.clearResourceFactory(resource_context);
|
|
581
|
+
this.style_manager.destroy(resource_context);
|
|
582
|
+
this.styles = {};
|
|
583
|
+
this.destroyUniformBuffers();
|
|
584
|
+
|
|
585
|
+
ShaderProgram.reset();
|
|
586
|
+
|
|
587
|
+
if (this.owns_gl) {
|
|
588
|
+
// Force context loss only when Tangram created and owns it
|
|
589
|
+
let ext = this.activeGL.getExtension('WEBGL_lose_context');
|
|
590
|
+
if (ext) {
|
|
591
|
+
ext.loseContext();
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
this.gl = null;
|
|
596
|
+
}
|
|
597
|
+
this.resources_initialized = false;
|
|
598
|
+
|
|
599
|
+
for (const source of Object.values(this.sources)) source.dispose?.();
|
|
600
|
+
this.sources = {};
|
|
601
|
+
|
|
602
|
+
this.destroyWorkers();
|
|
603
|
+
this.activeTileManager.destroy();
|
|
604
|
+
this.terrainInputLease?.release();
|
|
605
|
+
this.terrainStagedInputLease?.release();
|
|
606
|
+
for (const input of this.terrainFailedInputs) input.lease.release();
|
|
607
|
+
this.terrainFailedInputs = [];
|
|
608
|
+
this.terrain = undefined; this.terrainInputSnapshot = undefined; this.terrainStagedInputSnapshot = undefined;
|
|
609
|
+
this.terrainResourceBudget = undefined;
|
|
610
|
+
this.tile_manager = null;
|
|
611
|
+
log.reset?.();
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
createCanvas() {
|
|
615
|
+
if (this.gl || (this.portable_rendering && this.resources_initialized)) {
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
return this.withWebGLContext(() => this.createCanvasContext());
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
createCanvasContext() {
|
|
623
|
+
if (this.portable_rendering) {
|
|
624
|
+
if (!this.resource_context) throw new Error('Scene has no portable resource context');
|
|
625
|
+
Texture.setResourceFactory(this.resource_context, this.texture_factory);
|
|
626
|
+
this.media_capture.setCanvas(this.canvas, null);
|
|
627
|
+
this.createUniformBuffers();
|
|
628
|
+
this.resources_initialized = true;
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
else if (this.external_gl) {
|
|
632
|
+
this.gl = Context.configure(this.external_gl, this.webgl_context_scope);
|
|
633
|
+
this.canvas = this.activeGL.canvas;
|
|
634
|
+
}
|
|
635
|
+
else {
|
|
636
|
+
this.container = this.container || document.body;
|
|
637
|
+
this.canvas = document.createElement('canvas');
|
|
638
|
+
this.canvas.style.position = 'absolute';
|
|
639
|
+
this.canvas.style.top = '0';
|
|
640
|
+
this.canvas.style.left = '0';
|
|
641
|
+
|
|
642
|
+
// Force tangram canvas underneath all leaflet layers, and set background to transparent
|
|
643
|
+
this.container.style.backgroundColor = 'transparent';
|
|
644
|
+
this.container.appendChild(this.canvas);
|
|
645
|
+
|
|
646
|
+
try {
|
|
647
|
+
this.gl = Context.getContext(this.canvas, Object.assign({
|
|
648
|
+
alpha: true, premultipliedAlpha: true,
|
|
649
|
+
stencil: true,
|
|
650
|
+
device_pixel_ratio: Utils.device_pixel_ratio,
|
|
651
|
+
powerPreference: 'high-performance'
|
|
652
|
+
}, this.contextOptions));
|
|
653
|
+
}
|
|
654
|
+
catch(e) {
|
|
655
|
+
throw new Error(
|
|
656
|
+
'Couldn\'t create WebGL context. ' +
|
|
657
|
+
'Your browser may not support WebGL, or it\'s turned off? ' +
|
|
658
|
+
'Visit http://webglreport.com/ for more info.'
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
if (this.owns_gl) {
|
|
664
|
+
if (!this.container) throw new Error('Scene canvas has no container');
|
|
665
|
+
this.resizeMap(this.container.clientWidth, this.container.clientHeight);
|
|
666
|
+
}
|
|
667
|
+
Texture.setResourceFactory(this.gl, this.texture_factory);
|
|
668
|
+
VertexArrayObject.init(this.gl);
|
|
669
|
+
this.render_states = new RenderStateManager(this.gl);
|
|
670
|
+
this.media_capture.setCanvas(this.canvas, this.gl);
|
|
671
|
+
this.createUniformBuffers();
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
createUniformBuffers() {
|
|
675
|
+
if (!this.enable_uniform_buffers ||
|
|
676
|
+
(!UniformBuffer.isSupported(this.gl) && !this.uniform_buffer_factory)) {
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
this.uniform_buffers.TangramView = this.createUniformBuffer({
|
|
680
|
+
name: 'TangramView',
|
|
681
|
+
binding: 0,
|
|
682
|
+
uniforms: {
|
|
683
|
+
u_resolution: 'vec2',
|
|
684
|
+
u_time: 'float',
|
|
685
|
+
u_map_position: 'vec3',
|
|
686
|
+
u_meters_per_pixel: 'float',
|
|
687
|
+
u_device_pixel_ratio: 'float',
|
|
688
|
+
u_view_pan_snap_timer: 'float',
|
|
689
|
+
u_view_panning: 'bool',
|
|
690
|
+
u_projection_mode: 'int'
|
|
691
|
+
}
|
|
692
|
+
});
|
|
693
|
+
this.uniform_buffers.TangramCamera = this.createUniformBuffer({
|
|
694
|
+
name: 'TangramCamera',
|
|
695
|
+
binding: 1,
|
|
696
|
+
uniforms: {
|
|
697
|
+
u_projection: 'mat4',
|
|
698
|
+
u_eye: 'vec3',
|
|
699
|
+
u_vanishing_point: 'vec2'
|
|
700
|
+
}
|
|
701
|
+
});
|
|
702
|
+
this.uniform_buffers.TangramTile = this.createUniformBuffer({
|
|
703
|
+
name: 'TangramTile',
|
|
704
|
+
binding: 2,
|
|
705
|
+
snapshotPerMesh: true,
|
|
706
|
+
uniforms: {
|
|
707
|
+
u_tile_origin: 'vec4',
|
|
708
|
+
u_tile_proxy_order_offset: 'float',
|
|
709
|
+
u_tile_clip_bounds: 'vec4',
|
|
710
|
+
u_model: 'mat4',
|
|
711
|
+
u_modelView: 'mat4',
|
|
712
|
+
u_normalMatrix: 'mat3',
|
|
713
|
+
u_inverseNormalMatrix: 'mat3',
|
|
714
|
+
u_tile_fade_in: 'bool',
|
|
715
|
+
...(this.terrain ? {u_terrain_blend: 'float', u_terrain_exaggeration: 'float'} : {})
|
|
716
|
+
}
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
createUniformBuffer(options: UniformBufferOptions) {
|
|
721
|
+
const gl = this.gl;
|
|
722
|
+
return new UniformBuffer(this.portable_rendering || !UniformBuffer.isSupported(gl) ? null : gl, Object.assign({}, options, {
|
|
723
|
+
bufferFactory: this.uniform_buffer_factory
|
|
724
|
+
}));
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
destroyUniformBuffers() {
|
|
728
|
+
for (const uniform_buffer of Object.values(this.uniform_buffers)) {
|
|
729
|
+
uniform_buffer.destroy();
|
|
730
|
+
}
|
|
731
|
+
this.uniform_buffers = {};
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// Update list of any custom scripts (either at scene-level or data-source-level)
|
|
735
|
+
updateExternalScripts () {
|
|
736
|
+
let prev_scripts = [...(this.external_scripts||[])]; // save list of previously loaded scripts
|
|
737
|
+
let scripts = [];
|
|
738
|
+
|
|
739
|
+
// scene-level scripts
|
|
740
|
+
if (this.liveConfig.scene.scripts) {
|
|
741
|
+
for (let f in this.liveConfig.scene.scripts) {
|
|
742
|
+
const script = Reflect.get(this.liveConfig.scene.scripts, f);
|
|
743
|
+
if (scripts.indexOf(script) === -1) {
|
|
744
|
+
scripts.push(script);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// data-source-level scripts
|
|
750
|
+
for (let s in this.liveConfig.sources) {
|
|
751
|
+
let source = this.liveConfig.sources[s];
|
|
752
|
+
if (source.scripts) {
|
|
753
|
+
for (let f in source.scripts) {
|
|
754
|
+
const script = Reflect.get(source.scripts, f);
|
|
755
|
+
if (scripts.indexOf(script) === -1) {
|
|
756
|
+
scripts.push(script);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
this.external_scripts = scripts;
|
|
763
|
+
|
|
764
|
+
// Scripts changed?
|
|
765
|
+
return !(this.external_scripts.length === prev_scripts.length &&
|
|
766
|
+
this.external_scripts.every((v, i) => v === prev_scripts[i]));
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// Web workers handle heavy duty tile construction: networking, geometry processing, etc.
|
|
770
|
+
createWorkers() {
|
|
771
|
+
// Reset old workers (if any) if we need to re-instantiate with new external scripts
|
|
772
|
+
if (this.updateExternalScripts()) {
|
|
773
|
+
this.destroyWorkers();
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
if (!this.workers) {
|
|
777
|
+
return this.makeWorkers();
|
|
778
|
+
}
|
|
779
|
+
return Promise.resolve();
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// Instantiate workers from URL, init event handlers
|
|
783
|
+
makeWorkers() {
|
|
784
|
+
// Let VertexElements know if 32 bit indices for element arrays are available
|
|
785
|
+
let has_element_index_uint = this.portable_rendering ||
|
|
786
|
+
(this.activeGL.getExtension('OES_element_index_uint') ? true : false);
|
|
787
|
+
|
|
788
|
+
let queue = [];
|
|
789
|
+
this.workers = [];
|
|
790
|
+
for (let id=0; id < this.num_workers; id++) {
|
|
791
|
+
let url = this.worker_url || getWorkerURL();
|
|
792
|
+
if (!url) {
|
|
793
|
+
const fallbackValue: unknown = Reflect.get(Tangram, 'workerURL');
|
|
794
|
+
url = typeof fallbackValue === 'string' ? fallbackValue : undefined;
|
|
795
|
+
}
|
|
796
|
+
if (!url) throw new Error('Scene has no worker URL');
|
|
797
|
+
let worker = new Worker(url); // eslint-disable-line no-undef
|
|
798
|
+
this.workers[id] = worker;
|
|
799
|
+
|
|
800
|
+
WorkerBroker.addWorker(worker);
|
|
801
|
+
|
|
802
|
+
log('debug', `Scene.makeWorkers: initializing worker ${id}`);
|
|
803
|
+
let _id = id;
|
|
804
|
+
queue.push(WorkerBroker.postMessage(worker, 'self.init', this.id, id, this.num_workers, this.log_level, Utils.device_pixel_ratio, has_element_index_uint, this.external_scripts, this.maxConcurrentTileLoadsPerWorker).then(
|
|
805
|
+
(id) => {
|
|
806
|
+
log('debug', `Scene.makeWorkers: initialized worker ${id}`);
|
|
807
|
+
return id;
|
|
808
|
+
},
|
|
809
|
+
(error) => {
|
|
810
|
+
log('error', `Scene.makeWorkers: failed to initialize worker ${_id}:`, error);
|
|
811
|
+
return Promise.reject(error);
|
|
812
|
+
})
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
this.next_worker = 0;
|
|
817
|
+
return Promise.all(queue).then(() => {
|
|
818
|
+
log.setWorkers?.(this.workers ?? null);
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
destroyWorkers() {
|
|
823
|
+
this.destroyFeatureSelection(); // selections retain worker references and must be recreated
|
|
824
|
+
if (Array.isArray(this.workers)) {
|
|
825
|
+
log.setWorkers?.(null);
|
|
826
|
+
this.workers.forEach((worker) => {
|
|
827
|
+
if ('terminate' in worker && typeof worker.terminate === 'function') worker.terminate();
|
|
828
|
+
});
|
|
829
|
+
this.workers = null;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// Scene is ready for rendering
|
|
834
|
+
ready() {
|
|
835
|
+
if (!this.view.ready() || Object.keys(this.sources).length === 0) {
|
|
836
|
+
return false;
|
|
837
|
+
}
|
|
838
|
+
return true;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// Resize the map when device pixel ratio changes, e.g. when switching between displays
|
|
842
|
+
updateDevicePixelRatio () {
|
|
843
|
+
if (Utils.updateDevicePixelRatio()) {
|
|
844
|
+
WorkerBroker.postMessage(this.activeWorkers, 'self.updateDevicePixelRatio', Utils.device_pixel_ratio)
|
|
845
|
+
.then(() => this.rebuild())
|
|
846
|
+
.then(() => {const {width, height} = this.view.size.css; if (width !== undefined && height !== undefined) this.resizeMap(width, height);});
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
resizeMap(width: number, height: number) {
|
|
851
|
+
if (width === 0 && height === 0) {
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
this.dirty = true;
|
|
856
|
+
this.view.setViewportSize(width, height);
|
|
857
|
+
if (this.gl && this.owns_gl) {
|
|
858
|
+
Context.resize(this.gl, width, height, Utils.device_pixel_ratio);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
withWebGLContext<Result>(callback: () => Result): Result {
|
|
863
|
+
return this.webgl_context_scope ? this.webgl_context_scope(callback) : callback();
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
resetWebGLState() {
|
|
867
|
+
ShaderProgram.resetCurrent();
|
|
868
|
+
Texture.resetBindings();
|
|
869
|
+
if (this.render_states) {
|
|
870
|
+
this.render_states.invalidate();
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// Request scene be redrawn at next animation loop
|
|
875
|
+
requestRedraw() {
|
|
876
|
+
this.dirty = true;
|
|
877
|
+
if (this.render_loop === false && this.redraw_callback) {
|
|
878
|
+
this.redraw_callback();
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// Redraw scene immediately - don't wait for animation loop
|
|
883
|
+
// Use sparingly, but for cases where you need the closest possible sync with other UI elements,
|
|
884
|
+
// such as other, non-WebGL map layers (e.g. Leaflet raster layers, markers, etc.)
|
|
885
|
+
immediateRedraw() {
|
|
886
|
+
this.dirty = true;
|
|
887
|
+
this.update();
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
renderLoop () {
|
|
891
|
+
this.render_loop_active = true; // only let the render loop instantiate once
|
|
892
|
+
|
|
893
|
+
// Update and render the scene
|
|
894
|
+
this.update();
|
|
895
|
+
|
|
896
|
+
this.processTasks();
|
|
897
|
+
|
|
898
|
+
// Request the next frame if not scheduled to stop
|
|
899
|
+
if (!this.render_loop_stop) {
|
|
900
|
+
window.requestAnimationFrame(this.renderLoop.bind(this));
|
|
901
|
+
}
|
|
902
|
+
else {
|
|
903
|
+
this.render_loop_stop = false;
|
|
904
|
+
this.render_loop_active = false;
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// Advance background work normally serviced by Tangram's animation loop.
|
|
909
|
+
// Host-driven renderers must call this once per frame so tile labels and
|
|
910
|
+
// other incremental work can complete without a standalone render loop.
|
|
911
|
+
processTasks () {
|
|
912
|
+
Task.setState({ user_moving_view: this.view.user_input_active });
|
|
913
|
+
Task.processAll();
|
|
914
|
+
// Host loops may stop when no mesh is currently visible. Cooperative
|
|
915
|
+
// label work must still receive its next bounded slice to finish a stage.
|
|
916
|
+
if (Task.queue.length) this.requestRedraw();
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// Setup the render loop
|
|
920
|
+
setupRenderLoop() {
|
|
921
|
+
if (!this.render_loop_active) {
|
|
922
|
+
setTimeout(() => { this.renderLoop(); }, 0); // delay start by one tick
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
update({ force = false, renderPass = null }: {force?: boolean; renderPass?: RenderPass | null} = {}) {
|
|
927
|
+
return this.withWebGLContext(() => {
|
|
928
|
+
if (force) {
|
|
929
|
+
this.dirty = true;
|
|
930
|
+
}
|
|
931
|
+
if (this.external_gl) {
|
|
932
|
+
this.resetWebGLState();
|
|
933
|
+
}
|
|
934
|
+
return this.updateScene({ renderPass });
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
updateScene({ renderPass = null }: {renderPass?: RenderPass | null} = {}) {
|
|
939
|
+
// Capture time once per draw; hosts freeze it across all eyes of a logical frame.
|
|
940
|
+
this.animation_time = this.host_animation_time ?? ((Date.now() - this.start_time) / 1000);
|
|
941
|
+
// Determine which passes (if any) to render
|
|
942
|
+
let main = this.dirty;
|
|
943
|
+
let selection = this.selection ? this.activeSelection.hasPendingRequests() : false;
|
|
944
|
+
var will_render = !(
|
|
945
|
+
(main === false && selection === false) ||
|
|
946
|
+
this.initialized === false ||
|
|
947
|
+
this.updating > 0 ||
|
|
948
|
+
this.ready() === false
|
|
949
|
+
);
|
|
950
|
+
|
|
951
|
+
// Pre-render loop hook
|
|
952
|
+
this.trigger('pre_update', will_render);
|
|
953
|
+
|
|
954
|
+
// Update view (needs to update user input timer even if no render will occur)
|
|
955
|
+
this.view.update(this.host_animation_time === null ? undefined : this.start_time + this.host_animation_time * 1000);
|
|
956
|
+
|
|
957
|
+
// Bail if no need to render
|
|
958
|
+
if (!will_render) {
|
|
959
|
+
return false;
|
|
960
|
+
}
|
|
961
|
+
this.dirty = false; // subclasses can set this back to true when animation is needed
|
|
962
|
+
|
|
963
|
+
// Render the scene
|
|
964
|
+
this.updateDevicePixelRatio();
|
|
965
|
+
this.render({ main, selection, renderPass });
|
|
966
|
+
this.updateViewComplete(); // fires event when rendered tile set or style changes
|
|
967
|
+
this.media_capture.completeScreenshot(); // completes screenshot capture if requested
|
|
968
|
+
|
|
969
|
+
// Post-render loop hook
|
|
970
|
+
this.trigger('post_update', will_render);
|
|
971
|
+
|
|
972
|
+
// Redraw every frame if animating
|
|
973
|
+
if (this.animated === true || this.view.isAnimating()) {
|
|
974
|
+
this.dirty = true;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
this.frame++;
|
|
978
|
+
log('trace', 'Scene.render()');
|
|
979
|
+
return true;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
// Accepts flags indicating which render passes should be made
|
|
983
|
+
render({ main, selection, renderPass = null }: {main: boolean; selection: boolean; renderPass?: RenderPass | null}) {
|
|
984
|
+
var gl = this.gl;
|
|
985
|
+
|
|
986
|
+
this.updateBackground();
|
|
987
|
+
const lights = this.lights ?? {};
|
|
988
|
+
Object.keys(lights).forEach(i => lights[i].update());
|
|
989
|
+
|
|
990
|
+
// Render main pass
|
|
991
|
+
this.render_count_changed = false;
|
|
992
|
+
if (main) {
|
|
993
|
+
this.render_count = this.renderPass('program', { renderPass });
|
|
994
|
+
this.last_main_render = this.frame;
|
|
995
|
+
|
|
996
|
+
// Update feature selection map if necessary
|
|
997
|
+
if (this.render_count !== this.last_render_count) {
|
|
998
|
+
this.render_count_changed = true;
|
|
999
|
+
this.logFirstFrame();
|
|
1000
|
+
|
|
1001
|
+
this.getFeatureSelectionMapSize().then(size => {
|
|
1002
|
+
this.selection_feature_count = size;
|
|
1003
|
+
log('info', `Scene: rendered ${this.render_count} primitives (${size} features in selection map)`);
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
this.last_render_count = this.render_count;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
// Render selection pass (if needed)
|
|
1010
|
+
if (selection) {
|
|
1011
|
+
// Permit retrying an unfinished pass; delayed readback remains locked
|
|
1012
|
+
// between frames until every selection pipeline is ready.
|
|
1013
|
+
this.selection_render_pending = false;
|
|
1014
|
+
if (this.view.panning || this.view.user_input_active) {
|
|
1015
|
+
this.activeSelection.clearPendingRequests();
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
// Only re-render if selection buffer is out of date (relative to main render buffer)
|
|
1020
|
+
// and not locked (e.g. no tiles are actively building)
|
|
1021
|
+
if (!this.activeSelection.locked && this.last_selection_render < this.last_main_render) {
|
|
1022
|
+
if (this.activeSelection.framebuffer) {
|
|
1023
|
+
// An external host pass may still be open. Use a separate
|
|
1024
|
+
// encoder, and submit selection before its asynchronous copy.
|
|
1025
|
+
const device = this.device;
|
|
1026
|
+
if (!device) throw new Error('Scene selection framebuffer has no device');
|
|
1027
|
+
let encoder = device.type === 'webgpu' ? device.createCommandEncoder({id: 'tangram-selection'}) : null;
|
|
1028
|
+
let selection_pass = null;
|
|
1029
|
+
try {
|
|
1030
|
+
// Host eye scissors use full-canvas origins; selection targets
|
|
1031
|
+
// are view-local, including the attachment clear at pass creation.
|
|
1032
|
+
const {width, height} = this.activeSelection.fbo_size;
|
|
1033
|
+
selection_pass = (encoder ?? device).beginRenderPass({
|
|
1034
|
+
framebuffer: this.activeSelection.framebuffer,
|
|
1035
|
+
parameters: {viewport: [0, 0, width, height], scissorRect: [0, 0, width, height]},
|
|
1036
|
+
clearColor: FeatureSelection.defaultColor,
|
|
1037
|
+
clearDepth: 1
|
|
1038
|
+
});
|
|
1039
|
+
this.renderPass('selection_program', {
|
|
1040
|
+
allow_blend: false,
|
|
1041
|
+
renderPass: selection_pass
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
finally {
|
|
1045
|
+
try {
|
|
1046
|
+
selection_pass?.end();
|
|
1047
|
+
if (encoder && selection_pass) {
|
|
1048
|
+
const commands = encoder.finish();
|
|
1049
|
+
encoder = null; // finish releases the encoder; submit releases the commands.
|
|
1050
|
+
device.submit(commands);
|
|
1051
|
+
}
|
|
1052
|
+
} finally {
|
|
1053
|
+
encoder?.destroy();
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
else {
|
|
1058
|
+
this.activeSelection.bind(); // switch to FBO
|
|
1059
|
+
this.renderPass(
|
|
1060
|
+
'selection_program', // render w/alternate program
|
|
1061
|
+
{ allow_blend: false });
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
// Device render passes own state restoration; only classic rendering binds raw targets.
|
|
1065
|
+
if (!this.activeSelection.framebuffer) {
|
|
1066
|
+
const context = this.activeGL, color = this.background?.computed_color;
|
|
1067
|
+
if (!this.canvas || !color) throw new Error('Scene selection has no canvas/background');
|
|
1068
|
+
context.bindFramebuffer(context.FRAMEBUFFER, null);
|
|
1069
|
+
context.viewport(0, 0, this.canvas.width, this.canvas.height);
|
|
1070
|
+
context.clearColor(color[0], color[1], color[2], color[3]);
|
|
1071
|
+
}
|
|
1072
|
+
if (this.selection_render_pending) return true;
|
|
1073
|
+
this.last_selection_render = this.frame;
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
this.activeSelection.read(); // process any pending results from selection buffer
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
return true;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
// Render all active styles, grouped by blend/depth type (opaque, overlay, etc.) and by program (style)
|
|
1083
|
+
// Called both for main render pass, and for secondary passes like selection buffer
|
|
1084
|
+
renderPass(program_key: 'program' | 'selection_program' = 'program', { allow_blend, renderPass = null }: {allow_blend?: boolean; renderPass?: RenderPass | null} = {}) {
|
|
1085
|
+
// optionally force alpha off (e.g. for selection pass)
|
|
1086
|
+
allow_blend = (allow_blend == null) ? true : allow_blend;
|
|
1087
|
+
|
|
1088
|
+
if (this.portable_rendering) {
|
|
1089
|
+
return this.renderPortablePass(program_key, { allow_blend, renderPass });
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
this.clearFrame();
|
|
1093
|
+
|
|
1094
|
+
let count = 0; // how many primitives were rendered
|
|
1095
|
+
let last_blend; // blend mode active in last render pass
|
|
1096
|
+
let last_annotation_depth;
|
|
1097
|
+
|
|
1098
|
+
// Get sorted list of current blend orders, with accompanying list of styles to render for each
|
|
1099
|
+
const blend_orders = this.style_manager.getActiveBlendOrders();
|
|
1100
|
+
if (program_key === 'selection_program' && this.terrain?.occludeAnnotations) this.renderSelectionDepthPrepass(blend_orders, renderPass);
|
|
1101
|
+
for (const { blend_order, styles } of blend_orders) {
|
|
1102
|
+
// Render each style
|
|
1103
|
+
for (let s=0; s < styles.length; s++) {
|
|
1104
|
+
let style = this.liveStyles[styles[s]];
|
|
1105
|
+
if (style == null) {
|
|
1106
|
+
continue;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
// Only update render state when blend mode changes
|
|
1110
|
+
const annotation_depth = this.terrain?.occludeAnnotations && ['points', 'text'].includes(style.baseStyle?.());
|
|
1111
|
+
if (style.blend !== last_blend || annotation_depth !== last_annotation_depth) {
|
|
1112
|
+
let state = Object.assign({},
|
|
1113
|
+
Style.render_states[style.blend], // render state for blend mode
|
|
1114
|
+
{ blend: (allow_blend && style.blend) } // enable/disable blending (e.g. no blend for selection)
|
|
1115
|
+
);
|
|
1116
|
+
if (annotation_depth) {state.depth_test = true; state.depth_write = false;}
|
|
1117
|
+
this.setRenderState(state);
|
|
1118
|
+
if (annotation_depth && this.mesh_render_state) this.mesh_render_state.depthCompare = 'less-equal';
|
|
1119
|
+
if (this.terrain?.occludeAnnotations) this.activeGL.depthFunc(annotation_depth ? this.activeGL.LEQUAL : this.activeGL.LESS);
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
const blend = allow_blend && style.blend;
|
|
1123
|
+
if (blend === 'translucent') {
|
|
1124
|
+
const translucentRenderState = this.mesh_render_state;
|
|
1125
|
+
try {
|
|
1126
|
+
// Depth pre-pass for translucency
|
|
1127
|
+
this.activeGL.colorMask(false, false, false, false);
|
|
1128
|
+
// Resource-backed WebGL draws reapply luma pipeline parameters;
|
|
1129
|
+
// raw GL changes alone cannot control their depth/color passes.
|
|
1130
|
+
this.mesh_render_state = {...translucentRenderState, colorMask: 0};
|
|
1131
|
+
this.renderStyle(style.name, program_key, blend_order, null, renderPass);
|
|
1132
|
+
|
|
1133
|
+
this.activeGL.colorMask(true, true, true, true);
|
|
1134
|
+
this.activeGL.depthFunc(this.activeGL.EQUAL);
|
|
1135
|
+
this.mesh_render_state = {...translucentRenderState, colorMask: 15, depthCompare: 'equal'};
|
|
1136
|
+
|
|
1137
|
+
// Stencil buffer mask prevents overlap/flicker from compounding alpha of overlapping polys
|
|
1138
|
+
this.activeGL.enable(this.activeGL.STENCIL_TEST);
|
|
1139
|
+
this.activeGL.clearStencil(0);
|
|
1140
|
+
this.activeGL.clear(this.activeGL.STENCIL_BUFFER_BIT);
|
|
1141
|
+
this.activeGL.stencilFunc(this.activeGL.EQUAL, this.activeGL.ZERO, 0xFF);
|
|
1142
|
+
this.activeGL.stencilOp(this.activeGL.KEEP, this.activeGL.KEEP, this.activeGL.INCR);
|
|
1143
|
+
|
|
1144
|
+
// Main render pass
|
|
1145
|
+
count += this.renderStyle(style.name, program_key, blend_order, null, renderPass);
|
|
1146
|
+
} finally {
|
|
1147
|
+
// Disable translucency-specific settings, also after a failed draw.
|
|
1148
|
+
this.mesh_render_state = translucentRenderState;
|
|
1149
|
+
this.activeGL.colorMask(true, true, true, true);
|
|
1150
|
+
this.activeGL.disable(this.activeGL.STENCIL_TEST);
|
|
1151
|
+
this.activeGL.depthFunc(this.activeGL.LESS);
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
else if (blend !== 'opaque' && style.stencil_proxy_tiles === true) {
|
|
1155
|
+
// Mask proxy tiles to with stencil buffer to avoid overlap/flicker from compounding alpha
|
|
1156
|
+
// Find unique levels of proxy tiles to render for this style
|
|
1157
|
+
const proxy_levels = this.activeTileManager.getRenderableTiles()
|
|
1158
|
+
.filter(t => t.meshes[style.name]) // must have meshes for this style
|
|
1159
|
+
.map(t => t.proxy_level) // get the proxy depth
|
|
1160
|
+
.reduce<number[]>((levels, level) => { // count unique proxy depths
|
|
1161
|
+
levels.indexOf(level) > -1 || levels.push(level);
|
|
1162
|
+
return levels;
|
|
1163
|
+
}, [])
|
|
1164
|
+
.sort(); // sort by lower depth first
|
|
1165
|
+
|
|
1166
|
+
if (proxy_levels.length > 1) {
|
|
1167
|
+
// When there are multiple "levels" of tiles to render (e.g. non-proxy and one or more proxy
|
|
1168
|
+
// tile levels, or multiple proxy tile levels but no non-proxy tiles, etc.):
|
|
1169
|
+
// Render each proxy tile level to stencil buffer, masking each level such that it will not
|
|
1170
|
+
// render over any pixel rendered by a previous proxy tile level.
|
|
1171
|
+
this.activeGL.enable(this.activeGL.STENCIL_TEST);
|
|
1172
|
+
this.activeGL.clearStencil(0);
|
|
1173
|
+
this.activeGL.clear(this.activeGL.STENCIL_BUFFER_BIT);
|
|
1174
|
+
this.activeGL.stencilOp(this.activeGL.KEEP, this.activeGL.KEEP, this.activeGL.REPLACE);
|
|
1175
|
+
|
|
1176
|
+
for (let i = 0; i < proxy_levels.length; i++) {
|
|
1177
|
+
// stencil test passes either for zero (not-yet-rendered),
|
|
1178
|
+
// or for other pixels at this proxy level (but not previous proxy levels)
|
|
1179
|
+
this.activeGL.stencilFunc(this.activeGL.GEQUAL, proxy_levels.length - i, 0xFF);
|
|
1180
|
+
count += this.renderStyle(style.name, program_key, blend_order, proxy_levels[i], renderPass);
|
|
1181
|
+
}
|
|
1182
|
+
this.activeGL.disable(this.activeGL.STENCIL_TEST);
|
|
1183
|
+
}
|
|
1184
|
+
else {
|
|
1185
|
+
// No special render handling needed when there are no proxy tiles,
|
|
1186
|
+
// or if there is ONLY a single proxy tile level (e.g. with no non-proxy tiles)
|
|
1187
|
+
count += this.renderStyle(style.name, program_key, blend_order, null, renderPass);
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
else {
|
|
1191
|
+
// Regular render pass (no special blend handling, or selection buffer pass)
|
|
1192
|
+
count += this.renderStyle(style.name, program_key, blend_order, null, renderPass);
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
last_blend = style.blend;
|
|
1196
|
+
last_annotation_depth = annotation_depth;
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
return count;
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
// Render styles through luma pipelines without mutating a raw WebGL state machine.
|
|
1204
|
+
renderPortablePass(program_key: 'program' | 'selection_program', { allow_blend, renderPass }: {allow_blend: boolean; renderPass: RenderPass | null}) {
|
|
1205
|
+
let count = 0;
|
|
1206
|
+
let last_blend;
|
|
1207
|
+
let last_annotation_depth;
|
|
1208
|
+
const blend_orders = this.style_manager.getActiveBlendOrders();
|
|
1209
|
+
if (program_key === 'selection_program' && this.terrain?.occludeAnnotations) this.renderSelectionDepthPrepass(blend_orders, renderPass);
|
|
1210
|
+
for (const { blend_order, styles } of blend_orders) {
|
|
1211
|
+
for (const style_name of styles) {
|
|
1212
|
+
const style = this.liveStyles[style_name];
|
|
1213
|
+
if (!style) {
|
|
1214
|
+
continue;
|
|
1215
|
+
}
|
|
1216
|
+
const annotation_depth = this.terrain?.occludeAnnotations && ['points', 'text'].includes(style.baseStyle?.());
|
|
1217
|
+
if (style.blend !== last_blend || annotation_depth !== last_annotation_depth) {
|
|
1218
|
+
this.setRenderState(Object.assign({}, Style.render_states[style.blend], {
|
|
1219
|
+
blend: allow_blend && style.blend,
|
|
1220
|
+
...(annotation_depth ? {depth_test: true, depth_write: false} : {})
|
|
1221
|
+
}));
|
|
1222
|
+
if (annotation_depth && this.mesh_render_state) this.mesh_render_state.depthCompare = 'less-equal';
|
|
1223
|
+
last_blend = style.blend;
|
|
1224
|
+
last_annotation_depth = annotation_depth;
|
|
1225
|
+
}
|
|
1226
|
+
count += this.renderStyle(style.name, program_key, blend_order, null, renderPass);
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
return count;
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
/** Whether a non-selectable style contributes color depth but cannot write feature IDs. */
|
|
1233
|
+
isTerrainSelectionOccluder(style: {selection?: boolean; baseStyle?(): string; blend: import('../styles/style-runtime-types').StyleState['blend']} | undefined): boolean {
|
|
1234
|
+
return Boolean(this.terrain?.occludeAnnotations && style && !style.selection &&
|
|
1235
|
+
['raster', 'polygons', 'lines'].includes(style.baseStyle?.() ?? '') && Style.render_states[style.blend]?.depth_write !== false);
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
/** Populate non-selectable scene depth before any IDs; pending pipelines keep selection locked for retry. */
|
|
1239
|
+
renderSelectionDepthPrepass(blend_orders: {blend_order?: number; styles: string[]}[], renderPass: import('@luma.gl/core').RenderPass | null): void {
|
|
1240
|
+
const occluders = blend_orders.flatMap(({blend_order, styles}) => styles
|
|
1241
|
+
.filter(name => this.isTerrainSelectionOccluder(this.liveStyles[name])).map(name => ({name, blend_order})));
|
|
1242
|
+
if (!occluders.length) return;
|
|
1243
|
+
const previous = this.mesh_render_state;
|
|
1244
|
+
try {
|
|
1245
|
+
this.setRenderState({depth_test: true, depth_write: true, cull_face: true, blend: false});
|
|
1246
|
+
this.mesh_render_state = {...this.mesh_render_state, colorMask: 0, depthWriteEnabled: true, depthCompare: 'less'};
|
|
1247
|
+
if (!this.portable_rendering) {
|
|
1248
|
+
this.activeGL.colorMask(false, false, false, false); this.activeGL.depthFunc(this.activeGL.LESS);
|
|
1249
|
+
}
|
|
1250
|
+
for (const {name, blend_order} of occluders) this.renderStyle(name, 'program', blend_order, null, renderPass, true);
|
|
1251
|
+
} finally {
|
|
1252
|
+
this.mesh_render_state = previous;
|
|
1253
|
+
if (!this.portable_rendering) this.activeGL.colorMask(true, true, true, true);
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
renderStyle(style_name: string, program_key: 'program' | 'selection_program', blend_order: number | undefined, proxy_level: number | null = null, renderPass: RenderPass | null = null, selectionDepthPass = false) {
|
|
1258
|
+
let style = this.liveStyles[style_name];
|
|
1259
|
+
// Already submitted before any IDs. Repeating it later cannot erase covered IDs.
|
|
1260
|
+
if (program_key === 'selection_program' && this.terrain?.occludeAnnotations && this.isTerrainSelectionOccluder(style)) return 0;
|
|
1261
|
+
let first_for_style = true; // TODO: allow this state to be passed in (for multilpe blend orders, stencil tests, etc)
|
|
1262
|
+
let render_count = 0;
|
|
1263
|
+
let program: ShaderProgram | undefined;
|
|
1264
|
+
|
|
1265
|
+
// Render tile GL geometries
|
|
1266
|
+
let renderable_tiles = this.activeTileManager.getRenderableTiles();
|
|
1267
|
+
|
|
1268
|
+
// For each tile, only include meshes for the blend order currently being rendered
|
|
1269
|
+
// Each draw groups a tile, its meshes and optional missing-detail clip bounds.
|
|
1270
|
+
let tile_meshes = renderable_tiles
|
|
1271
|
+
.filter(t => typeof proxy_level !== 'number' || t.proxy_level === proxy_level) // optional filter by proxy level
|
|
1272
|
+
.flatMap((t): [import('../tile/tile').default, import('../gl/vbo_mesh').default[], readonly [number, number, number, number] | undefined][] => {
|
|
1273
|
+
if (t.meshes[style_name]) {
|
|
1274
|
+
const meshes = t.meshes[style_name].filter(m => m.variant.blend_order === blend_order);
|
|
1275
|
+
if (t.fallback_for) {
|
|
1276
|
+
// Do not duplicate labels or report placeholder feature picks.
|
|
1277
|
+
if (program_key === 'selection_program' ||
|
|
1278
|
+
!isGlobeFallbackStyle(style.name, style.base)) return [];
|
|
1279
|
+
return Array.from(t.fallback_for.values(), clip => [t, meshes, clip]);
|
|
1280
|
+
}
|
|
1281
|
+
return [[t, meshes, undefined]];
|
|
1282
|
+
}
|
|
1283
|
+
return [];
|
|
1284
|
+
});
|
|
1285
|
+
|
|
1286
|
+
// Mesh variants must be rendered in requested order across tiles, to prevent labels that cross
|
|
1287
|
+
// tile boundaries from rendering over adjacent tile features meant to be underneath
|
|
1288
|
+
let max_mesh_order =
|
|
1289
|
+
Math.max(...tile_meshes.map(([, meshes]) => {
|
|
1290
|
+
return Math.max(...meshes.map(m => Number(m.variant.mesh_order)));
|
|
1291
|
+
}));
|
|
1292
|
+
|
|
1293
|
+
// One pass per mesh variant order (loop goes to max value +1 because 0 is a valid order value)
|
|
1294
|
+
for (let mo=0; mo < max_mesh_order + 1; mo++) {
|
|
1295
|
+
// Loop over tiles, with meshes pre-filtered by current blend order
|
|
1296
|
+
for (let [tile, meshes, clip] of tile_meshes) {
|
|
1297
|
+
let first_for_tile = true;
|
|
1298
|
+
|
|
1299
|
+
// Skip proxy tiles if new tiles have finished loading this style
|
|
1300
|
+
if (!tile.shouldProxyForStyle(style_name)) {
|
|
1301
|
+
// log('trace', `Scene.renderStyle(): Skip proxy tile for style '${style_name}' `, tile, tile.proxy_for);
|
|
1302
|
+
continue;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
// Filter meshes further by current variant order
|
|
1306
|
+
const order_meshes = meshes.filter(m => m.variant.mesh_order === mo);
|
|
1307
|
+
if (order_meshes.length === 0) {
|
|
1308
|
+
continue;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
// Style-specific state
|
|
1312
|
+
// Only setup style if rendering for first time this frame
|
|
1313
|
+
// (lazy init, not all styles will be used in all screen views; some styles might be defined but never used)
|
|
1314
|
+
if (first_for_style === true) {
|
|
1315
|
+
first_for_style = false;
|
|
1316
|
+
program = this.setupStyle(style, program_key);
|
|
1317
|
+
if (!program) {
|
|
1318
|
+
// no program found, e.g. happens when rendering selection pass, but style doesn't support selection
|
|
1319
|
+
return 0;
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
// Render each mesh (for current variant order)
|
|
1324
|
+
if (!program) throw new Error('Scene mesh program is not ready');
|
|
1325
|
+
const activeProgram = program;
|
|
1326
|
+
order_meshes.forEach(mesh => {
|
|
1327
|
+
// Tile-specific state
|
|
1328
|
+
if (first_for_tile === true) {
|
|
1329
|
+
first_for_tile = false;
|
|
1330
|
+
this.view.setupTile(tile, activeProgram, clip ?? (mesh.variant.terrainGroundClip ? [0, -4096, 4096, 0] : undefined));
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
// Render this mesh variant
|
|
1334
|
+
if (mesh.variant.terrain) {
|
|
1335
|
+
const now = this.terrainFrameTime ?? performance.now();
|
|
1336
|
+
const origin = this.activeTileManager.getTerrainTransitionOrigin(mesh, now);
|
|
1337
|
+
const duration = mesh.variant.terrainTransitionDuration ?? this.terrain?.transitionDuration ?? 0;
|
|
1338
|
+
const blend = duration ? Math.max(0, Math.min(1, (now - origin) / duration)) : 1;
|
|
1339
|
+
mesh.terrainBlend = blend;
|
|
1340
|
+
const uniforms = {u_terrain_blend: blend, u_terrain_exaggeration: this.terrainExaggeration};
|
|
1341
|
+
if (this.uniform_buffers.TangramTile) this.uniform_buffers.TangramTile.setUniforms(uniforms);
|
|
1342
|
+
else {activeProgram.uniform('1f', 'u_terrain_blend', blend); activeProgram.uniform('1f', 'u_terrain_exaggeration', this.terrainExaggeration);}
|
|
1343
|
+
if (blend < 1) this.requestRedraw();
|
|
1344
|
+
}
|
|
1345
|
+
if (style.render(mesh, {
|
|
1346
|
+
projection: this.view.projection.type,
|
|
1347
|
+
renderPass: renderPass ?? undefined,
|
|
1348
|
+
meshRenderer: this.mesh_renderer,
|
|
1349
|
+
renderState: this.mesh_render_state ?? undefined
|
|
1350
|
+
})) {
|
|
1351
|
+
if (program_key === 'selection_program' || selectionDepthPass) this.selection_render_pending = true;
|
|
1352
|
+
this.requestRedraw();
|
|
1353
|
+
}
|
|
1354
|
+
render_count += mesh.geometry_count;
|
|
1355
|
+
});
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
return render_count;
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
setupStyle(style: import('../styles/style-mixing-types').ManagedStyle, program_key: 'program' | 'selection_program') {
|
|
1363
|
+
// Get shader program from style, lazily compiling if necessary
|
|
1364
|
+
let program: ShaderProgram | undefined;
|
|
1365
|
+
try {
|
|
1366
|
+
program = style.getProgram(program_key);
|
|
1367
|
+
if (!program) {
|
|
1368
|
+
return;
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
catch(error) {
|
|
1372
|
+
this.trigger('warning', {
|
|
1373
|
+
type: 'styles',
|
|
1374
|
+
message: `Error compiling style ${style.name}`,
|
|
1375
|
+
style,
|
|
1376
|
+
shader_errors: style.program && style.program.shader_errors
|
|
1377
|
+
});
|
|
1378
|
+
return;
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
program.use();
|
|
1382
|
+
style.setup();
|
|
1383
|
+
|
|
1384
|
+
const time = this.animated ? this.animation_time : 0;
|
|
1385
|
+
if (this.uniform_buffers.TangramView) {
|
|
1386
|
+
this.uniform_buffers.TangramView.setUniform('u_time', time);
|
|
1387
|
+
this.view.setupProgram(program, this.uniform_buffers);
|
|
1388
|
+
if (this.uniform_buffers.TangramLighting) {
|
|
1389
|
+
this.uniform_buffers.TangramLighting.setUniforms(getPortableLightUniforms(this.getLumaLightDefinitions()));
|
|
1390
|
+
}
|
|
1391
|
+
program.bindUniformBlocks();
|
|
1392
|
+
}
|
|
1393
|
+
else {
|
|
1394
|
+
program.uniform('1f', 'u_time', time);
|
|
1395
|
+
this.view.setupProgram(program);
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
for (let i in this.lights) {
|
|
1399
|
+
this.lights[i].setupProgram(program);
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
return program;
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
clearFrame() {
|
|
1406
|
+
if (!this.initialized) {
|
|
1407
|
+
return;
|
|
1408
|
+
}
|
|
1409
|
+
if (this.portable_rendering) {
|
|
1410
|
+
return;
|
|
1411
|
+
}
|
|
1412
|
+
if (!this.render_states) throw new Error('Scene render state is not initialized');
|
|
1413
|
+
this.render_states.depth_write.set({ depth_write: true });
|
|
1414
|
+
this.activeGL.clear(this.activeGL.COLOR_BUFFER_BIT | this.activeGL.DEPTH_BUFFER_BIT | this.activeGL.STENCIL_BUFFER_BIT);
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
setRenderState({ depth_test, depth_write, cull_face, blend }: {depth_test?: boolean; depth_write?: boolean; cull_face?: boolean; blend?: import('../gpu/tangram_gpu_backend').TangramBlendMode} = {}) {
|
|
1418
|
+
if (!this.initialized) {
|
|
1419
|
+
return;
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
// Defaults
|
|
1423
|
+
// TODO: when we abstract out support for multiple render passes, these can be per-pass config options
|
|
1424
|
+
let render_states = this.render_states;
|
|
1425
|
+
const defaults = this.portable_rendering ? {
|
|
1426
|
+
depth_test: true,
|
|
1427
|
+
depth_write: true,
|
|
1428
|
+
culling: true,
|
|
1429
|
+
blending: false as const
|
|
1430
|
+
} : this.getRenderStateDefaults();
|
|
1431
|
+
depth_test = (depth_test === false) ? false : defaults.depth_test; // default true
|
|
1432
|
+
depth_write = (depth_write === false) ? false : defaults.depth_write; // default true
|
|
1433
|
+
cull_face = (cull_face === false) ? false : defaults.culling; // default true
|
|
1434
|
+
blend = (blend != null) ? blend : defaults.blending; // default false
|
|
1435
|
+
this.mesh_render_state = this.mesh_renderer ?
|
|
1436
|
+
this.mesh_renderer.getRenderPipelineParameters({
|
|
1437
|
+
depthTest: depth_test,
|
|
1438
|
+
depthWrite: depth_write,
|
|
1439
|
+
cullFace: cull_face,
|
|
1440
|
+
blend
|
|
1441
|
+
}) : null;
|
|
1442
|
+
|
|
1443
|
+
if (this.portable_rendering) {
|
|
1444
|
+
return;
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
// Reset frame state
|
|
1448
|
+
if (!render_states) throw new Error('Scene render state is not initialized');
|
|
1449
|
+
let gl = this.activeGL;
|
|
1450
|
+
|
|
1451
|
+
render_states.depth_test.set({ depth_test });
|
|
1452
|
+
render_states.depth_write.set({ depth_write });
|
|
1453
|
+
render_states.culling.set({ cull: cull_face, face: render_states.defaults.culling_face });
|
|
1454
|
+
|
|
1455
|
+
// Blending of alpha channel is modified to account for WebGL alpha behavior, see:
|
|
1456
|
+
// http://webglfundamentals.org/webgl/lessons/webgl-and-alpha.html
|
|
1457
|
+
// http://stackoverflow.com/a/11533416
|
|
1458
|
+
if (blend) {
|
|
1459
|
+
// Opaque: all source, no destination
|
|
1460
|
+
if (blend === 'opaque') {
|
|
1461
|
+
render_states.blending.set({
|
|
1462
|
+
blend: false
|
|
1463
|
+
});
|
|
1464
|
+
}
|
|
1465
|
+
// Traditional alpha blending
|
|
1466
|
+
else if (blend === 'overlay' || blend === 'inlay' || blend === 'translucent') {
|
|
1467
|
+
render_states.blending.set({
|
|
1468
|
+
blend: true,
|
|
1469
|
+
src: gl.SRC_ALPHA, dst: gl.ONE_MINUS_SRC_ALPHA,
|
|
1470
|
+
src_alpha: gl.ONE, dst_alpha: gl.ONE_MINUS_SRC_ALPHA
|
|
1471
|
+
});
|
|
1472
|
+
}
|
|
1473
|
+
// Additive blending
|
|
1474
|
+
else if (blend === 'add') {
|
|
1475
|
+
render_states.blending.set({
|
|
1476
|
+
blend: true,
|
|
1477
|
+
src: gl.ONE, dst: gl.ONE
|
|
1478
|
+
});
|
|
1479
|
+
}
|
|
1480
|
+
// Multiplicative blending
|
|
1481
|
+
else if (blend === 'multiply') {
|
|
1482
|
+
render_states.blending.set({
|
|
1483
|
+
blend: true,
|
|
1484
|
+
src: gl.ZERO, dst: gl.SRC_COLOR
|
|
1485
|
+
});
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
else {
|
|
1489
|
+
render_states.blending.set({ blend: false });
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
// Request feature selection at given pixel. Runs async and returns results via a promise.
|
|
1494
|
+
/** Classic render-state defaults are absent on device-only scenes. */
|
|
1495
|
+
private getRenderStateDefaults(): RenderStateManager['defaults'] {
|
|
1496
|
+
if (!this.render_states) throw new Error('Scene render state is not initialized');
|
|
1497
|
+
return this.render_states.defaults;
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
getFeatureAt(pixel: {x: number; y: number}, { radius, renderViewId, width, height }:
|
|
1501
|
+
{radius?: number; renderViewId?: string; width?: number; height?: number} = {}): Promise<FeatureSelectionResult | undefined> {
|
|
1502
|
+
if (!this.initialized) {
|
|
1503
|
+
log('debug', 'Scene.getFeatureAt() called before scene was initialized');
|
|
1504
|
+
return Promise.resolve(undefined);
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
// skip selection if no interactive features
|
|
1508
|
+
if (this.selection_feature_count === 0) {
|
|
1509
|
+
return Promise.resolve(undefined);
|
|
1510
|
+
}
|
|
1511
|
+
width ??= this.view.size.css.width;
|
|
1512
|
+
height ??= this.view.size.css.height;
|
|
1513
|
+
if (width === undefined || height === undefined) return Promise.resolve(undefined);
|
|
1514
|
+
|
|
1515
|
+
// only instantiate feature selection on-demand
|
|
1516
|
+
if (renderViewId === undefined && !this.selection) {
|
|
1517
|
+
this.resetFeatureSelection();
|
|
1518
|
+
}
|
|
1519
|
+
const selection = renderViewId === undefined ? this.activeSelection : this.getFeatureSelectionView(renderViewId).selection;
|
|
1520
|
+
|
|
1521
|
+
// Scale point and radius to [0..1] range
|
|
1522
|
+
let point = {
|
|
1523
|
+
x: pixel.x / width,
|
|
1524
|
+
y: pixel.y / height
|
|
1525
|
+
};
|
|
1526
|
+
|
|
1527
|
+
let selectionRadius: {x: number; y: number} | null;
|
|
1528
|
+
if (radius !== undefined && radius > 0) {
|
|
1529
|
+
selectionRadius = {
|
|
1530
|
+
x: radius / width,
|
|
1531
|
+
y: radius / height
|
|
1532
|
+
};
|
|
1533
|
+
}
|
|
1534
|
+
else {
|
|
1535
|
+
selectionRadius = null;
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
return selection.getFeatureAt(point, { radius: selectionRadius }).
|
|
1539
|
+
then(selection => Object.assign(selection, { pixel })).
|
|
1540
|
+
catch(error => Promise.resolve({ error }));
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
// Query features within visible tiles, with optional filter conditions
|
|
1544
|
+
async queryFeatures({ filter, unique = true, group_by = null, visible = null, geometry = false }: import('../types').SceneQueryOptions = {}): Promise<import('../types').SceneFeature[] | Record<string, import('../types').SceneFeature[]>> {
|
|
1545
|
+
if (!this.initialized) {
|
|
1546
|
+
return [];
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
filter = Utils.serializeWithFunctions(filter);
|
|
1550
|
+
|
|
1551
|
+
// Optional uniqueify criteria
|
|
1552
|
+
// Valid values: true, false/null, single property name, or array of property names
|
|
1553
|
+
unique = (typeof unique === 'string') ? [unique] : unique;
|
|
1554
|
+
const uniqueify_on_id = (unique === true || (Array.isArray(unique) && unique.indexOf('$id') > -1));
|
|
1555
|
+
const uniqueify = unique && ((obj: import('../types').SceneFeature) => {
|
|
1556
|
+
const properties = Array.isArray(unique) ? sliceObject(obj.properties, unique) : obj.properties;
|
|
1557
|
+
const id = uniqueify_on_id ? obj.id : null;
|
|
1558
|
+
if (geometry) {
|
|
1559
|
+
// when `geometry` flag is set, we need to uniqueify based on *both* feature properties and geometry
|
|
1560
|
+
return JSON.stringify({ geometry: obj.geometry, properties, id });
|
|
1561
|
+
}
|
|
1562
|
+
return JSON.stringify({ properties, id });
|
|
1563
|
+
});
|
|
1564
|
+
|
|
1565
|
+
// Optional grouping criteria
|
|
1566
|
+
// Valid values: false/null, single property name, or array of property names
|
|
1567
|
+
const groupProperties = group_by;
|
|
1568
|
+
const group = groupProperties && ((obj: Record<string, unknown>) => {
|
|
1569
|
+
return typeof groupProperties === 'string' ? obj[groupProperties] : JSON.stringify(sliceObject(obj, groupProperties));
|
|
1570
|
+
});
|
|
1571
|
+
|
|
1572
|
+
const tile_keys = this.activeTileManager.getRenderableTiles().map(t => t.key);
|
|
1573
|
+
const results = await WorkerBroker.postMessage<import('../types').SceneFeature[]>(this.activeWorkers, 'self.queryFeatures', { filter, visible, geometry, tile_keys });
|
|
1574
|
+
const features: import('../types').SceneFeature[] = [];
|
|
1575
|
+
const keys: Record<string, boolean> = {};
|
|
1576
|
+
const groups: Record<string, import('../types').SceneFeature[]> = {};
|
|
1577
|
+
|
|
1578
|
+
results.forEach(r => r.forEach(feature => {
|
|
1579
|
+
if (uniqueify) {
|
|
1580
|
+
const str = uniqueify(feature);
|
|
1581
|
+
if (keys[str]) {
|
|
1582
|
+
return;
|
|
1583
|
+
}
|
|
1584
|
+
keys[str] = true;
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
if (group) {
|
|
1588
|
+
const str = String(group(feature.properties));
|
|
1589
|
+
groups[str] = groups[str] || [];
|
|
1590
|
+
groups[str].push(feature);
|
|
1591
|
+
}
|
|
1592
|
+
else {
|
|
1593
|
+
features.push(feature);
|
|
1594
|
+
}
|
|
1595
|
+
}));
|
|
1596
|
+
return group ? groups : features; // returned grouped results, or all results
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
// Rebuild all tiles, without re-parsing the config or re-compiling styles
|
|
1600
|
+
// sync: boolean of whether to sync the config object to the worker
|
|
1601
|
+
// sources: optional array of data sources to selectively rebuild (by default all our rebuilt)
|
|
1602
|
+
rebuild({ initial = false, new_generation = true, sources = null, serialize_funcs, profile = false, fade_in = false, preserveTileCache = false, preserveTime = false, terrainRibbonZoom }: RebuildOptions = {}) {
|
|
1603
|
+
return new Promise<boolean | void>((resolve, reject) => {
|
|
1604
|
+
// Skip rebuild if already in progress
|
|
1605
|
+
if (this.building) {
|
|
1606
|
+
// Queue up to one rebuild call at a time, only save last request
|
|
1607
|
+
if (this.building.queued) {
|
|
1608
|
+
// notify previous request that it did not complete
|
|
1609
|
+
log('debug', 'Scene.rebuild: request superceded by a newer call');
|
|
1610
|
+
this.building.queued.resolve(false); // false flag indicates rebuild request was superceded
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
// Save queued request
|
|
1614
|
+
let options = { initial, new_generation, sources, serialize_funcs, profile, fade_in, preserveTileCache, preserveTime, terrainRibbonZoom };
|
|
1615
|
+
this.building.queued = { resolve, reject, options };
|
|
1616
|
+
log('trace', 'Scene.rebuild(): queuing request');
|
|
1617
|
+
return;
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
// Admission can reject before creating a build/stage. Otherwise a
|
|
1621
|
+
// synchronous budget error would leave recovery queued behind a
|
|
1622
|
+
// build that never started and can never settle.
|
|
1623
|
+
if (this.terrain && preserveTileCache && preserveTime) this.reserveTerrainInputs(true);
|
|
1624
|
+
|
|
1625
|
+
// Track tile build state
|
|
1626
|
+
this.building = { resolve, reject, initial };
|
|
1627
|
+
this.terrainTransaction = Boolean(this.terrain && preserveTileCache && preserveTime);
|
|
1628
|
+
if (this.terrainTransaction) this.activeTileManager.forEachTile(tile => tile.beginTerrainStage());
|
|
1629
|
+
|
|
1630
|
+
// Profiling
|
|
1631
|
+
if (profile) {
|
|
1632
|
+
this.debug.profile('Scene.rebuild');
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
// Increment generation to ensure style/tile building stay in sync
|
|
1636
|
+
// (skipped if calling function already incremented)
|
|
1637
|
+
if (new_generation) {
|
|
1638
|
+
this.generation = ++Scene.generation;
|
|
1639
|
+
for (let style in this.styles) {
|
|
1640
|
+
this.liveStyles[style].setGeneration(this.generation);
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
if (this.terrainTransaction) {
|
|
1645
|
+
const selections = new Set(Array.from(this.selection_views.values(), state => state.selection));
|
|
1646
|
+
if (this.selection) selections.add(this.selection);
|
|
1647
|
+
for (const selection of selections) selection.invalidateReadback();
|
|
1648
|
+
this.last_selection_render = -1;
|
|
1649
|
+
for (const state of this.selection_views.values()) state.lastRender = -1;
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
const build = this.building;
|
|
1653
|
+
const rebuildTiles = () => {
|
|
1654
|
+
if (this.building !== build || !this.tile_manager) return;
|
|
1655
|
+
this.resetWorkerFeatureSelection(sources);
|
|
1656
|
+
if (!preserveTime) this.resetTime();
|
|
1657
|
+
// Rebuild visible tiles only after every terrain worker has replaced its inputs.
|
|
1658
|
+
if (!preserveTileCache) this.activeTileManager.pruneToVisibleTiles();
|
|
1659
|
+
this.activeTileManager.forEachTile(tile => {
|
|
1660
|
+
if (!sources || sources.indexOf(tile.source.name) > -1) this.activeTileManager.buildTile(tile, { fade_in });
|
|
1661
|
+
});
|
|
1662
|
+
this.activeTileManager.updateTilesForView();
|
|
1663
|
+
this.activeTileManager.checkBuildQueue();
|
|
1664
|
+
};
|
|
1665
|
+
const rejectTerrainConfig = (error: unknown) => {
|
|
1666
|
+
if (this.building !== build || !this.tile_manager) return;
|
|
1667
|
+
build.terrainConfigPending = false;
|
|
1668
|
+
this.tileManagerBuildError(error);
|
|
1669
|
+
this.tileManagerBuildDone();
|
|
1670
|
+
};
|
|
1671
|
+
if (this.terrainTransaction) {
|
|
1672
|
+
// An empty tile queue is not proof that workers released a failed snapshot.
|
|
1673
|
+
// Wait for configuration acknowledgements before even a zero-tile recovery settles.
|
|
1674
|
+
build.terrainConfigPending = true;
|
|
1675
|
+
try {Promise.resolve(this.syncConfigToWorker({serialize_funcs, terrainRibbonZoom})).then(() => {
|
|
1676
|
+
build.terrainConfigPending = false;
|
|
1677
|
+
rebuildTiles();
|
|
1678
|
+
}).catch(rejectTerrainConfig);}
|
|
1679
|
+
catch (error) {rejectTerrainConfig(error);}
|
|
1680
|
+
} else {
|
|
1681
|
+
this.syncConfigToWorker({serialize_funcs});
|
|
1682
|
+
rebuildTiles();
|
|
1683
|
+
}
|
|
1684
|
+
}).then(completed => {
|
|
1685
|
+
// Profiling
|
|
1686
|
+
if (profile) {
|
|
1687
|
+
this.debug.profileEnd('Scene.rebuild');
|
|
1688
|
+
}
|
|
1689
|
+
return completed;
|
|
1690
|
+
});
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
/** Record the first current-generation failure; settle only after work drains. */
|
|
1694
|
+
tileManagerBuildError(error: unknown) {
|
|
1695
|
+
if (this.building && !this.building.error) {
|
|
1696
|
+
this.building.error = error instanceof Error ? error : new Error(String(error));
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
// Tile manager finished building tiles
|
|
1701
|
+
// TODO move to tile manager
|
|
1702
|
+
tileManagerBuildDone() {
|
|
1703
|
+
if (this.building?.terrainConfigPending) return;
|
|
1704
|
+
TextCanvas.pruneTextCache();
|
|
1705
|
+
|
|
1706
|
+
if (this.building) {
|
|
1707
|
+
log('info', 'Scene: build geometry finished');
|
|
1708
|
+
if (this.terrainTransaction) {
|
|
1709
|
+
const time = performance.now();
|
|
1710
|
+
const build = this.building;
|
|
1711
|
+
if (this.terrainStagedInputLease && this.terrainStagedInputSnapshot) {
|
|
1712
|
+
if (build.error) this.terrainFailedInputs.push({lease: this.terrainStagedInputLease, snapshot: this.terrainStagedInputSnapshot});
|
|
1713
|
+
else {
|
|
1714
|
+
this.terrainInputLease?.release(); this.terrainInputLease = this.terrainStagedInputLease;
|
|
1715
|
+
this.terrainInputSnapshot = this.terrainStagedInputSnapshot;
|
|
1716
|
+
}
|
|
1717
|
+
this.terrainStagedInputLease = undefined; this.terrainStagedInputSnapshot = undefined;
|
|
1718
|
+
}
|
|
1719
|
+
if (!build.error) {
|
|
1720
|
+
for (const input of this.terrainFailedInputs) input.lease.release();
|
|
1721
|
+
this.terrainFailedInputs = [];
|
|
1722
|
+
}
|
|
1723
|
+
if (!build.error && this.terrain) this.activeTileManager.startTerrainTransition(this.terrain.transitionGeneration, time);
|
|
1724
|
+
this.activeTileManager.forEachTile(tile => {if (build.error) tile.discardTerrainStage(); else tile.commitTerrainStage(time);});
|
|
1725
|
+
if (!this.building.error) this.terrainDisplayGeneration = this.generation;
|
|
1726
|
+
this.terrainTransaction = false;
|
|
1727
|
+
this.activeTileManager.publishTerrainDisplay();
|
|
1728
|
+
this.requestRedraw();
|
|
1729
|
+
}
|
|
1730
|
+
if (this.building.error) {
|
|
1731
|
+
this.building.reject(this.building.error);
|
|
1732
|
+
} else {
|
|
1733
|
+
this.logFirstBuild();
|
|
1734
|
+
this.building.resolve(true);
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
// Another rebuild queued?
|
|
1738
|
+
var queued = this.building.queued;
|
|
1739
|
+
this.building = null;
|
|
1740
|
+
if (queued) {
|
|
1741
|
+
log('debug', 'Scene: starting queued rebuild() request');
|
|
1742
|
+
this.rebuild(queued.options).then(queued.resolve, queued.reject);
|
|
1743
|
+
}
|
|
1744
|
+
else {
|
|
1745
|
+
this.activeTileManager.updateLabels(); // refresh label if nothing to rebuild
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
/**
|
|
1751
|
+
Load (or reload) the scene config
|
|
1752
|
+
@return {Promise}
|
|
1753
|
+
*/
|
|
1754
|
+
async loadScene(config_source: SceneDefinition | null = null, { base_path, file_type }: SceneLoadOptions = {}) {
|
|
1755
|
+
this.config_source = config_source || this.config_source;
|
|
1756
|
+
|
|
1757
|
+
if (typeof this.config_source === 'string') {
|
|
1758
|
+
this.base_path = URLs.pathForURL(base_path || this.config_source);
|
|
1759
|
+
}
|
|
1760
|
+
else {
|
|
1761
|
+
this.base_path = URLs.pathForURL(base_path);
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
// backwards compatibility for accessing base path under previous name
|
|
1765
|
+
// TODO: schedule for deprecation
|
|
1766
|
+
this.config_path = this.base_path;
|
|
1767
|
+
|
|
1768
|
+
const { config, bundle, texture_nodes } = await SceneLoader.loadScene(
|
|
1769
|
+
this.config_source ?? {},
|
|
1770
|
+
{ path: this.base_path, type: file_type, cameraMode: this.view.camera_mode });
|
|
1771
|
+
|
|
1772
|
+
// Finalized registries are consumed by owners that validate each
|
|
1773
|
+
// authored source, style, camera, and light value before use.
|
|
1774
|
+
this.config = config as SceneRuntimeConfiguration;
|
|
1775
|
+
this.config_bundle = bundle;
|
|
1776
|
+
return { texture_nodes }; // pass along texture nodes for resolution after global property subtistution
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
// Add source to a scene, arguments `name` and `config` need to be provided:
|
|
1780
|
+
// - If the name doesn't match a sources it will create it
|
|
1781
|
+
// - the `config` obj follow the YAML scene spec, ex: ```{type: 'TopoJSON', url: "//tile.mapzen.com/mapzen/vector/v1/all/{z}/{x}/{y}.topojson"]}```
|
|
1782
|
+
// that looks like:
|
|
1783
|
+
//
|
|
1784
|
+
// scene.setDataSource("osm", {type: 'TopoJSON', url: "//tile.mapzen.com/mapzen/vector/v1/all/{z}/{x}/{y}.topojson" });
|
|
1785
|
+
//
|
|
1786
|
+
// - also can be pass a ```data``` obj: ```{type: 'GeoJSON', data: JSObj ]}```
|
|
1787
|
+
//
|
|
1788
|
+
// var geojson_data = {};
|
|
1789
|
+
// ...
|
|
1790
|
+
// scene.setDataSource("dynamic_data", {type: 'GeoJSON', data: geojson_data });
|
|
1791
|
+
//
|
|
1792
|
+
setDataSource (name: string, config: import('./scene-resource-types').SceneSourceDefinition) {
|
|
1793
|
+
if (!name || !config || !config.type || (!config.url && !config.tilejson && !config.data)) {
|
|
1794
|
+
log('error', 'No name provided or not a valid config:', name, config);
|
|
1795
|
+
return;
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
let load = (this.liveConfig.sources[name] == null);
|
|
1799
|
+
let source = this.liveConfig.sources[name] = Object.assign({}, config);
|
|
1800
|
+
|
|
1801
|
+
// Convert raw data into blob URL
|
|
1802
|
+
if (source.data && typeof source.data === 'object') {
|
|
1803
|
+
const url = URLs.createObjectURL(new Blob([JSON.stringify(source.data)], { type: 'application/json' }));
|
|
1804
|
+
if (typeof url !== 'string') throw new Error('Could not create a data source URL');
|
|
1805
|
+
source.url = url;
|
|
1806
|
+
delete source.data;
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
if (load) {
|
|
1810
|
+
return this.updateConfig({ rebuild: { sources: [name] } });
|
|
1811
|
+
} else {
|
|
1812
|
+
// Source-only updates keep the lightweight rebuild path, but must
|
|
1813
|
+
// replace main-thread instances and notify attribution subscribers.
|
|
1814
|
+
this.createDataSources();
|
|
1815
|
+
this.trigger('update', {config: this.config});
|
|
1816
|
+
return this.rebuild({ sources: [name] });
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
// (Re-)create all data sources. Re-layout view and rebuild tiles when either:
|
|
1821
|
+
// 1) all tiles if `rebuild_all` parameter is specified (used when loading a new scene)
|
|
1822
|
+
// 2) the data source has changed in a way that affects tile layout (e.g. tile size, max_zoom, etc.)
|
|
1823
|
+
createDataSources(rebuild_all = false) {
|
|
1824
|
+
const reset: string[] = []; // sources to reset
|
|
1825
|
+
const prev_source_names = Object.keys(this.sources);
|
|
1826
|
+
let source_id = 0;
|
|
1827
|
+
|
|
1828
|
+
for (var name in this.liveConfig.sources) {
|
|
1829
|
+
const source = this.liveConfig.sources[name];
|
|
1830
|
+
const prev_source = this.sources[name];
|
|
1831
|
+
|
|
1832
|
+
try {
|
|
1833
|
+
const config = { ...source, name, id: source_id++ };
|
|
1834
|
+
const created = DataSource.create(config, this.sources);
|
|
1835
|
+
if (!created) {
|
|
1836
|
+
throw {};
|
|
1837
|
+
}
|
|
1838
|
+
this.sources[name] = created;
|
|
1839
|
+
}
|
|
1840
|
+
catch(e) {
|
|
1841
|
+
delete this.sources[name];
|
|
1842
|
+
const message = `Could not create data source: ${e instanceof Error ? e.message : (e && typeof e === 'object' ? Reflect.get(e, 'message') : undefined)}`;
|
|
1843
|
+
log('warn', `Scene: ${message}`, source);
|
|
1844
|
+
this.trigger('warning', { type: 'sources', source, message });
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
// Data source changed in a way that affects tile layout?
|
|
1848
|
+
// If so, we'll re-calculate the tiles in view for this source and rebuild them
|
|
1849
|
+
if (rebuild_all || DataSource.tileLayoutChanged(this.sources[name], prev_source)) {
|
|
1850
|
+
reset.push(name);
|
|
1851
|
+
}
|
|
1852
|
+
if (this.sources[name] !== prev_source) prev_source?.dispose?.();
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
// Sources that were removed
|
|
1856
|
+
prev_source_names.forEach(s => {
|
|
1857
|
+
if (!this.liveConfig.sources[s]) {
|
|
1858
|
+
this.sources[s]?.dispose?.();
|
|
1859
|
+
delete this.sources[s]; // TODO: remove from workers too?
|
|
1860
|
+
reset.push(s);
|
|
1861
|
+
}
|
|
1862
|
+
});
|
|
1863
|
+
|
|
1864
|
+
// Remove tiles from sources that have changed
|
|
1865
|
+
if (reset.length > 0) {
|
|
1866
|
+
this.activeTileManager.removeTiles(tile => {
|
|
1867
|
+
return (reset.indexOf(tile.source.name) > -1);
|
|
1868
|
+
});
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
// Mark sources that will generate geometry tiles (any that are referenced in scene layers)
|
|
1872
|
+
for (let ln in this.liveConfig.layers) {
|
|
1873
|
+
let layer = this.liveConfig.layers[ln];
|
|
1874
|
+
if (layer.enabled !== false && layer.data && this.sources[layer.data.source]) {
|
|
1875
|
+
this.sources[layer.data.source].builds_geometry_tiles = true;
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
// Load all textures in the scene definition
|
|
1881
|
+
loadTextures() {
|
|
1882
|
+
const resource_context = this.portable_rendering ? this.resource_context : this.gl;
|
|
1883
|
+
return Texture.createFromObject(resource_context, this.liveConfig.textures)
|
|
1884
|
+
.then(() => Texture.createDefault(resource_context)); // create a 'default' texture for placeholders
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
// Free textures from previously loaded scene
|
|
1888
|
+
freePreviousTextures() {
|
|
1889
|
+
if (!this.prev_textures) {
|
|
1890
|
+
return;
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
this.prev_textures.forEach(t => {
|
|
1894
|
+
// free textures that aren't in the new scene, but are still in the global texture set
|
|
1895
|
+
const texture = Texture.textures[t];
|
|
1896
|
+
if (!this.liveConfig.textures[t] && texture instanceof Texture) {
|
|
1897
|
+
texture.destroy();
|
|
1898
|
+
}
|
|
1899
|
+
});
|
|
1900
|
+
this.prev_textures = null;
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
// Called (currently manually) after styles are updated in stylesheet
|
|
1904
|
+
updateStyles() {
|
|
1905
|
+
if (!this.initialized && !this.initializing) {
|
|
1906
|
+
throw new Error('Scene.updateStyles() called before scene was initialized');
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
// (Re)build styles from config
|
|
1910
|
+
this.styles = this.style_manager.build(this.liveConfig.styles);
|
|
1911
|
+
this.style_manager.initStyles(this);
|
|
1912
|
+
const hasRibbon = (layer: unknown): boolean => {
|
|
1913
|
+
if (!layer || typeof layer !== 'object' || Array.isArray(layer)) return false;
|
|
1914
|
+
if ('draw' in layer && layer.draw && typeof layer.draw === 'object' && !Array.isArray(layer.draw)) {
|
|
1915
|
+
for (const [name, group] of Object.entries(layer.draw)) {
|
|
1916
|
+
const style = group && typeof group === 'object' && 'style' in group && typeof group.style === 'string' ? group.style : name;
|
|
1917
|
+
if (this.styles?.[style]?.baseStyle() === 'lines') return true;
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
return Object.entries(layer).some(([name, value]) => !['draw', 'filter', 'data', 'properties'].includes(name) && hasRibbon(value));
|
|
1921
|
+
};
|
|
1922
|
+
this.terrainRibbonStyles = hasRibbon(this.liveConfig.layers);
|
|
1923
|
+
|
|
1924
|
+
// Optionally set GL context (used when initializing or re-initializing GL resources)
|
|
1925
|
+
for (let style in this.styles) {
|
|
1926
|
+
this.liveStyles[style].setGL(this.portable_rendering ? null : this.gl, this.uniform_buffers, {
|
|
1927
|
+
resourceContext: (this.portable_rendering ? this.resource_context : this.gl) ?? undefined,
|
|
1928
|
+
shaderFactory: this.shader_factory,
|
|
1929
|
+
shaderProgramValidator: this.shader_program_validator,
|
|
1930
|
+
shaderLanguage: this.shader_language,
|
|
1931
|
+
portableLighting: Boolean(this.uniform_buffers.TangramLighting),
|
|
1932
|
+
portableLightCount: Object.keys(this.lights ?? {}).length,
|
|
1933
|
+
deviceShaderCompilation: this.device_shader_compilation,
|
|
1934
|
+
uniformBlockFactory: options => this.createUniformBuffer(options),
|
|
1935
|
+
meshBufferFactory: this.mesh_buffer_factory,
|
|
1936
|
+
textureFactory: this.texture_factory,
|
|
1937
|
+
deferUniformBlocks: Boolean(this.mesh_renderer),
|
|
1938
|
+
deferTextureBindings: Boolean(this.mesh_renderer),
|
|
1939
|
+
deferUniformUpdates: Boolean(this.mesh_renderer),
|
|
1940
|
+
maxTextureSize: this.max_texture_size
|
|
1941
|
+
});
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
this.dirty = true;
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
// Is scene currently animating?
|
|
1948
|
+
get animated () {
|
|
1949
|
+
// Disable animation is scene flag requests it, otherwise enable animation if any animated styles are in view
|
|
1950
|
+
return (this.liveConfig.scene.animated === false ?
|
|
1951
|
+
false :
|
|
1952
|
+
this.style_manager.getActiveStyles().some(s => this.liveStyles[s].animated));
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
// Get active camera - for public API
|
|
1956
|
+
getActiveCamera() {
|
|
1957
|
+
return this.view.getActiveCamera();
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
// Set active camera - for public API
|
|
1961
|
+
setActiveCamera(name: string) {
|
|
1962
|
+
return this.view.setActiveCamera(name);
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
// Create lighting
|
|
1966
|
+
createLights() {
|
|
1967
|
+
this.lights = {};
|
|
1968
|
+
this.uniform_buffers = this.uniform_buffers || {};
|
|
1969
|
+
if (Array.isArray(this.liveConfig.lights) && this.liveConfig.lights.length === 0) {
|
|
1970
|
+
this.liveConfig.scene = {...this.liveConfig.scene, lighting: 'configured'};
|
|
1971
|
+
}
|
|
1972
|
+
this.liveConfig.lights = normalizeSceneLights(this.liveConfig.lights) as SceneRuntimeConfiguration['lights'];
|
|
1973
|
+
|
|
1974
|
+
if (debugSettings.wireframe) {
|
|
1975
|
+
Light.enabled = false; // disable lighting for wireframe mode
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
for (let i in this.liveConfig.lights) {
|
|
1979
|
+
if (!this.liveConfig.lights[i] || typeof this.liveConfig.lights[i] !== 'object') {
|
|
1980
|
+
continue;
|
|
1981
|
+
}
|
|
1982
|
+
let light = this.liveConfig.lights[i];
|
|
1983
|
+
const visible = (light.visible ?? light.luma?.visible) !== false;
|
|
1984
|
+
light.name = i.replace('-', '_'); // light names are injected in shaders, can't have hyphens
|
|
1985
|
+
if (visible) {
|
|
1986
|
+
const created = Light.create(this.view, light);
|
|
1987
|
+
if (created) this.lights[light.name] = created;
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
// CPU-projected surfaces explicitly opt into diffuse lighting. Named
|
|
1991
|
+
// Tangram lights must use the same bounded WGSL path as native lights;
|
|
1992
|
+
// otherwise only WebGL consumes the authored light contributions.
|
|
1993
|
+
const projectedLighting = this.liveConfig.scene?.cpu_projection !== undefined &&
|
|
1994
|
+
Object.values(this.liveConfig.styles || {}).some(style =>
|
|
1995
|
+
style && typeof style.lighting === 'string' && ['vertex', 'fragment'].includes(style.lighting));
|
|
1996
|
+
const configured = this.shader_language === 'wgsl' && (projectedLighting || this.liveConfig.scene?.lighting === 'configured' ||
|
|
1997
|
+
Object.values(this.liveConfig.lights).some(light => light && typeof light === 'object' && 'luma' in light));
|
|
1998
|
+
if (configured) {
|
|
1999
|
+
getPortableLightUniforms(this.getLumaLightDefinitions()); // Reject resource overflow before compiling styles.
|
|
2000
|
+
if (!this.uniform_buffers.TangramLighting) {
|
|
2001
|
+
this.uniform_buffers.TangramLighting = this.createUniformBuffer({
|
|
2002
|
+
name: 'TangramLighting', binding: 6, snapshotPerMesh: true,
|
|
2003
|
+
uniforms: PORTABLE_LIGHT_UNIFORMS
|
|
2004
|
+
});
|
|
2005
|
+
}
|
|
2006
|
+
} else if (this.uniform_buffers.TangramLighting) {
|
|
2007
|
+
this.uniform_buffers.TangramLighting.destroy();
|
|
2008
|
+
delete this.uniform_buffers.TangramLighting;
|
|
2009
|
+
}
|
|
2010
|
+
Light.inject(this.lights);
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
/** Snapshot current light definitions after unit conversion and active-eye camera application. */
|
|
2014
|
+
getLumaLightDefinitions() {
|
|
2015
|
+
return Object.values(this.lights || {}).map(light => light.toLumaLight());
|
|
2016
|
+
}
|
|
2017
|
+
|
|
2018
|
+
// Set background color from scene config
|
|
2019
|
+
setBackground() {
|
|
2020
|
+
const bg = this.liveConfig.scene.background;
|
|
2021
|
+
|
|
2022
|
+
this.background = {};
|
|
2023
|
+
if (bg && bg.color) {
|
|
2024
|
+
this.background.color = StyleParser.createColorPropertyCache(bg.color);
|
|
2025
|
+
}
|
|
2026
|
+
if (!this.background.color) {
|
|
2027
|
+
this.background.color = StyleParser.createColorPropertyCache([0, 0, 0, 0]); // default background TODO: vary w/scene alpha
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
// Update background color each frame as needed (e.g. may be zoom-interpolated)
|
|
2032
|
+
updateBackground () {
|
|
2033
|
+
const background = this.background;
|
|
2034
|
+
if (!background?.color) return;
|
|
2035
|
+
const last_color = background.computed_color;
|
|
2036
|
+
const color = background.computed_color = StyleParser.evalCachedColorProperty(background.color, { zoom: this.view.tile_zoom });
|
|
2037
|
+
if (!color) return;
|
|
2038
|
+
|
|
2039
|
+
// update GL/canvas if color has changed
|
|
2040
|
+
if (!last_color || color.some((v, i) => last_color[i] !== v)) {
|
|
2041
|
+
// if background is fully opaque, set canvas background to match
|
|
2042
|
+
if (this.owns_gl && this.canvas && 'style' in this.canvas) {
|
|
2043
|
+
if (color[3] === 1) {
|
|
2044
|
+
this.canvas.style.backgroundColor =
|
|
2045
|
+
`rgba(${color.map(c => Math.floor(c * 255)).join(', ')})`;
|
|
2046
|
+
}
|
|
2047
|
+
else {
|
|
2048
|
+
this.canvas.style.backgroundColor = 'transparent';
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
if (!this.portable_rendering) {
|
|
2053
|
+
this.activeGL.clearColor(color[0], color[1], color[2], color[3]);
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
// Turn introspection mode on/off
|
|
2059
|
+
setIntrospection (val: boolean) {
|
|
2060
|
+
if (val !== this.introspection) {
|
|
2061
|
+
this.introspection = (val != null) ? val : false;
|
|
2062
|
+
this.updating++;
|
|
2063
|
+
return this.updateConfig({ normalize: false }).then(() => this.updating--);
|
|
2064
|
+
}
|
|
2065
|
+
return Promise.resolve();
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
/**
|
|
2069
|
+
* Returns deduplicated credits for all current sources, including TileJSON providers.
|
|
2070
|
+
* Hosts must display these credits and sanitize HTML before DOM insertion.
|
|
2071
|
+
* Metadata loading is cached by each source; failures reject instead of silently dropping credits.
|
|
2072
|
+
*/
|
|
2073
|
+
async getAttributions(): Promise<string[]> {
|
|
2074
|
+
const metadata = await this.getSourceMetadata();
|
|
2075
|
+
return [...new Set(Object.values(metadata).flatMap(source => source.attributions ?? []))];
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
/** Resolve unified source capabilities; archive providers remain isolated in their worker. */
|
|
2079
|
+
async getSourceMetadata(): Promise<Record<string, TangramTileSourceMetadata>> {
|
|
2080
|
+
const sources = {...this.sources};
|
|
2081
|
+
const providerNames = Object.keys(sources).filter(name => sources[name].tile_provider);
|
|
2082
|
+
const metadata = Object.fromEntries(await Promise.all(Object.entries(sources)
|
|
2083
|
+
.filter(([name]) => !providerNames.includes(name))
|
|
2084
|
+
.map(async ([name, source]) => [name, await source.getMetadata()])));
|
|
2085
|
+
if (providerNames.length > 0) {
|
|
2086
|
+
if (!this.workers?.length) throw new Error('Provider metadata requires an initialized scene worker');
|
|
2087
|
+
const generation = this.generation;
|
|
2088
|
+
Object.assign(metadata, await WorkerBroker.postMessage(this.workers[0], 'self.getSourceMetadata', providerNames, generation));
|
|
2089
|
+
if (generation !== this.generation) throw new Error('Scene changed during metadata loading');
|
|
2090
|
+
}
|
|
2091
|
+
if (Object.keys(sources).length !== Object.keys(this.sources).length ||
|
|
2092
|
+
Object.keys(sources).some(name => sources[name] !== this.sources[name])) {
|
|
2093
|
+
throw new Error('Sources changed during metadata loading');
|
|
2094
|
+
}
|
|
2095
|
+
return metadata;
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
/** Detached host-kernel execution work; absent when using worker-local projection kernels. */
|
|
2099
|
+
getProjectionEngineStatistics(): import('../procedures/projection-batch-executor').ProjectionExecutionStatistics | undefined {
|
|
2100
|
+
return this.projectionAdapter?.getStatistics();
|
|
2101
|
+
}
|
|
2102
|
+
|
|
2103
|
+
/** Return detached per-worker load diagnostics; worker-pool replacement invalidates a snapshot. */
|
|
2104
|
+
async getTileSourceStatistics(): Promise<TileSourceStatistics[]> {
|
|
2105
|
+
const workers = this.workers?.slice() ?? [];
|
|
2106
|
+
if (workers.length === 0) return [];
|
|
2107
|
+
const statistics = await WorkerBroker.postMessage<Omit<TileSourceStatistics, 'workerId'>>(workers, 'self.getTileSourceStatistics');
|
|
2108
|
+
const currentWorkers = this.workers;
|
|
2109
|
+
if (workers.length !== currentWorkers?.length || workers.some((worker, index) => worker !== currentWorkers?.[index])) {
|
|
2110
|
+
throw new Error('Workers changed during tile-source statistics loading');
|
|
2111
|
+
}
|
|
2112
|
+
return statistics.map((value, workerId) => ({...value, workerId}));
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
// Update scene config, and optionally rebuild geometry
|
|
2116
|
+
// rebuild can be boolean, or an object containing rebuild options to passthrough
|
|
2117
|
+
updateConfig({ loading = false, rebuild = true, serialize_funcs, texture_nodes = {}, normalize = true, fade_in = false }: {loading?: boolean; rebuild?: boolean | RebuildOptions; serialize_funcs?: boolean; texture_nodes?: SceneTextureNodes; normalize?: boolean; fade_in?: boolean} = {}) {
|
|
2118
|
+
this.generation = ++Scene.generation;
|
|
2119
|
+
this.updating++;
|
|
2120
|
+
|
|
2121
|
+
// Apply globals, finalize textures and other resource paths if needed
|
|
2122
|
+
this.config = SceneLoader.applyGlobalProperties(this.liveConfig);
|
|
2123
|
+
const bundle = this.config_bundle;
|
|
2124
|
+
if (!bundle) throw new Error('Scene configuration has no resource bundle');
|
|
2125
|
+
if (normalize) {
|
|
2126
|
+
// normalize whole scene if requested - usually when user is making run-time updates to scene
|
|
2127
|
+
SceneLoader.normalize(this.liveConfig, bundle, texture_nodes);
|
|
2128
|
+
}
|
|
2129
|
+
SceneLoader.hoistTextureNodes(this.liveConfig, bundle, texture_nodes);
|
|
2130
|
+
|
|
2131
|
+
this.trigger(loading ? 'load' : 'update', { config: this.config });
|
|
2132
|
+
|
|
2133
|
+
this.withWebGLContext(() => {
|
|
2134
|
+
this.style_manager.init();
|
|
2135
|
+
this.view.reset();
|
|
2136
|
+
this.createLights();
|
|
2137
|
+
this.createDataSources(loading);
|
|
2138
|
+
this.loadTextures();
|
|
2139
|
+
this.setBackground();
|
|
2140
|
+
FontManager.loadFonts(this.liveConfig.fonts);
|
|
2141
|
+
|
|
2142
|
+
// TODO: detect changes to styles? already (currently) need to recompile anyway when camera or lights change
|
|
2143
|
+
this.updateStyles();
|
|
2144
|
+
});
|
|
2145
|
+
|
|
2146
|
+
// Optionally rebuild geometry
|
|
2147
|
+
let done = rebuild ?
|
|
2148
|
+
this.rebuild(Object.assign({ initial: loading, new_generation: false, serialize_funcs, fade_in }, typeof rebuild === 'object' && rebuild)) :
|
|
2149
|
+
this.syncConfigToWorker({ serialize_funcs }); // rebuild() also syncs config
|
|
2150
|
+
|
|
2151
|
+
// Finish by updating bounds and re-rendering
|
|
2152
|
+
this.updating--;
|
|
2153
|
+
this.view.updateBounds();
|
|
2154
|
+
this.requestRedraw();
|
|
2155
|
+
|
|
2156
|
+
return done.then(() => {
|
|
2157
|
+
this.last_render_count = 0; // force re-evaluation of selection map
|
|
2158
|
+
this.requestRedraw();
|
|
2159
|
+
});
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
// Serialize config and send to worker
|
|
2163
|
+
syncConfigToWorker({ serialize_funcs = true, terrainRibbonZoom }: {serialize_funcs?: boolean; terrainRibbonZoom?: number | null} = {}) {
|
|
2164
|
+
this.reserveTerrainInputs(this.terrainTransaction === true);
|
|
2165
|
+
if (terrainRibbonZoom !== undefined) {
|
|
2166
|
+
if (!this.terrainTransaction) throw new Error('Terrain ribbon refinement requires a staged terrain rebuild');
|
|
2167
|
+
this.terrainRibbonZoom = terrainRibbonZoom ?? undefined;
|
|
2168
|
+
}
|
|
2169
|
+
// Shader specialization only needs the enabled flag on the main thread,
|
|
2170
|
+
// but its retained style objects must not pin the initial DEM forever.
|
|
2171
|
+
if (this.terrain) for (const style of Object.values(this.styles || {})) style.terrain = this.terrain;
|
|
2172
|
+
// Tell workers we're about to rebuild (so they can update styles, etc.)
|
|
2173
|
+
const configuration = terrainRibbonZoom !== undefined ? {generation: this.generation, ribbonZoom: this.terrainRibbonZoom} : {
|
|
2174
|
+
config: serialize_funcs ? Utils.serializeWithFunctions(this.config) : JSON.stringify(this.config),
|
|
2175
|
+
generation: this.generation,
|
|
2176
|
+
introspection: this.introspection,
|
|
2177
|
+
projection_target: this.projectionTarget,
|
|
2178
|
+
terrain: this.terrain,
|
|
2179
|
+
terrainRibbonZoom: this.terrainRibbonZoom,
|
|
2180
|
+
shader_language: this.shader_language
|
|
2181
|
+
};
|
|
2182
|
+
const method = terrainRibbonZoom !== undefined ? 'self.updateTerrainView' : 'self.updateConfig';
|
|
2183
|
+
if (!this.terrainTransaction) return WorkerBroker.postMessage(this.activeWorkers, method, configuration, debugSettings);
|
|
2184
|
+
// Promise.all rejects at the first worker failure while other broker messages
|
|
2185
|
+
// still retain the snapshot. Drain all replies before recovery can release it.
|
|
2186
|
+
return Promise.allSettled(this.activeWorkers.map(worker => Promise.resolve().then(() =>
|
|
2187
|
+
WorkerBroker.postMessage(worker, method, configuration, debugSettings)
|
|
2188
|
+
))).then(results => {
|
|
2189
|
+
const failure = results.find(result => result.status === 'rejected');
|
|
2190
|
+
if (failure?.status === 'rejected') throw failure.reason;
|
|
2191
|
+
return results.map(result => result.status === 'fulfilled' ? result.value : undefined);
|
|
2192
|
+
});
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
/** Reserve worker-input copies and lazy query capacities before cloning a new field. */
|
|
2196
|
+
private reserveTerrainInputs(staged: boolean): void {
|
|
2197
|
+
const terrain = this.terrain, budget = this.terrainResourceBudget;
|
|
2198
|
+
if (!terrain || !budget || terrain === this.terrainInputSnapshot || terrain === this.terrainStagedInputSnapshot) return;
|
|
2199
|
+
// Renderer retains a submitted picking snapshot and a settled annotation
|
|
2200
|
+
// snapshot. Reserve both capacities even when they happen to share data.
|
|
2201
|
+
const bytes = terrain.byteLength * ((this.workers?.length ?? 0) + 1) + 2 * (terrain.maxQueryIndexBytes + terrain.maxProjectedQueryBytes);
|
|
2202
|
+
const lease = budget.reserve('retained', bytes);
|
|
2203
|
+
if (!lease) throw new Error('Terrain shared input/query capacity budget exceeded');
|
|
2204
|
+
if (staged) {
|
|
2205
|
+
this.terrainStagedInputLease?.release(); this.terrainStagedInputLease = lease;
|
|
2206
|
+
this.terrainStagedInputSnapshot = terrain;
|
|
2207
|
+
} else {
|
|
2208
|
+
this.terrainInputLease?.release(); this.terrainInputLease = lease; this.terrainInputSnapshot = terrain;
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2212
|
+
// Listen to related objects
|
|
2213
|
+
createListeners () {
|
|
2214
|
+
this.listeners = {};
|
|
2215
|
+
|
|
2216
|
+
this.listeners.view = {
|
|
2217
|
+
move: () => this.trigger('move')
|
|
2218
|
+
};
|
|
2219
|
+
this.view.subscribe(this.listeners.view);
|
|
2220
|
+
|
|
2221
|
+
this.listeners.texture = {
|
|
2222
|
+
update: () => this.dirty = true,
|
|
2223
|
+
warning: (data: unknown) => this.trigger('warning', Object.assign({ type: 'textures' }, data))
|
|
2224
|
+
};
|
|
2225
|
+
Texture.subscribe(this.listeners.texture);
|
|
2226
|
+
|
|
2227
|
+
this.listeners.scene_loader = {
|
|
2228
|
+
error: (data: unknown) => this.trigger('error', Object.assign({ type: 'scene' }, data))
|
|
2229
|
+
};
|
|
2230
|
+
SceneLoader.subscribe(this.listeners.scene_loader);
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
destroyListeners () {
|
|
2234
|
+
this.unsubscribeAll();
|
|
2235
|
+
if (!this.listeners) return;
|
|
2236
|
+
this.view.unsubscribe(this.listeners.view);
|
|
2237
|
+
Texture.unsubscribe(this.listeners.texture);
|
|
2238
|
+
SceneLoader.unsubscribe(this.listeners.scene_loader);
|
|
2239
|
+
this.listeners = null;
|
|
2240
|
+
}
|
|
2241
|
+
|
|
2242
|
+
destroyFeatureSelection() {
|
|
2243
|
+
for (const state of this.selection_views?.values() ?? []) state.selection.destroy();
|
|
2244
|
+
this.selection_views?.clear();
|
|
2245
|
+
this.selection_view_id = null;
|
|
2246
|
+
if (this.selection) {
|
|
2247
|
+
this.activeSelection.destroy();
|
|
2248
|
+
}
|
|
2249
|
+
this.selection = null;
|
|
2250
|
+
}
|
|
2251
|
+
|
|
2252
|
+
resetFeatureSelection() {
|
|
2253
|
+
this.destroyFeatureSelection();
|
|
2254
|
+
this.selection = this.createFeatureSelection(() => Boolean(this.building || this.selection_render_pending));
|
|
2255
|
+
this.last_render_count = 0; // force re-evaluation of selection map
|
|
2256
|
+
}
|
|
2257
|
+
|
|
2258
|
+
/** Allocate one readback target/queue per eye, never a separate source feature map. */
|
|
2259
|
+
getFeatureSelectionView(id: string): {selection: FeatureSelection; lastRender: number; pending: boolean} {
|
|
2260
|
+
let state = this.selection_views.get(id);
|
|
2261
|
+
if (!state) {
|
|
2262
|
+
const selection = this.createFeatureSelection(() => Boolean(this.building || (this.selection === state?.selection ? this.selection_render_pending : state?.pending)));
|
|
2263
|
+
state = {selection, lastRender: -1, pending: false};
|
|
2264
|
+
this.selection_views.set(id, state);
|
|
2265
|
+
if (this.selection_view_id === id) {
|
|
2266
|
+
// A host may render again without calling setFrame after its query.
|
|
2267
|
+
this.selection = state.selection;
|
|
2268
|
+
this.last_selection_render = state.lastRender;
|
|
2269
|
+
this.selection_render_pending = state.pending;
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
return state;
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
/** Choose the validated classic/device constructor without an unchecked overload cast. */
|
|
2276
|
+
private createFeatureSelection(lock: () => boolean): FeatureSelection {
|
|
2277
|
+
return this.device ? new FeatureSelection(this.gl, this.activeWorkers, lock, this.device) : new FeatureSelection(this.activeGL, this.activeWorkers, lock);
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
/** Select an eye's target before drawing; invalidate removed views and preserve in-flight reads. */
|
|
2281
|
+
setFeatureSelectionView(id: string, retainedIds: readonly string[]) {
|
|
2282
|
+
if (this.selection_view_id === null && this.selection) this.activeSelection.destroy();
|
|
2283
|
+
const previous = this.selection_view_id === null ? undefined : this.selection_views.get(this.selection_view_id);
|
|
2284
|
+
if (previous) {
|
|
2285
|
+
previous.lastRender = this.last_selection_render;
|
|
2286
|
+
previous.pending = this.selection_render_pending;
|
|
2287
|
+
}
|
|
2288
|
+
for (const [key, state] of this.selection_views) {
|
|
2289
|
+
if (!retainedIds.includes(key)) {
|
|
2290
|
+
state.selection.destroy();
|
|
2291
|
+
this.selection_views.delete(key);
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
const state = this.selection_views.get(id);
|
|
2295
|
+
this.selection_view_id = id;
|
|
2296
|
+
this.selection = state?.selection ?? null;
|
|
2297
|
+
this.last_selection_render = state?.lastRender ?? -1;
|
|
2298
|
+
this.selection_render_pending = state?.pending ?? false;
|
|
2299
|
+
}
|
|
2300
|
+
|
|
2301
|
+
resetWorkerFeatureSelection(sources: string[] | null = null) {
|
|
2302
|
+
if (this.workers) {
|
|
2303
|
+
WorkerBroker.postMessage(this.activeWorkers, 'self.resetFeatureSelection', sources);
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
|
|
2307
|
+
// Gets the current feature selection map size across all workers. Returns a promise.
|
|
2308
|
+
getFeatureSelectionMapSize() {
|
|
2309
|
+
// Only allow one fetch process to run at a time
|
|
2310
|
+
if (this.fetching_selection_map == null) {
|
|
2311
|
+
this.fetching_selection_map = WorkerBroker.postMessage<number>(this.activeWorkers, 'self.getFeatureSelectionMapSize')
|
|
2312
|
+
.then(sizes => {
|
|
2313
|
+
this.fetching_selection_map = null;
|
|
2314
|
+
return sizes.reduce((a, b) => a + b);
|
|
2315
|
+
});
|
|
2316
|
+
}
|
|
2317
|
+
return this.fetching_selection_map;
|
|
2318
|
+
}
|
|
2319
|
+
|
|
2320
|
+
// Reset internal clock, mostly useful for consistent experience when changing styles/debugging
|
|
2321
|
+
resetTime() {
|
|
2322
|
+
this.start_time = +new Date();
|
|
2323
|
+
}
|
|
2324
|
+
|
|
2325
|
+
// Fires event when rendered tile set or style changes
|
|
2326
|
+
updateViewComplete () {
|
|
2327
|
+
if ((this.render_count_changed || this.generation !== this.last_complete_generation) &&
|
|
2328
|
+
!this.building &&
|
|
2329
|
+
!this.activeTileManager.isLoadingVisibleTiles() &&
|
|
2330
|
+
this.activeTileManager.allVisibleTilesLabeled()) {
|
|
2331
|
+
this.activeTileManager.updateLabels();
|
|
2332
|
+
this.last_complete_generation = this.generation;
|
|
2333
|
+
this.trigger('view_complete', { first: (this.view_complete !== true) });
|
|
2334
|
+
this.view_complete = true;
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
|
|
2338
|
+
resetViewComplete () {
|
|
2339
|
+
this.last_complete_generation = null;
|
|
2340
|
+
}
|
|
2341
|
+
|
|
2342
|
+
// Take a screenshot
|
|
2343
|
+
// Asynchronous because we have to wait for next render to capture buffer
|
|
2344
|
+
// Returns a promise
|
|
2345
|
+
screenshot ({ background = 'white' } = {}) {
|
|
2346
|
+
this.requestRedraw();
|
|
2347
|
+
return this.media_capture.screenshot({background});
|
|
2348
|
+
}
|
|
2349
|
+
|
|
2350
|
+
startVideoCapture () {
|
|
2351
|
+
this.requestRedraw();
|
|
2352
|
+
return this.media_capture.startVideoCapture();
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2355
|
+
stopVideoCapture () {
|
|
2356
|
+
return this.media_capture.stopVideoCapture();
|
|
2357
|
+
}
|
|
2358
|
+
|
|
2359
|
+
// Log first frame rendered (with any geometry)
|
|
2360
|
+
logFirstFrame() {
|
|
2361
|
+
if (this.last_render_count === 0 && !this.times.first_frame) {
|
|
2362
|
+
this.times.first_frame = (+new Date()) - this.start_time;
|
|
2363
|
+
log('debug', `Scene: initial frame time: ${this.times.first_frame}`);
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
|
|
2367
|
+
// Log completion of first scene build
|
|
2368
|
+
logFirstBuild() {
|
|
2369
|
+
if (this.times.first_build == null) {
|
|
2370
|
+
this.times.first_build = (+new Date()) - this.start_time;
|
|
2371
|
+
log('debug', `Scene: initial build time: ${this.times.first_build}`);
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
Scene.id = 0; // unique id for a scene instance
|
|
2378
|
+
Scene.generation = 0; // id that is incremented each time a scene config is re-parsed
|