@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,542 @@
|
|
|
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
|
+
import log from '../utils/log';
|
|
7
|
+
import Texture from '../gl/texture';
|
|
8
|
+
import WorkerBroker from '../utils/worker_broker';
|
|
9
|
+
import type {Device, Framebuffer} from '@luma.gl/core';
|
|
10
|
+
import {Texture as DeviceTexture} from '@luma.gl/core';
|
|
11
|
+
import {findSelectionKey, readSelectionPixels} from './selection_pixels';
|
|
12
|
+
|
|
13
|
+
import type {BrokerWorker} from '../utils/worker-types';
|
|
14
|
+
import type {SelectionContext, SelectionEntry, SelectionPoint, SelectionReply, SelectionRequest,
|
|
15
|
+
SelectionResult, SelectionSourceFeature, SelectionTile, SelectionTileEntries} from './selection-types';
|
|
16
|
+
|
|
17
|
+
/** Selection readback and worker lookup lifecycle, independent of the scene's render backend. */
|
|
18
|
+
export default class FeatureSelection {
|
|
19
|
+
/** Invalidates asynchronous GPU pixels when worker feature tables move to another generation. */
|
|
20
|
+
private readbackRevision = 0;
|
|
21
|
+
/** Legacy GL handle, absent for device-only selection. */
|
|
22
|
+
declare gl: WebGLRenderingContext | null;
|
|
23
|
+
/** Worker lookup endpoints indexed by the alpha-byte worker identifier. */
|
|
24
|
+
declare workers: BrokerWorker[] | Record<number, BrokerWorker>;
|
|
25
|
+
/** luma device used for asynchronous texture readback. */
|
|
26
|
+
declare device: Device | null;
|
|
27
|
+
/** Optional external selection-buffer lock. */
|
|
28
|
+
declare _lock_fn: false | (() => boolean);
|
|
29
|
+
/** Whether teardown has invalidated all pending work. */
|
|
30
|
+
declare destroyed: boolean;
|
|
31
|
+
/** Requests keyed by their monotonically increasing selection identifier. */
|
|
32
|
+
declare requests: Record<string | number, SelectionRequest>;
|
|
33
|
+
/** Last resolved payload; custom workers may supply arbitrary cloneable data. */
|
|
34
|
+
declare feature: unknown;
|
|
35
|
+
/** Delay before reading the selection buffer. */
|
|
36
|
+
declare read_delay: number;
|
|
37
|
+
/** Scheduled readback timer, or null after cancellation/execution. */
|
|
38
|
+
declare read_delay_timer: ReturnType<typeof setTimeout> | null;
|
|
39
|
+
/** Lazily allocated legacy readback storage. */
|
|
40
|
+
declare pixels: Uint8Array | null;
|
|
41
|
+
/** Selection attachment dimensions. */
|
|
42
|
+
declare fbo_size: {width: number; height: number};
|
|
43
|
+
/** Owned luma framebuffer, installed only on the device path. */
|
|
44
|
+
declare framebuffer: Framebuffer | null | undefined;
|
|
45
|
+
/** Explicit copyable attachment; the framebuffer does not own supplied textures. */
|
|
46
|
+
private selectionTexture: DeviceTexture | null = null;
|
|
47
|
+
/** Legacy color attachment owned by this target, never shared through a fixed texture name. */
|
|
48
|
+
private legacySelectionTexture: {destroy(): void} | null = null;
|
|
49
|
+
/** Legacy depth attachment released with its framebuffer. */
|
|
50
|
+
private depthRenderbuffer: WebGLRenderbuffer | null = null;
|
|
51
|
+
/** Unique names also isolate eyes belonging to different scenes/contexts. */
|
|
52
|
+
private static nextAttachmentId = 0;
|
|
53
|
+
/** Owned legacy GL framebuffer, installed only on the GL path. */
|
|
54
|
+
declare fbo: WebGLFramebuffer | null | undefined;
|
|
55
|
+
/** Deliberately uninitialized until the first request retains legacy ID allocation. */
|
|
56
|
+
declare selection_request_id: number | undefined;
|
|
57
|
+
/** Worker-local selection entries. */
|
|
58
|
+
declare static map: Record<number, SelectionEntry>;
|
|
59
|
+
/** Worker-local selection ownership by build key. */
|
|
60
|
+
declare static tiles: Record<string, SelectionTileEntries>;
|
|
61
|
+
/** Number of live worker-local selection entries. */
|
|
62
|
+
declare static map_size: number;
|
|
63
|
+
/** Next worker-local color counter. */
|
|
64
|
+
declare static map_entry: number;
|
|
65
|
+
/** Worker identifier encoded in the alpha channel. */
|
|
66
|
+
declare static map_prefix: number;
|
|
67
|
+
/** Clear color for empty selection pixels. */
|
|
68
|
+
declare static defaultColor: [number, number, number, number];
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
/** Create the existing device or legacy GL selection attachments. */
|
|
72
|
+
constructor(gl: WebGLRenderingContext, workers: BrokerWorker[] | Record<number, BrokerWorker>,
|
|
73
|
+
lock_fn?: (() => boolean) | null, device?: null);
|
|
74
|
+
constructor(gl: WebGLRenderingContext | null, workers: BrokerWorker[] | Record<number, BrokerWorker>,
|
|
75
|
+
lock_fn: (() => boolean) | null | undefined, device: Device);
|
|
76
|
+
constructor(gl: WebGLRenderingContext | null, workers: BrokerWorker[] | Record<number, BrokerWorker>,
|
|
77
|
+
lock_fn?: (() => boolean) | null, device: Device | null = null) {
|
|
78
|
+
this.gl = gl;
|
|
79
|
+
this.workers = workers; // pool of workers to request feature look-ups from, keyed by id
|
|
80
|
+
this.device = device;
|
|
81
|
+
this._lock_fn = (typeof lock_fn === 'function') && lock_fn; // indicates if safe to read/write selection buffer this frame
|
|
82
|
+
this.init();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Initialize empty selection state and allocate its framebuffer. */
|
|
86
|
+
init(): void {
|
|
87
|
+
this.destroyed = false;
|
|
88
|
+
// Selection state tracking
|
|
89
|
+
this.requests = {}; // pending selection requests
|
|
90
|
+
this.feature = null; // currently selected feature
|
|
91
|
+
this.read_delay = 0; // delay time from selection render to framebuffer sample, to avoid CPU/GPU sync lock
|
|
92
|
+
this.read_delay_timer = null; // current timer (setTimeout) for delayed selection reads
|
|
93
|
+
this.pixels = null; // allocated lazily on request
|
|
94
|
+
|
|
95
|
+
this.fbo_size = { width: 256, height: 256 }; // TODO: make configurable / adaptive based on canvas size
|
|
96
|
+
if (this.device) {
|
|
97
|
+
// Device-backed textures cannot be attached using the legacy raw
|
|
98
|
+
// WebGL path. Let luma own the attachments and selection render pass.
|
|
99
|
+
const texture = this.device.createTexture({
|
|
100
|
+
id: 'tangram-selection-color', format: 'rgba8unorm',
|
|
101
|
+
width: this.fbo_size.width, height: this.fbo_size.height,
|
|
102
|
+
usage: DeviceTexture.RENDER_ATTACHMENT | DeviceTexture.COPY_SRC
|
|
103
|
+
});
|
|
104
|
+
try {
|
|
105
|
+
this.framebuffer = this.device.createFramebuffer({
|
|
106
|
+
id: 'tangram-selection',
|
|
107
|
+
width: this.fbo_size.width,
|
|
108
|
+
height: this.fbo_size.height,
|
|
109
|
+
colorAttachments: [texture],
|
|
110
|
+
depthStencilAttachment: 'depth16unorm'
|
|
111
|
+
});
|
|
112
|
+
this.selectionTexture = texture;
|
|
113
|
+
} catch (error) {
|
|
114
|
+
texture.destroy();
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Frame buffer for selection
|
|
121
|
+
// TODO: initiate lazily in case we don't need to do any selection
|
|
122
|
+
this.fbo = this.gl!.createFramebuffer();
|
|
123
|
+
this.gl!.bindFramebuffer(this.gl!.FRAMEBUFFER, this.fbo!);
|
|
124
|
+
|
|
125
|
+
// Texture for the FBO color attachment
|
|
126
|
+
var fbo_texture = (Texture as unknown as {
|
|
127
|
+
create(gl: WebGLRenderingContext, name: string, options: {filtering: string}): {
|
|
128
|
+
texture: WebGLTexture; setData(width: number, height: number, data: null, options: {filtering: string}): void;
|
|
129
|
+
destroy(): void;
|
|
130
|
+
};
|
|
131
|
+
}).create( this.gl!, `__selection_fbo_${FeatureSelection.nextAttachmentId++}`, { filtering: 'nearest' });
|
|
132
|
+
this.legacySelectionTexture = fbo_texture;
|
|
133
|
+
fbo_texture.setData(this.fbo_size.width, this.fbo_size.height, null, { filtering: 'nearest' });
|
|
134
|
+
this.gl!.framebufferTexture2D(this.gl!.FRAMEBUFFER, this.gl!.COLOR_ATTACHMENT0, this.gl!.TEXTURE_2D, fbo_texture.texture, 0);
|
|
135
|
+
|
|
136
|
+
// Renderbuffer for the FBO depth attachment
|
|
137
|
+
var fbo_depth_rb = this.gl!.createRenderbuffer();
|
|
138
|
+
this.depthRenderbuffer = fbo_depth_rb;
|
|
139
|
+
this.gl!.bindRenderbuffer(this.gl!.RENDERBUFFER, fbo_depth_rb);
|
|
140
|
+
this.gl!.renderbufferStorage(this.gl!.RENDERBUFFER, this.gl!.DEPTH_COMPONENT16, this.fbo_size.width, this.fbo_size.height);
|
|
141
|
+
this.gl!.framebufferRenderbuffer(this.gl!.FRAMEBUFFER, this.gl!.DEPTH_ATTACHMENT, this.gl!.RENDERBUFFER, fbo_depth_rb);
|
|
142
|
+
|
|
143
|
+
this.gl!.bindFramebuffer(this.gl!.FRAMEBUFFER, null);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Reject pending requests, cancel delayed reads, and release the framebuffer once. */
|
|
147
|
+
destroy(): void {
|
|
148
|
+
if (this.destroyed) return;
|
|
149
|
+
this.destroyed = true;
|
|
150
|
+
clearTimeout(this.read_delay_timer!);
|
|
151
|
+
this.read_delay_timer = null;
|
|
152
|
+
for (const request of Object.values(this.requests)) request.reject(new Error('Feature selection destroyed'));
|
|
153
|
+
this.requests = {};
|
|
154
|
+
if (this.framebuffer) {
|
|
155
|
+
this.framebuffer.destroy();
|
|
156
|
+
this.selectionTexture?.destroy();
|
|
157
|
+
this.selectionTexture = null;
|
|
158
|
+
this.framebuffer = null;
|
|
159
|
+
this.fbo = null;
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (this.gl && this.fbo) {
|
|
163
|
+
this.gl!.deleteFramebuffer(this.fbo);
|
|
164
|
+
this.fbo = null;
|
|
165
|
+
this.gl!.bindFramebuffer(this.gl!.FRAMEBUFFER, null);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
this.legacySelectionTexture?.destroy();
|
|
169
|
+
this.legacySelectionTexture = null;
|
|
170
|
+
if (this.gl && this.depthRenderbuffer) this.gl.deleteRenderbuffer(this.depthRenderbuffer);
|
|
171
|
+
this.depthRenderbuffer = null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// external lock function determines when it's safe to read/write from selection buffer
|
|
175
|
+
/** Whether the host currently prevents selection readback. */
|
|
176
|
+
get locked (): boolean {
|
|
177
|
+
return (this._lock_fn && this._lock_fn()) || false;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Bind the legacy framebuffer; the device path requires a host-owned render pass. */
|
|
181
|
+
bind(): void {
|
|
182
|
+
if (this.device) throw new Error('Device selection must use a luma render pass');
|
|
183
|
+
// Switch to FBO
|
|
184
|
+
this.gl!.bindFramebuffer(this.gl!.FRAMEBUFFER, this.fbo!);
|
|
185
|
+
this.gl!.viewport(0, 0, this.fbo_size.width, this.fbo_size.height);
|
|
186
|
+
this.gl!.clearColor(...FeatureSelection.defaultColor);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Request feature selection
|
|
190
|
+
// Runs asynchronously, schedules selection buffer to be updated
|
|
191
|
+
/** Queue a normalized canvas point and optional radius for the next selection frame. */
|
|
192
|
+
getFeatureAt(point: SelectionPoint | null | undefined, { radius }: {radius?: SelectionPoint | null}): Promise<SelectionResult> {
|
|
193
|
+
if (this.destroyed) return Promise.reject(new Error('Feature selection destroyed'));
|
|
194
|
+
// ensure requested point is in canvas bounds
|
|
195
|
+
if (!point || ![point.x, point.y].every(Number.isFinite) || point.x < 0 || point.y < 0 || point.x > 1 || point.y > 1) {
|
|
196
|
+
return Promise.resolve({ feature: null, changed: false });
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return new Promise<SelectionResult>((resolve, reject) => {
|
|
200
|
+
// Queue requests for feature selection, and they will be picked up by the render loop
|
|
201
|
+
this.selection_request_id = (this.selection_request_id! + 1) || 0;
|
|
202
|
+
this.requests[this.selection_request_id] = {
|
|
203
|
+
id: this.selection_request_id,
|
|
204
|
+
point,
|
|
205
|
+
radius,
|
|
206
|
+
resolve,
|
|
207
|
+
reject
|
|
208
|
+
};
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Any pending selection requests
|
|
213
|
+
/** Return pending request records, or null when the queue is empty. */
|
|
214
|
+
pendingRequests(): Record<string | number, SelectionRequest> | null {
|
|
215
|
+
return Object.keys(this.requests).length ? this.requests : null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Whether any selection read or worker reply is outstanding. */
|
|
219
|
+
hasPendingRequests(): boolean {
|
|
220
|
+
return this.pendingRequests() != null;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Reject unsent requests without canceling worker replies already in flight. */
|
|
224
|
+
clearPendingRequests(): void {
|
|
225
|
+
for (var r in this.requests) {
|
|
226
|
+
var request = this.requests[r];
|
|
227
|
+
|
|
228
|
+
// This request was already sent to the worker, we're just awaiting its reply
|
|
229
|
+
if (request.sent) {
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Reject request since it will never be fulfilled
|
|
234
|
+
// TODO: pass a reason for rejection?
|
|
235
|
+
request.reject({ request });
|
|
236
|
+
delete this.requests[r];
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** Keep unsent promises queued, but prevent old GPU pixels from looking up newly assigned feature keys. */
|
|
241
|
+
invalidateReadback(): void {
|
|
242
|
+
this.readbackRevision++;
|
|
243
|
+
if (this.read_delay_timer !== null) clearTimeout(this.read_delay_timer);
|
|
244
|
+
this.read_delay_timer = null;
|
|
245
|
+
for (const request of Object.values(this.requests)) if (!request.sent) request.reading = false;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Read pending results from the selection buffer. Called after rendering to selection buffer.
|
|
249
|
+
/** Schedule readback after rendering, respecting the external lock. */
|
|
250
|
+
read(): void {
|
|
251
|
+
// Delay reading the pixel result from the selection buffer to avoid CPU/GPU sync lock.
|
|
252
|
+
// Calling readPixels synchronously caused a massive performance hit, presumably since it
|
|
253
|
+
// forced this function to wait for the GPU to finish rendering and retrieve the texture contents.
|
|
254
|
+
if (this.read_delay_timer != null) {
|
|
255
|
+
clearTimeout(this.read_delay_timer!);
|
|
256
|
+
}
|
|
257
|
+
this.read_delay_timer = setTimeout(() => {
|
|
258
|
+
this.read_delay_timer = null;
|
|
259
|
+
if (this.locked || this.destroyed) {
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if (this.device) {
|
|
263
|
+
void this.readDeviceRequests();
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
var gl = this.gl!;
|
|
268
|
+
|
|
269
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, this.fbo!);
|
|
270
|
+
|
|
271
|
+
for (var r in this.requests) {
|
|
272
|
+
var request = this.requests[r];
|
|
273
|
+
|
|
274
|
+
// This request was already sent to the worker, we're just awaiting its reply
|
|
275
|
+
if (request.sent) {
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Check selection map against FBO
|
|
280
|
+
let feature_key, worker_id = 255;
|
|
281
|
+
let {point, radius} = request;
|
|
282
|
+
let diam_px;
|
|
283
|
+
|
|
284
|
+
if (!radius) {
|
|
285
|
+
radius = { x: 0, y: 0 };
|
|
286
|
+
diam_px = { x: 1, y: 1 };
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
// diameter in selection buffer pixels
|
|
290
|
+
let max_radius = Math.min(this.fbo_size.width, this.fbo_size.height);
|
|
291
|
+
diam_px = {
|
|
292
|
+
x: Math.min(Math.ceil(radius.x * 2 * this.fbo_size.width), max_radius),
|
|
293
|
+
y: Math.min(Math.ceil(radius.y * 2 * this.fbo_size.height), max_radius)
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// allocate or resize
|
|
298
|
+
if (this.pixels == null || this.pixels.byteLength < diam_px.x * diam_px.y * 4) {
|
|
299
|
+
this.pixels = new Uint8Array(diam_px.x * diam_px.y * 4);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// clear pixels
|
|
303
|
+
if (this.pixels.fill instanceof Function) {
|
|
304
|
+
this.pixels.fill(0); // native typed array fill
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
for (let p=0; p < this.pixels.length; p++) {
|
|
308
|
+
this.pixels[p] = 0;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// capture pixels
|
|
313
|
+
gl.readPixels(
|
|
314
|
+
Math.round(((point.x - radius.x) * this.fbo_size.width)),
|
|
315
|
+
Math.round((1 - point.y - radius.y) * this.fbo_size.height),
|
|
316
|
+
diam_px.x, diam_px.y, gl.RGBA, gl.UNSIGNED_BYTE, this.pixels);
|
|
317
|
+
|
|
318
|
+
// first check center pixel (avoid scanning all pixels if cursor is directly on a feature)
|
|
319
|
+
let p = (Math.round(diam_px.y / 2) * diam_px.x + Math.round(diam_px.x / 2)) * 4;
|
|
320
|
+
let v = this.pixels[p] + (this.pixels[p+1] << 8) + (this.pixels[p+2] << 16); // feature id in RGB channels
|
|
321
|
+
if (v > 0) {
|
|
322
|
+
feature_key = (v + (this.pixels[p+3] << 24)) >>> 0; // worker id in alpha channel
|
|
323
|
+
worker_id = this.pixels[p+3];
|
|
324
|
+
}
|
|
325
|
+
else {
|
|
326
|
+
// scan all pixels for feature closest to cursor
|
|
327
|
+
let min_dist = -1 >>> 0;
|
|
328
|
+
p = 0;
|
|
329
|
+
for (let y=0; y < diam_px.y; y++) {
|
|
330
|
+
for (let x=0; x < diam_px.x; x++, p += 4) {
|
|
331
|
+
v = this.pixels[p] + (this.pixels[p+1] << 8) + (this.pixels[p+2] << 16); // feature id in RGB channels
|
|
332
|
+
if (v > 0) { // non-zero value indicates a feature
|
|
333
|
+
// check to see if closer than last found feature
|
|
334
|
+
let dist = (x - diam_px.x/2) * (x - diam_px.x/2) + (y - diam_px.y/2) * (y - diam_px.y/2);
|
|
335
|
+
if (dist <= min_dist) {
|
|
336
|
+
// get worker id from alpha channel
|
|
337
|
+
feature_key = (v + (this.pixels[p+3] << 24)) >>> 0;
|
|
338
|
+
worker_id = this.pixels[p+3];
|
|
339
|
+
min_dist = dist;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// If feature found, ask appropriate web worker to lookup feature
|
|
347
|
+
if (worker_id !== 255) { // 255 indicates an empty selection buffer pixel
|
|
348
|
+
if (this.workers[worker_id] != null) {
|
|
349
|
+
WorkerBroker.postMessage<SelectionReply>(
|
|
350
|
+
this.workers[worker_id],
|
|
351
|
+
'self.getFeatureSelection',
|
|
352
|
+
{ id: request.id, key: feature_key })
|
|
353
|
+
.then(message => {
|
|
354
|
+
this.finishRead(message);
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
// No feature found, but still need to resolve promise
|
|
359
|
+
else {
|
|
360
|
+
this.finishRead({ id: request.id });
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
request.sent = true;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
367
|
+
|
|
368
|
+
}, this.read_delay);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/** Read each request without raw GL handles; late/canceled reads cannot update selection state. */
|
|
372
|
+
async readDeviceRequests(): Promise<void> {
|
|
373
|
+
for (const request of Object.values(this.requests)) {
|
|
374
|
+
// A subsequent frame may invalidate the target while a previous
|
|
375
|
+
// request awaits its worker. Leave the remaining requests queued.
|
|
376
|
+
if (this.locked) return;
|
|
377
|
+
if (this.destroyed || this.requests[request.id] !== request || request.sent || request.reading) continue;
|
|
378
|
+
request.reading = true;
|
|
379
|
+
const revision = this.readbackRevision;
|
|
380
|
+
try {
|
|
381
|
+
const texture = this.framebuffer!.colorAttachments[0].texture;
|
|
382
|
+
const {pixels, width, height} = await readSelectionPixels(this.device!, texture, request.point, request.radius);
|
|
383
|
+
if (revision !== this.readbackRevision) return;
|
|
384
|
+
if (this.destroyed || this.requests[request.id] !== request) continue;
|
|
385
|
+
if (this.locked) {request.reading = false; return;}
|
|
386
|
+
const hit = findSelectionKey(pixels, width, height);
|
|
387
|
+
if (hit && hit.workerId !== 255 && this.workers[hit.workerId]) {
|
|
388
|
+
request.sent = true;
|
|
389
|
+
const message = await WorkerBroker.postMessage<SelectionReply>(this.workers[hit.workerId], 'self.getFeatureSelection',
|
|
390
|
+
{id: request.id, key: hit.key});
|
|
391
|
+
if (!this.destroyed && this.requests[request.id] === request) this.finishRead(message);
|
|
392
|
+
} else {
|
|
393
|
+
this.finishRead({id: request.id});
|
|
394
|
+
}
|
|
395
|
+
} catch (error) {
|
|
396
|
+
if (revision !== this.readbackRevision) return;
|
|
397
|
+
if (this.requests[request.id] === request) {
|
|
398
|
+
request.reject(error);
|
|
399
|
+
delete this.requests[request.id];
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Called on main thread when a web worker finds a feature in the selection buffer
|
|
406
|
+
/** Resolve a worker reply and preserve the existing JSON-based change comparison. */
|
|
407
|
+
finishRead (message: SelectionReply): void {
|
|
408
|
+
var request = this.requests[message.id];
|
|
409
|
+
if (!request) {
|
|
410
|
+
log('error', 'FeatureSelection.finishRead(): could not find message', message);
|
|
411
|
+
return; // request was cleared before it returned
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
var feature = message.feature;
|
|
415
|
+
var changed = false;
|
|
416
|
+
if ((feature != null && this.feature == null) ||
|
|
417
|
+
(feature == null && this.feature != null) ||
|
|
418
|
+
(feature != null && this.feature != null &&
|
|
419
|
+
JSON.stringify(feature) !== JSON.stringify(this.feature))) {
|
|
420
|
+
changed = true;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
this.feature = feature; // store the most recently selected feature
|
|
424
|
+
|
|
425
|
+
// Resolve the request
|
|
426
|
+
request.resolve({ feature, changed, request });
|
|
427
|
+
delete this.requests[message.id]; // done processing this request
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
// Selection map generation
|
|
432
|
+
// Each worker will create its own independent, 'local' selection map
|
|
433
|
+
|
|
434
|
+
// Create a unique 32-bit color to identify a feature
|
|
435
|
+
// Workers independently create/modify selection colors in their own threads, but we also
|
|
436
|
+
// need the main thread to know where each feature color originated. To accomplish this,
|
|
437
|
+
// we partition the map by setting the 4th component (alpha channel) to the worker's id.
|
|
438
|
+
/** Allocate a worker-prefixed selection color owned by the given tile build. */
|
|
439
|
+
static makeEntry(tile: SelectionTile): SelectionEntry {
|
|
440
|
+
// 32-bit color key
|
|
441
|
+
this.map_entry++;
|
|
442
|
+
var ir = this.map_entry & 255;
|
|
443
|
+
var ig = (this.map_entry >> 8) & 255;
|
|
444
|
+
var ib = (this.map_entry >> 16) & 255;
|
|
445
|
+
var ia = this.map_prefix;
|
|
446
|
+
var r = ir / 255;
|
|
447
|
+
var g = ig / 255;
|
|
448
|
+
var b = ib / 255;
|
|
449
|
+
var a = ia / 255;
|
|
450
|
+
var key = (ir + (ig << 8) + (ib << 16) + (ia << 24)) >>> 0; // need unsigned right shift to convert to positive #
|
|
451
|
+
|
|
452
|
+
this.map[key] = {
|
|
453
|
+
color: [r, g, b, a],
|
|
454
|
+
};
|
|
455
|
+
this.map_size++;
|
|
456
|
+
|
|
457
|
+
// Initialize tile-specific tracking info
|
|
458
|
+
if (!this.tiles[tile.key]) {
|
|
459
|
+
this.tiles[tile.key] = {
|
|
460
|
+
entries: [], // set of feature entries in this thread
|
|
461
|
+
tile: { // subset of tile properties to pass back with feature
|
|
462
|
+
key: tile.key,
|
|
463
|
+
coords: tile.coords,
|
|
464
|
+
style_z: tile.style_z,
|
|
465
|
+
source: tile.source,
|
|
466
|
+
generation: tile.generation
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
this.tiles[tile.key].entries.push(key);
|
|
472
|
+
|
|
473
|
+
return this.map[key];
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/** Attach feature/source metadata to a newly allocated selection entry. */
|
|
477
|
+
static makeColor(feature: SelectionSourceFeature, tile: SelectionTile, context: SelectionContext): SelectionEntry['color'] {
|
|
478
|
+
var selector = this.makeEntry(tile);
|
|
479
|
+
selector.feature = {
|
|
480
|
+
id: feature.id,
|
|
481
|
+
properties: feature.properties,
|
|
482
|
+
source_name: context.source,
|
|
483
|
+
source_layer: context.layer,
|
|
484
|
+
layers: context.layers,
|
|
485
|
+
tile: this.tiles[tile.key].tile
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
return selector.color;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/** Clear named sources, or all entries when no source list is supplied. */
|
|
492
|
+
static reset(sources?: string[] | null): void {
|
|
493
|
+
// Clear specific sources
|
|
494
|
+
if (Array.isArray(sources)) {
|
|
495
|
+
sources.forEach(source => this.clearSource(source));
|
|
496
|
+
}
|
|
497
|
+
// Clear all sources
|
|
498
|
+
else {
|
|
499
|
+
this.tiles = {};
|
|
500
|
+
this.map = {};
|
|
501
|
+
this.map_size = 0;
|
|
502
|
+
this.map_entry = 0;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/** Release every selection entry owned by the named source. */
|
|
507
|
+
static clearSource(source: string): void {
|
|
508
|
+
for (let key in this.tiles) {
|
|
509
|
+
if (this.tiles[key].tile.source === source) {
|
|
510
|
+
this.clearTile(key);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/** Release every selection key owned by one tile build. */
|
|
516
|
+
static clearTile(key: string): void {
|
|
517
|
+
if (this.tiles[key]) {
|
|
518
|
+
this.tiles[key].entries.forEach(k => delete this.map[k]);
|
|
519
|
+
this.map_size -= this.tiles[key].entries.length;
|
|
520
|
+
delete this.tiles[key];
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/** Return the number of live worker-local feature entries. */
|
|
525
|
+
static getMapSize(): number {
|
|
526
|
+
return this.map_size;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/** Set the worker identifier used in future selection colors. */
|
|
530
|
+
static setPrefix(prefix: number): void {
|
|
531
|
+
this.map_prefix = prefix;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// Static properties
|
|
537
|
+
FeatureSelection.map = {}; // this will be unique per module instance (so unique per worker)
|
|
538
|
+
FeatureSelection.tiles = {}; // selection keys, by tile
|
|
539
|
+
FeatureSelection.map_size = 0;
|
|
540
|
+
FeatureSelection.map_entry = 0;
|
|
541
|
+
FeatureSelection.map_prefix = 0; // set by worker to worker id #
|
|
542
|
+
FeatureSelection.defaultColor = [0, 0, 0, 1];
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Tangram
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) 2013-2016 Brett Camper and Mapzen
|
|
4
|
+
|
|
5
|
+
// Fragment shader for feature selection passes
|
|
6
|
+
// Renders in silhouette according to selection (picking) color, or black if none defined
|
|
7
|
+
|
|
8
|
+
#ifdef TANGRAM_FEATURE_SELECTION
|
|
9
|
+
varying vec4 v_selection_color;
|
|
10
|
+
#endif
|
|
11
|
+
|
|
12
|
+
void main (void) {
|
|
13
|
+
#ifdef TANGRAM_FEATURE_SELECTION
|
|
14
|
+
gl_FragColor = v_selection_color;
|
|
15
|
+
#else
|
|
16
|
+
gl_FragColor = vec4(0., 0., 0., 1.);
|
|
17
|
+
#endif
|
|
18
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Tangram
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) 2013-2016 Brett Camper and Mapzen
|
|
4
|
+
|
|
5
|
+
// Vertex attribute + varying for feature selection
|
|
6
|
+
#if defined(TANGRAM_FEATURE_SELECTION) && defined(TANGRAM_VERTEX_SHADER)
|
|
7
|
+
attribute vec4 a_selection_color;
|
|
8
|
+
varying vec4 v_selection_color;
|
|
9
|
+
#endif
|
|
@@ -0,0 +1,59 @@
|
|
|
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
|
+
import type {Device, Texture} from '@luma.gl/core';
|
|
7
|
+
import {readDevicePixels} from '../gpu/pixel_readback';
|
|
8
|
+
|
|
9
|
+
/** Read a selection rectangle with transparent padding where it crosses texture edges. */
|
|
10
|
+
export async function readSelectionPixels(device: Device, texture: Texture,
|
|
11
|
+
point: {x: number; y: number}, radius?: {x: number; y: number} | null): Promise<{pixels: Uint8Array; width: number; height: number}> {
|
|
12
|
+
if (![point.x, point.y, radius?.x ?? 0, radius?.y ?? 0].every(Number.isFinite) ||
|
|
13
|
+
(radius && (radius.x < 0 || radius.y < 0))) {
|
|
14
|
+
throw new Error('Selection point and radius must be finite, with a non-negative radius');
|
|
15
|
+
}
|
|
16
|
+
const maximum = Math.min(texture.width, texture.height);
|
|
17
|
+
const width = radius ? Math.min(Math.ceil(radius.x * 2 * texture.width), maximum) : 1;
|
|
18
|
+
const height = radius ? Math.min(Math.ceil(radius.y * 2 * texture.height), maximum) : 1;
|
|
19
|
+
if (!Number.isSafeInteger(width) || !Number.isSafeInteger(height) || width < 0 || height < 0) {
|
|
20
|
+
throw new Error('Selection radius must produce finite non-negative pixel dimensions');
|
|
21
|
+
}
|
|
22
|
+
const pixels = new Uint8Array(width * height * 4);
|
|
23
|
+
// CSS points are top-origin; native readback retains each backend's row order.
|
|
24
|
+
const x = Math.round((point.x - (radius?.x || 0)) * texture.width);
|
|
25
|
+
const y = Math.round(((device.type === 'webgpu' ? point.y : 1 - point.y) - (radius?.y || 0)) * texture.height);
|
|
26
|
+
const sourceX = Math.max(0, x), sourceY = Math.max(0, y);
|
|
27
|
+
const clippedWidth = Math.min(texture.width, x + width) - sourceX;
|
|
28
|
+
const clippedHeight = Math.min(texture.height, y + height) - sourceY;
|
|
29
|
+
if (clippedWidth > 0 && clippedHeight > 0) {
|
|
30
|
+
const data = await readDevicePixels(device, texture, {x: sourceX, y: sourceY, width: clippedWidth, height: clippedHeight});
|
|
31
|
+
for (let row = 0; row < clippedHeight; row++) {
|
|
32
|
+
pixels.set(data.subarray(row * clippedWidth * 4, (row + 1) * clippedWidth * 4),
|
|
33
|
+
((sourceY - y + row) * width + sourceX - x) * 4);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return {pixels, width, height};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Decode the existing RGB feature/alpha worker key, preferring the center then the nearest hit. */
|
|
40
|
+
export function findSelectionKey(pixels: Uint8Array, width: number, height: number): {key: number; workerId: number} | null {
|
|
41
|
+
if (width <= 0 || height <= 0) return null;
|
|
42
|
+
const center = (Math.min(height - 1, Math.round(height / 2)) * width + Math.min(width - 1, Math.round(width / 2))) * 4;
|
|
43
|
+
const decode = (offset: number) => {
|
|
44
|
+
const feature = pixels[offset] + (pixels[offset + 1] << 8) + (pixels[offset + 2] << 16);
|
|
45
|
+
return feature > 0 ? {key: (feature + (pixels[offset + 3] << 24)) >>> 0, workerId: pixels[offset + 3]} : null;
|
|
46
|
+
};
|
|
47
|
+
const centerHit = decode(center);
|
|
48
|
+
if (centerHit) return centerHit;
|
|
49
|
+
let nearest = null;
|
|
50
|
+
let minimumDistance = Infinity;
|
|
51
|
+
for (let y = 0; y < height; y++) {
|
|
52
|
+
for (let x = 0; x < width; x++) {
|
|
53
|
+
const hit = decode((y * width + x) * 4);
|
|
54
|
+
const distance = (x - width / 2) ** 2 + (y - height / 2) ** 2;
|
|
55
|
+
if (hit && distance <= minimumDistance) { nearest = hit; minimumDistance = distance; }
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return nearest;
|
|
59
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Tangram
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) 2013-2016 Brett Camper and Mapzen
|
|
4
|
+
|
|
5
|
+
// Selection pass-specific rendering
|
|
6
|
+
#if defined(TANGRAM_FEATURE_SELECTION) && defined(TANGRAM_VERTEX_SHADER)
|
|
7
|
+
if (a_selection_color.rgb == vec3(0.)) {
|
|
8
|
+
// Discard by forcing invalid triangle if we're in the feature
|
|
9
|
+
// selection pass but have no selection info
|
|
10
|
+
// TODO: in some cases we may actually want non-selectable features to occlude selectable ones?
|
|
11
|
+
gl_Position = vec4(0., 0., 0., 1.);
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
v_selection_color = a_selection_color;
|
|
15
|
+
#endif
|