@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,399 @@
|
|
|
1
|
+
// tangram-layers
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import {ProjectionTransform} from '@math.gl/projection/core';
|
|
6
|
+
import {equalEarth} from '@math.gl/projection/projections/eqearth';
|
|
7
|
+
import {albersEqualArea} from '@math.gl/projection/projections/aea';
|
|
8
|
+
import {equidistantCylindrical} from '@math.gl/projection/projections/eqc';
|
|
9
|
+
import {mercator} from '@math.gl/projection/projections/merc';
|
|
10
|
+
import {refineGlobeMesh} from '../gl/globe_mesh';
|
|
11
|
+
import {normalizeProjectedBasemapOptions} from '../procedures/mesh-projector';
|
|
12
|
+
import {getProjectedCoordinateOptions, PROJECTED_COMMON_SCALE} from '../procedures/projected-coordinate-transform';
|
|
13
|
+
import {projectSurfaceNormal} from './projected-normal';
|
|
14
|
+
import {PACKED_HEIGHT_SCALE} from '../gl/vertex-constants';
|
|
15
|
+
import type {MeshProjectionRequest, ProjectedMesh, ProjectedBasemapOptions, MeshProjectionStatistics} from '../procedures/mesh-projector';
|
|
16
|
+
import {ProjectedMeshPreparationCache} from './projected-mesh-cache';
|
|
17
|
+
import {clipProjectedMesh, getProjectedSourceDomain} from './projected-mesh-domain';
|
|
18
|
+
import {refineProjectedMesh} from './projected-mesh-refinement';
|
|
19
|
+
import {projectSymbolMesh} from './projected-symbol-mesh';
|
|
20
|
+
import type {ProjectedRefinementWork} from './projected-mesh-refinement';
|
|
21
|
+
|
|
22
|
+
const SPHERE_RADIUS = 6378137;
|
|
23
|
+
const transforms = new Map<ProjectedBasemapOptions['type'], ProjectionTransform>();
|
|
24
|
+
const preparationCache = new ProjectedMeshPreparationCache();
|
|
25
|
+
let projectedResultCache = new ProjectedMeshPreparationCache(16 * 1024 * 1024, 32);
|
|
26
|
+
let projectionStatistics = createProjectionStatistics();
|
|
27
|
+
|
|
28
|
+
/** Start a new worker-lifecycle snapshot; pending requests retain their old accounting epoch. */
|
|
29
|
+
function createProjectionStatistics(): MeshProjectionStatistics {
|
|
30
|
+
return {completedMeshes: 0, failedMeshes: 0, sourceVertices: 0, outputVertices: 0,
|
|
31
|
+
outputTriangles: 0, projectionBatches: 0, projectedPositions: 0, edgeRounds: 0, interiorRounds: 0};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Release optional worker-owned preparation and compiled local transforms on worker reset. */
|
|
35
|
+
export function clearProjectedMeshPreparation(): void {
|
|
36
|
+
preparationCache.clear();
|
|
37
|
+
projectedResultCache.clear();
|
|
38
|
+
projectedResultCache = new ProjectedMeshPreparationCache(16 * 1024 * 1024, 32);
|
|
39
|
+
transforms.clear();
|
|
40
|
+
projectionStatistics = createProjectionStatistics();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Detached cumulative projection/refinement work, never mutable mesh or cache records. */
|
|
44
|
+
export function getProjectedMeshWorkStatistics(): MeshProjectionStatistics {
|
|
45
|
+
return {...projectionStatistics};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Internal worker diagnostics for verifying warm source topology independently of target projection. */
|
|
49
|
+
export function getProjectedMeshPreparationStatistics(): import('../procedures/mesh-projector').MeshPreparationStatistics {
|
|
50
|
+
const projectedResults = projectedResultCache.getStatistics();
|
|
51
|
+
return {...preparationCache.getStatistics(), ...(projectedResults.misses ? {projectedResults} : {})};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Include every source-layout, tile, target and refinement input; never reuse across host transform identities. */
|
|
55
|
+
function getProjectedResultCacheKey(request: MeshProjectionRequest): string {
|
|
56
|
+
return JSON.stringify({projection: normalizeProjectedBasemapOptions(request.projection),
|
|
57
|
+
geometry: request.geometry, tileZoom: request.tile.coords.z,
|
|
58
|
+
min: [request.tile.min.x, request.tile.min.y], overzoom: request.tile.overzoom2 ?? 1,
|
|
59
|
+
styleZoom: request.tile.style_z,
|
|
60
|
+
engine: request.projectPositions ? request.projectionCacheKey : 'local-kernels',
|
|
61
|
+
stride: request.layout.stride, attributes: request.layout.dynamic_attribs.map(attribute =>
|
|
62
|
+
[attribute.name, attribute.type, attribute.size, attribute.offset, attribute.normalized])});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Project ground degrees to north-positive common coordinates (256 units per sphere radius). */
|
|
66
|
+
export function projectBasemapPosition(position: readonly [number, number], type: ProjectedBasemapOptions['type']): [number, number, number] {
|
|
67
|
+
if (!position.every(Number.isFinite) || Math.abs(position[0]) > 180 || Math.abs(position[1]) > 85.0511287798066) {
|
|
68
|
+
throw new Error('Projected basemap position is outside the finite tile domain');
|
|
69
|
+
}
|
|
70
|
+
normalizeProjectedBasemapOptions({type});
|
|
71
|
+
let transform = transforms.get(type);
|
|
72
|
+
if (!transform) {
|
|
73
|
+
// EPSG:3395 uses the WGS84 ellipsoid; EPSG:3857 uses the same major radius as a sphere.
|
|
74
|
+
// Use matching geographic CRS geometry so this is projection, not a datum conversion.
|
|
75
|
+
transform = new ProjectionTransform({projections: [equalEarth, albersEqualArea, equidistantCylindrical, mercator],
|
|
76
|
+
...getProjectedCoordinateOptions(type)});
|
|
77
|
+
transforms.set(type, transform);
|
|
78
|
+
}
|
|
79
|
+
const projected = transform.projectSync([position[0], position[1]]);
|
|
80
|
+
if (!projected.every(Number.isFinite)) throw new Error('Projected basemap produced a nonfinite position');
|
|
81
|
+
return [projected[0] * PROJECTED_COMMON_SCALE, projected[1] * PROJECTED_COMMON_SCALE, 0];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Refine in packed tile space, then write separate projected positions, preserving UVs and feature IDs. */
|
|
85
|
+
export function projectBasemapMesh(request: MeshProjectionRequest): ProjectedMesh {
|
|
86
|
+
if (request.projection.cacheProjectedMeshes) {
|
|
87
|
+
const result = projectedResultCache.getOrCreate(getProjectedResultCacheKey(request), request.vertices, request.indices,
|
|
88
|
+
() => projectBasemapMesh({...request, projection: {...request.projection, cacheProjectedMeshes: false}}));
|
|
89
|
+
return {vertices: result.vertices.slice(), indices: result.indices.slice()};
|
|
90
|
+
}
|
|
91
|
+
const work = {edgeRounds: 0, interiorRounds: 0};
|
|
92
|
+
const statistics = projectionStatistics;
|
|
93
|
+
try {
|
|
94
|
+
const refinement = prepareProjectedMesh(request, work);
|
|
95
|
+
let step = refinement.next();
|
|
96
|
+
while (!step.done) {
|
|
97
|
+
countProjectionBatch(step.value, statistics);
|
|
98
|
+
const positions = step.value.slice();
|
|
99
|
+
for (let index = 0; index < positions.length; index += 2) {
|
|
100
|
+
const point = projectBasemapPosition([positions[index], positions[index + 1]], request.projection.type);
|
|
101
|
+
positions[index] = point[0]; positions[index + 1] = point[1];
|
|
102
|
+
}
|
|
103
|
+
step = refinement.next(positions);
|
|
104
|
+
}
|
|
105
|
+
recordCompletedProjection(request, step.value, work, statistics);
|
|
106
|
+
return step.value;
|
|
107
|
+
} catch (error) {
|
|
108
|
+
statistics.failedMeshes++;
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Batch all refined vertices through the injected host engine, never one RPC per vertex. */
|
|
114
|
+
export async function projectBasemapMeshWithEngine(request: MeshProjectionRequest): Promise<ProjectedMesh> {
|
|
115
|
+
if (request.projectPositions && request.projection.cacheProjectedMeshes && request.projectionCacheKey) {
|
|
116
|
+
const result = await projectedResultCache.getOrCreateAsync(getProjectedResultCacheKey(request), request.vertices, request.indices,
|
|
117
|
+
() => projectBasemapMeshWithEngine({...request, projection: {...request.projection, cacheProjectedMeshes: false}}));
|
|
118
|
+
return {vertices: result.vertices.slice(), indices: result.indices.slice()};
|
|
119
|
+
}
|
|
120
|
+
const work = {edgeRounds: 0, interiorRounds: 0};
|
|
121
|
+
const statistics = projectionStatistics;
|
|
122
|
+
try {
|
|
123
|
+
if (!request.projectPositions) throw new Error('Injected projection requires a host batch callback');
|
|
124
|
+
const refinement = prepareProjectedMesh(request, work);
|
|
125
|
+
let step = refinement.next();
|
|
126
|
+
while (!step.done) {
|
|
127
|
+
countProjectionBatch(step.value, statistics);
|
|
128
|
+
step = refinement.next(await request.projectPositions(step.value));
|
|
129
|
+
}
|
|
130
|
+
recordCompletedProjection(request, step.value, work, statistics);
|
|
131
|
+
return step.value;
|
|
132
|
+
} catch (error) {
|
|
133
|
+
statistics.failedMeshes++;
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Account submitted work even if its engine rejects the request. */
|
|
139
|
+
function countProjectionBatch(coordinates: Float64Array, statistics: MeshProjectionStatistics): void {
|
|
140
|
+
statistics.projectionBatches++;
|
|
141
|
+
statistics.projectedPositions += coordinates.length / 2;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Count returned meshes only, not partially refined or failed output. */
|
|
145
|
+
function recordCompletedProjection(request: MeshProjectionRequest, result: ProjectedMesh, work: ProjectedRefinementWork,
|
|
146
|
+
statistics: MeshProjectionStatistics): void {
|
|
147
|
+
statistics.completedMeshes++;
|
|
148
|
+
statistics.sourceVertices += request.vertices.byteLength / request.layout.stride;
|
|
149
|
+
statistics.outputVertices += result.vertices.byteLength / request.layout.stride;
|
|
150
|
+
statistics.outputTriangles += result.indices.length / 3;
|
|
151
|
+
statistics.edgeRounds += work.edgeRounds;
|
|
152
|
+
statistics.interiorRounds += work.interiorRounds;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Share refinement and tile conventions between worker-local and host-injected transforms. */
|
|
156
|
+
function* prepareProjectedMesh(request: MeshProjectionRequest, work: ProjectedRefinementWork): Generator<Float64Array, ProjectedMesh, Float64Array> {
|
|
157
|
+
const projection = normalizeProjectedBasemapOptions(request.projection);
|
|
158
|
+
if (request.geometry === 'points' || request.geometry === 'text') return yield* projectSymbolMesh(request);
|
|
159
|
+
const {tile, layout} = request;
|
|
160
|
+
const unitsPerMeter = 4096 * 2 ** tile.coords.z / (2 * Math.PI * SPHERE_RADIUS);
|
|
161
|
+
const position = layout.dynamic_attribs.find(attribute => attribute.name === 'a_position');
|
|
162
|
+
const projected = layout.dynamic_attribs.find(attribute => attribute.name === 'a_projected_position');
|
|
163
|
+
const extrusion = layout.dynamic_attribs.find(attribute => attribute.name === 'a_extrude');
|
|
164
|
+
const line = request.geometry === 'lines';
|
|
165
|
+
const surfaceNormal = layout.dynamic_attribs.find(attribute => attribute.name === 'a_normal');
|
|
166
|
+
const projectedNormal = layout.dynamic_attribs.find(attribute => attribute.name === 'a_projected_normal');
|
|
167
|
+
if (projectedNormal && (projectedNormal.type !== 5126 || projectedNormal.size !== 3 ||
|
|
168
|
+
projectedNormal.offset === undefined || projectedNormal.offset < 0 || projectedNormal.offset + 12 > layout.stride)) {
|
|
169
|
+
throw new Error('CPU projection requires a valid projected normal layout');
|
|
170
|
+
}
|
|
171
|
+
if (surfaceNormal && (surfaceNormal.type !== 5120 || surfaceNormal.size < 3 ||
|
|
172
|
+
surfaceNormal.offset === undefined || surfaceNormal.offset < 0 || surfaceNormal.offset + 3 > layout.stride)) {
|
|
173
|
+
throw new Error('CPU projection requires a packed surface normal');
|
|
174
|
+
}
|
|
175
|
+
const overzoom = tile.overzoom2 ?? 1;
|
|
176
|
+
if (!position || position.type !== 5122 || position.size !== 4 || position.offset === undefined ||
|
|
177
|
+
!projected || projected.type !== 5126 || projected.size !== 3 || projected.offset === undefined ||
|
|
178
|
+
!Number.isSafeInteger(layout.stride) || layout.stride <= 0 ||
|
|
179
|
+
request.vertices.byteLength % layout.stride !== 0 ||
|
|
180
|
+
position.offset < 0 || position.offset + 8 > layout.stride ||
|
|
181
|
+
projected.offset < 0 || projected.offset + 12 > layout.stride ||
|
|
182
|
+
![tile.min.x, tile.min.y, unitsPerMeter].every(Number.isFinite) || unitsPerMeter <= 0) {
|
|
183
|
+
throw new Error('CPU projection requires a packed ground layout and finite tile metadata');
|
|
184
|
+
}
|
|
185
|
+
if (line && (!extrusion || extrusion.type !== 5122 || extrusion.size !== 2 ||
|
|
186
|
+
extrusion.offset === undefined || extrusion.offset < 0 || extrusion.offset + 4 > layout.stride ||
|
|
187
|
+
!Number.isFinite(overzoom) || overzoom < 1)) {
|
|
188
|
+
throw new Error('CPU projected lines require packed extrusion and finite overzoom');
|
|
189
|
+
}
|
|
190
|
+
const optionalLineOffsets = line ? layout.dynamic_attribs.filter(attribute =>
|
|
191
|
+
['a_offset', 'a_z_and_offset_scale'].includes(attribute.name)).map(attribute => {
|
|
192
|
+
if (attribute.type !== 5122 || attribute.size !== 2 || attribute.offset === undefined ||
|
|
193
|
+
attribute.offset < 0 || attribute.offset + 4 > layout.stride) {
|
|
194
|
+
throw new Error('CPU projected lines require packed ground attributes');
|
|
195
|
+
}
|
|
196
|
+
return attribute.offset;
|
|
197
|
+
}) : [];
|
|
198
|
+
for (const [name, size] of [['a_projected_stroke', 4], ['a_projected_normals', 4], ['a_projected_widths', 2]] as const) {
|
|
199
|
+
const attribute = layout.dynamic_attribs.find(attribute => attribute.name === name);
|
|
200
|
+
if (attribute && (attribute.type !== 5126 || attribute.size !== size || attribute.offset === undefined ||
|
|
201
|
+
attribute.offset < 0 || attribute.offset + size * 4 > layout.stride)) {
|
|
202
|
+
throw new Error('CPU projected stroke attributes require a valid float layout');
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const source = new DataView(request.vertices.buffer, request.vertices.byteOffset, request.vertices.byteLength);
|
|
206
|
+
for (let offset = 0; offset < source.byteLength; offset += layout.stride) {
|
|
207
|
+
const heightOffset = line ? layout.dynamic_attribs.find(attribute => attribute.name === 'a_z_and_offset_scale')?.offset : position.offset + 4;
|
|
208
|
+
if (heightOffset !== undefined && source.getInt16(offset + heightOffset, true) !== 0 &&
|
|
209
|
+
(line || !projection.allowElevation)) throw new Error('CPU projection elevation requires allowElevation for polygon/raster meshes');
|
|
210
|
+
}
|
|
211
|
+
// A ribbon's real corners are centerline + packed extrusion. Refine those
|
|
212
|
+
// edges (including width and round joins), not only the collapsed centerline.
|
|
213
|
+
const positionOffset = position.offset;
|
|
214
|
+
const extrusionOffset = extrusion?.offset ?? 0;
|
|
215
|
+
const offsetAttribute = layout.dynamic_attribs.find(attribute => attribute.name === 'a_offset');
|
|
216
|
+
let delta = Math.max(0, Math.min(4, (tile.style_z ?? tile.coords.z) - tile.coords.z));
|
|
217
|
+
delta += (delta >= 1 ? 1 - delta : 0) + 2 * Math.max(0, Math.min(1, (delta - 2) / 2));
|
|
218
|
+
const widthFactor = (vertex: DataView): number => 1 - vertex.getInt16(positionOffset + 4, true) / 1024 * (delta - 0.5) * 2;
|
|
219
|
+
const readPosition = (vertex: DataView): [number, number] => [
|
|
220
|
+
vertex.getInt16(positionOffset, true) + (line ? (vertex.getInt16(extrusionOffset, true) * widthFactor(vertex) +
|
|
221
|
+
(offsetAttribute?.offset === undefined ? 0 : vertex.getInt16(offsetAttribute.offset, true))) / overzoom : 0),
|
|
222
|
+
vertex.getInt16(positionOffset + 2, true) + (line ? (vertex.getInt16(extrusionOffset + 2, true) * widthFactor(vertex) +
|
|
223
|
+
(offsetAttribute?.offset === undefined ? 0 : vertex.getInt16(offsetAttribute.offset + 2, true))) / overzoom : 0)
|
|
224
|
+
];
|
|
225
|
+
const metadata = JSON.stringify({geometry: request.geometry, tileZoom: tile.coords.z, min: tile.min, overzoom, styleZoom: tile.style_z,
|
|
226
|
+
stride: layout.stride, attributes: layout.dynamic_attribs.map(attribute =>
|
|
227
|
+
[attribute.name, attribute.type, attribute.size, attribute.offset, attribute.normalized]),
|
|
228
|
+
maxAngularSpan: projection.maxAngularSpan, maxAdditionalVertices: projection.maxAdditionalVertices});
|
|
229
|
+
const prepared = preparationCache.getOrCreate(metadata, request.vertices, request.indices, () =>
|
|
230
|
+
refineGlobeMesh(request.vertices, request.indices, layout, {
|
|
231
|
+
tileZoom: tile.coords.z, maxAngularSpan: projection.maxAngularSpan,
|
|
232
|
+
maxAdditionalVertices: projection.maxAdditionalVertices,
|
|
233
|
+
...(line ? {getPosition: readPosition} : {})
|
|
234
|
+
}));
|
|
235
|
+
const clipped = clipProjectedMesh(prepared, request, readPosition, projection.maxAdditionalVertices! -
|
|
236
|
+
(prepared.vertices.byteLength - request.vertices.byteLength) / layout.stride);
|
|
237
|
+
// Never mutate or transfer cached/source buffers, even when refinement adds no vertices.
|
|
238
|
+
const domain = getProjectedSourceDomain(projection.type);
|
|
239
|
+
const geographicPosition = (vertex: DataView, center = false, displacement: readonly [number, number] = [0, 0]): [number, number] => {
|
|
240
|
+
let [localX, localY] = center ? [vertex.getInt16(positionOffset, true), vertex.getInt16(positionOffset + 2, true)] : readPosition(vertex);
|
|
241
|
+
localX += displacement[0]; localY += displacement[1];
|
|
242
|
+
// Clipping handled whole triangles. Clamp only packed-coordinate rounding at cut intersections.
|
|
243
|
+
const x = Math.max(domain[0], Math.min(domain[2], tile.min.x + localX / unitsPerMeter));
|
|
244
|
+
const y = Math.max(domain[1], Math.min(domain[3], tile.min.y + localY / unitsPerMeter));
|
|
245
|
+
const longitude = Math.max(-180, Math.min(180, x / SPHERE_RADIUS * 180 / Math.PI));
|
|
246
|
+
const latitude = Math.max(-85.0511287798066, Math.min(85.0511287798066,
|
|
247
|
+
(2 * Math.atan(Math.exp(y / SPHERE_RADIUS)) - Math.PI / 2) * 180 / Math.PI));
|
|
248
|
+
return [longitude, latitude];
|
|
249
|
+
};
|
|
250
|
+
const refined = yield* refineProjectedMesh(clipped, request,
|
|
251
|
+
(vertex, displacement) => geographicPosition(vertex, false, displacement), readPosition, work);
|
|
252
|
+
const result = {vertices: refined.vertices.slice(), indices: refined.indices.slice()};
|
|
253
|
+
const coordinates: number[] = [];
|
|
254
|
+
for (let offset = 0; offset < result.vertices.byteLength; offset += layout.stride) {
|
|
255
|
+
coordinates.push(...geographicPosition(new DataView(result.vertices.buffer, result.vertices.byteOffset + offset, layout.stride)));
|
|
256
|
+
}
|
|
257
|
+
const stroke = layout.dynamic_attribs.find(attribute => attribute.name === 'a_projected_stroke');
|
|
258
|
+
const normals = layout.dynamic_attribs.find(attribute => attribute.name === 'a_projected_normals');
|
|
259
|
+
const widths = layout.dynamic_attribs.find(attribute => attribute.name === 'a_projected_widths');
|
|
260
|
+
const vertexCount = result.vertices.byteLength / layout.stride;
|
|
261
|
+
// Four clamped probes per wall normal share the final host/local batch.
|
|
262
|
+
// Domain-edge probes use their actual physical separation (one-sided there).
|
|
263
|
+
const normalSamples = new Map<number, {offset: number; eastSpan: number; northSpan: number}>();
|
|
264
|
+
if (!line && projectedNormal?.offset !== undefined && surfaceNormal?.offset !== undefined) {
|
|
265
|
+
for (let index = 0; index < vertexCount; index++) {
|
|
266
|
+
const vertex = new DataView(result.vertices.buffer, result.vertices.byteOffset + index * layout.stride, layout.stride);
|
|
267
|
+
if (vertex.getInt8(surfaceNormal.offset) === 0 && vertex.getInt8(surfaceNormal.offset + 1) === 0) continue;
|
|
268
|
+
const samples = [[-8, 0], [8, 0], [0, -8], [0, 8]].map(displacement =>
|
|
269
|
+
geographicPosition(vertex, false, [displacement[0], displacement[1]]));
|
|
270
|
+
const latitudeScale = Math.cos(geographicPosition(vertex)[1] * Math.PI / 180);
|
|
271
|
+
const mercatorY = (latitude: number) => SPHERE_RADIUS * Math.asinh(Math.tan(latitude * Math.PI / 180));
|
|
272
|
+
normalSamples.set(index, {offset: coordinates.length,
|
|
273
|
+
eastSpan: (samples[1][0] - samples[0][0]) * Math.PI / 180 * SPHERE_RADIUS * latitudeScale,
|
|
274
|
+
northSpan: (mercatorY(samples[3][1]) - mercatorY(samples[2][1])) * latitudeScale});
|
|
275
|
+
for (const sample of samples) coordinates.push(...sample);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
const roadSamplesOffset = coordinates.length / 2;
|
|
279
|
+
if (line && stroke?.offset !== undefined) for (let offset = 0; offset < result.vertices.byteLength; offset += layout.stride) {
|
|
280
|
+
coordinates.push(...geographicPosition(new DataView(result.vertices.buffer, result.vertices.byteOffset + offset, layout.stride), true));
|
|
281
|
+
}
|
|
282
|
+
// Project adjacent road tangents, not source normals. Their perpendiculars
|
|
283
|
+
// define CSS widths even under anisotropic high-latitude projections.
|
|
284
|
+
if (line && stroke?.offset !== undefined && normals?.offset !== undefined && widths?.offset !== undefined) {
|
|
285
|
+
for (let offset = 0; offset < result.vertices.byteLength; offset += layout.stride) {
|
|
286
|
+
const vertex = new DataView(result.vertices.buffer, result.vertices.byteOffset + offset, layout.stride);
|
|
287
|
+
for (let normal = 0; normal < 2; normal++) {
|
|
288
|
+
const normalX = vertex.getFloat32(normals.offset + normal * 8, true);
|
|
289
|
+
const normalY = vertex.getFloat32(normals.offset + normal * 8 + 4, true);
|
|
290
|
+
for (const direction of [-8, 8]) coordinates.push(...geographicPosition(vertex, true,
|
|
291
|
+
[normalY * direction, -normalX * direction]));
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
const positions = yield new Float64Array(coordinates);
|
|
296
|
+
if (!(positions instanceof Float64Array) || positions.length !== coordinates.length ||
|
|
297
|
+
!positions.every(value => Number.isFinite(value) && Math.abs(value) <= 3.4028234663852886e38)) {
|
|
298
|
+
throw new Error('Projection engine returned an invalid common-position batch');
|
|
299
|
+
}
|
|
300
|
+
const output = new DataView(result.vertices.buffer, result.vertices.byteOffset, result.vertices.byteLength);
|
|
301
|
+
for (let index = 0; index < vertexCount; index++) {
|
|
302
|
+
const offset = index * layout.stride + projected.offset;
|
|
303
|
+
output.setFloat32(offset, positions[index * 2], true);
|
|
304
|
+
output.setFloat32(offset + 4, positions[index * 2 + 1], true);
|
|
305
|
+
output.setFloat32(offset + 8, line ? 0 :
|
|
306
|
+
output.getInt16(index * layout.stride + position.offset + 4, true) / PACKED_HEIGHT_SCALE * PROJECTED_COMMON_SCALE, true);
|
|
307
|
+
if (projectedNormal?.offset !== undefined) {
|
|
308
|
+
const normalOffset = projectedNormal.offset;
|
|
309
|
+
const packedOffset = surfaceNormal?.offset;
|
|
310
|
+
const normal: [number, number, number] = packedOffset === undefined ? [0, 0, 1] : [
|
|
311
|
+
output.getInt8(index * layout.stride + packedOffset) / 127,
|
|
312
|
+
output.getInt8(index * layout.stride + packedOffset + 1) / 127,
|
|
313
|
+
output.getInt8(index * layout.stride + packedOffset + 2) / 127];
|
|
314
|
+
const sample = normalSamples.get(index);
|
|
315
|
+
const transformed = sample ? projectSurfaceNormal(normal,
|
|
316
|
+
[(positions[sample.offset + 2] - positions[sample.offset]) / sample.eastSpan,
|
|
317
|
+
(positions[sample.offset + 3] - positions[sample.offset + 1]) / sample.eastSpan],
|
|
318
|
+
[(positions[sample.offset + 6] - positions[sample.offset + 4]) / sample.northSpan,
|
|
319
|
+
(positions[sample.offset + 7] - positions[sample.offset + 5]) / sample.northSpan]) : normal;
|
|
320
|
+
const length = Math.hypot(...transformed);
|
|
321
|
+
if (!Number.isFinite(length) || length === 0) throw new Error('Projected surface normal must be nonzero and finite');
|
|
322
|
+
transformed.forEach((value, component) => output.setFloat32(index * layout.stride + normalOffset + component * 4, value / length, true));
|
|
323
|
+
}
|
|
324
|
+
if (line && stroke?.offset !== undefined) {
|
|
325
|
+
const vertex = new DataView(result.vertices.buffer, result.vertices.byteOffset + index * layout.stride, layout.stride);
|
|
326
|
+
const pixelScale = vertex.getFloat32(stroke.offset + 12, true);
|
|
327
|
+
let radius = Math.hypot(readPosition(vertex)[0] - vertex.getInt16(positionOffset, true),
|
|
328
|
+
readPosition(vertex)[1] - vertex.getInt16(positionOffset + 2, true)) * overzoom * pixelScale;
|
|
329
|
+
if (pixelScale > 0 && normals?.offset !== undefined && widths?.offset !== undefined) {
|
|
330
|
+
const normalsOffset = normals.offset;
|
|
331
|
+
const sourceNormals = [0, 1].map(normal => [vertex.getFloat32(normalsOffset + normal * 8, true),
|
|
332
|
+
vertex.getFloat32(normalsOffset + normal * 8 + 4, true)]);
|
|
333
|
+
const projectedNormals = [0, 1].map(normal => {
|
|
334
|
+
const sample = (roadSamplesOffset + vertexCount + index * 4 + normal * 2) * 2;
|
|
335
|
+
const tangentX = positions[sample + 2] - positions[sample];
|
|
336
|
+
const tangentY = positions[sample + 3] - positions[sample + 1];
|
|
337
|
+
const length = Math.hypot(tangentX, tangentY);
|
|
338
|
+
return length === 0 ? [0, 0] : [-tangentY / length, tangentX / length];
|
|
339
|
+
});
|
|
340
|
+
const halfWidth = vertex.getFloat32(widths.offset, true);
|
|
341
|
+
const offsetWidth = vertex.getFloat32(widths.offset + 4, true);
|
|
342
|
+
const extrusion = [vertex.getInt16(extrusionOffset, true), vertex.getInt16(extrusionOffset + 2, true)];
|
|
343
|
+
const shape = reorientProjectedStroke(extrusion, halfWidth, sourceNormals, projectedNormals);
|
|
344
|
+
const offsetShape = reorientProjectedStroke([
|
|
345
|
+
(offsetAttribute?.offset === undefined ? 0 : vertex.getInt16(offsetAttribute.offset, true)),
|
|
346
|
+
(offsetAttribute?.offset === undefined ? 0 : vertex.getInt16(offsetAttribute.offset + 2, true))],
|
|
347
|
+
Math.abs(offsetWidth), sourceNormals, projectedNormals);
|
|
348
|
+
const displacement = shape.map((value, component) =>
|
|
349
|
+
(value * widthFactor(vertex) + offsetShape[component]) * pixelScale);
|
|
350
|
+
const centerOffset = (vertexCount + index) * 2;
|
|
351
|
+
output.setFloat32(offset, positions[centerOffset] + displacement[0], true);
|
|
352
|
+
output.setFloat32(offset + 4, positions[centerOffset + 1] + displacement[1], true);
|
|
353
|
+
radius = Math.hypot(...displacement);
|
|
354
|
+
}
|
|
355
|
+
output.setFloat32(index * layout.stride + stroke.offset, positions[(vertexCount + index) * 2], true);
|
|
356
|
+
output.setFloat32(index * layout.stride + stroke.offset + 4, positions[(vertexCount + index) * 2 + 1], true);
|
|
357
|
+
output.setFloat32(index * layout.stride + stroke.offset + 8, radius, true);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return result;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** Retain cap/fan shape and Tangram's miter convention around projected segment perpendiculars. */
|
|
364
|
+
function reorientProjectedStroke(vector: number[], halfWidth: number, sourceNormals: number[][],
|
|
365
|
+
projectedNormals: number[][]): number[] {
|
|
366
|
+
const length = Math.hypot(...vector);
|
|
367
|
+
if (length === 0 || halfWidth === 0) return [0, 0];
|
|
368
|
+
const normalize = (normal: number[]) => {
|
|
369
|
+
const magnitude = Math.hypot(...normal);
|
|
370
|
+
return magnitude === 0 ? [0, 0] : normal.map(value => value / magnitude);
|
|
371
|
+
};
|
|
372
|
+
const [first, second] = sourceNormals.map(normalize);
|
|
373
|
+
const [projectedFirst, projectedSecond] = projectedNormals;
|
|
374
|
+
const cross = (first: number[], second: number[]) => first[0] * second[1] - first[1] * second[0];
|
|
375
|
+
const dot = (first: number[], second: number[]) => first[0] * second[0] + first[1] * second[1];
|
|
376
|
+
const angle = Math.atan2(cross(first, second), dot(first, second));
|
|
377
|
+
if (Math.abs(angle) < 0.0001) {
|
|
378
|
+
// Restore authored radii at the packed edge, rather than carrying a
|
|
379
|
+
// direction-dependent integer-rounding loss into CSS-pixel widths.
|
|
380
|
+
const snap = (distance: number) => Math.abs(Math.abs(distance) - halfWidth) <= 1 ?
|
|
381
|
+
Math.sign(distance) * halfWidth : Math.abs(distance) <= 1 ? 0 : distance;
|
|
382
|
+
const normalDistance = snap(dot(vector, first));
|
|
383
|
+
const tangentDistance = snap(vector[0] * first[1] - vector[1] * first[0]);
|
|
384
|
+
return [projectedFirst[0] * normalDistance + projectedFirst[1] * tangentDistance,
|
|
385
|
+
projectedFirst[1] * normalDistance - projectedFirst[0] * tangentDistance];
|
|
386
|
+
}
|
|
387
|
+
const side = Math.sign(dot(vector, [first[0] + second[0], first[1] + second[1]]));
|
|
388
|
+
if (length > halfWidth * 1.01) {
|
|
389
|
+
const bisector = normalize([projectedFirst[0] + projectedSecond[0], projectedFirst[1] + projectedSecond[1]]);
|
|
390
|
+
const scale = 2 / (1 + Math.abs(dot(projectedFirst, bisector)));
|
|
391
|
+
return bisector.map(value => value * scale ** 2 * halfWidth * side);
|
|
392
|
+
}
|
|
393
|
+
const direction = vector.map(value => value * side / length);
|
|
394
|
+
const fraction = Math.max(0, Math.min(1, Math.atan2(cross(first, direction), dot(first, direction)) / angle));
|
|
395
|
+
const projectedAngle = Math.atan2(cross(projectedFirst, projectedSecond), dot(projectedFirst, projectedSecond)) * fraction;
|
|
396
|
+
const cosine = Math.cos(projectedAngle), sine = Math.sin(projectedAngle);
|
|
397
|
+
return [(projectedFirst[0] * cosine - projectedFirst[1] * sine) * length * side,
|
|
398
|
+
(projectedFirst[0] * sine + projectedFirst[1] * cosine) * length * side];
|
|
399
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// tangram-layers
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import {PROJECTED_COMMON_SCALE} from '../procedures/projected-coordinate-transform';
|
|
6
|
+
|
|
7
|
+
/** Transform an ENU normal with the inverse transpose of a sampled map Jacobian.
|
|
8
|
+
* Columns are common XY per physical east/north meter; vertical meters retain
|
|
9
|
+
* the projection's fixed common scale. Reject singular kernels rather than emit
|
|
10
|
+
* NaNs or invent a surface orientation. This is not a terrain normal map.
|
|
11
|
+
*/
|
|
12
|
+
export function projectSurfaceNormal(normal: readonly [number, number, number],
|
|
13
|
+
east: readonly [number, number], north: readonly [number, number]): [number, number, number] {
|
|
14
|
+
const determinant = east[0] * north[1] - north[0] * east[1];
|
|
15
|
+
const magnitude = Math.hypot(...east) * Math.hypot(...north);
|
|
16
|
+
if (![...normal, ...east, ...north].every(Number.isFinite) || magnitude === 0 ||
|
|
17
|
+
Math.abs(determinant) <= magnitude * 1e-12) throw new Error('Projected surface normal requires a nonsingular finite Jacobian');
|
|
18
|
+
const transformed: [number, number, number] = [
|
|
19
|
+
(north[1] * normal[0] - east[1] * normal[1]) / determinant,
|
|
20
|
+
(east[0] * normal[1] - north[0] * normal[0]) / determinant,
|
|
21
|
+
normal[2] / PROJECTED_COMMON_SCALE
|
|
22
|
+
];
|
|
23
|
+
const length = Math.hypot(...transformed);
|
|
24
|
+
if (!Number.isFinite(length) || length === 0) throw new Error('Projected surface normal must be nonzero and finite');
|
|
25
|
+
return [transformed[0] / length, transformed[1] / length, transformed[2] / length];
|
|
26
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// tangram-layers
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import {getProjectedSourceDomain} from './projected-mesh-domain';
|
|
6
|
+
import {PACKED_HEIGHT_SCALE} from '../gl/vertex-constants';
|
|
7
|
+
import {PROJECTED_COMMON_SCALE} from '../procedures/projected-coordinate-transform';
|
|
8
|
+
import type {MeshProjectionRequest, ProjectedMesh} from '../procedures/mesh-projector';
|
|
9
|
+
|
|
10
|
+
/** Project billboard anchors without subdividing, reordering or clipping atlas quads or label byte ranges. */
|
|
11
|
+
export function* projectSymbolMesh(request: MeshProjectionRequest): Generator<Float64Array, ProjectedMesh, Float64Array> {
|
|
12
|
+
const {layout, tile} = request;
|
|
13
|
+
const position = layout.dynamic_attribs.find(attribute => attribute.name === 'a_position');
|
|
14
|
+
const projected = layout.dynamic_attribs.find(attribute => attribute.name === 'a_projected_position');
|
|
15
|
+
const shape = layout.dynamic_attribs.find(attribute => attribute.name === 'a_shape');
|
|
16
|
+
if (!Number.isSafeInteger(layout.stride) || layout.stride <= 0 || request.vertices.byteLength % layout.stride ||
|
|
17
|
+
!position || position.type !== 5122 || position.size !== 4 || position.offset === undefined ||
|
|
18
|
+
position.offset < 0 || position.offset + 8 > layout.stride ||
|
|
19
|
+
!shape || shape.type !== 5122 || shape.size !== 4 || shape.offset === undefined ||
|
|
20
|
+
shape.offset < 0 || shape.offset + 8 > layout.stride ||
|
|
21
|
+
!projected || projected.type !== 5126 || projected.size !== 3 || projected.offset === undefined ||
|
|
22
|
+
projected.offset < 0 || projected.offset + 12 > layout.stride ||
|
|
23
|
+
![tile.min.x, tile.min.y].every(Number.isFinite) || !Number.isSafeInteger(tile.coords.z) ||
|
|
24
|
+
tile.coords.z < 0 || tile.coords.z > 22) throw new Error('Projected symbols require a packed billboard layout and finite tile metadata');
|
|
25
|
+
const projectedOffset = projected.offset;
|
|
26
|
+
const positionOffset = position.offset;
|
|
27
|
+
const count = request.vertices.byteLength / layout.stride;
|
|
28
|
+
const indices = request.indices === false ? new Uint32Array(Array.from({length: count}, (_, index) => index)) : request.indices.slice();
|
|
29
|
+
if (indices.length % 3 || indices.some(index => index >= count)) throw new Error('Projected symbols require valid triangle indices');
|
|
30
|
+
const vertices = request.vertices.slice();
|
|
31
|
+
const view = new DataView(vertices.buffer, vertices.byteOffset, vertices.byteLength);
|
|
32
|
+
const unitsPerMeter = 4096 * 2 ** tile.coords.z / (2 * Math.PI * 6378137);
|
|
33
|
+
const domain = getProjectedSourceDomain(request.projection.type);
|
|
34
|
+
const coordinates: number[] = [];
|
|
35
|
+
const anchors = new Map<string, number>();
|
|
36
|
+
const anchorIndices: number[] = [];
|
|
37
|
+
for (let index = 0; index < count; index++) {
|
|
38
|
+
const offset = index * layout.stride;
|
|
39
|
+
if (view.getInt16(offset + position.offset + 4, true) !== 0 && !request.projection.allowElevation) {
|
|
40
|
+
throw new Error('Projected symbols require ground anchors unless allowElevation is enabled');
|
|
41
|
+
}
|
|
42
|
+
const x = tile.min.x + view.getInt16(offset + position.offset, true) / unitsPerMeter;
|
|
43
|
+
const y = tile.min.y + view.getInt16(offset + position.offset + 2, true) / unitsPerMeter;
|
|
44
|
+
if (x < domain[0] || x > domain[2] || y < domain[1] || y > domain[3]) {
|
|
45
|
+
view.setInt16(offset + shape.offset + 6, 0, true);
|
|
46
|
+
for (let component = 0; component < 3; component++) view.setFloat32(offset + projected.offset + component * 4, 0, true);
|
|
47
|
+
anchorIndices.push(-1);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
const key = `${x},${y}`;
|
|
51
|
+
let anchor = anchors.get(key);
|
|
52
|
+
if (anchor === undefined) {
|
|
53
|
+
anchor = coordinates.length / 2;
|
|
54
|
+
anchors.set(key, anchor);
|
|
55
|
+
coordinates.push(x / 6378137 * 180 / Math.PI,
|
|
56
|
+
(2 * Math.atan(Math.exp(y / 6378137)) - Math.PI / 2) * 180 / Math.PI);
|
|
57
|
+
}
|
|
58
|
+
anchorIndices.push(anchor);
|
|
59
|
+
}
|
|
60
|
+
const positions = coordinates.length ? yield new Float64Array(coordinates) : new Float64Array();
|
|
61
|
+
if (!(positions instanceof Float64Array) || positions.length !== coordinates.length ||
|
|
62
|
+
!positions.every(value => Number.isFinite(value) && Math.abs(value) <= 3.4028234663852886e38)) {
|
|
63
|
+
throw new Error('Projection engine returned an invalid symbol-position batch');
|
|
64
|
+
}
|
|
65
|
+
anchorIndices.forEach((anchor, index) => {
|
|
66
|
+
if (anchor < 0) return;
|
|
67
|
+
const offset = index * layout.stride + projectedOffset;
|
|
68
|
+
view.setFloat32(offset, positions[anchor * 2], true);
|
|
69
|
+
view.setFloat32(offset + 4, positions[anchor * 2 + 1], true);
|
|
70
|
+
const height = view.getInt16(index * layout.stride + positionOffset + 4, true) / PACKED_HEIGHT_SCALE;
|
|
71
|
+
view.setFloat32(offset + 8, height * PROJECTED_COMMON_SCALE, true);
|
|
72
|
+
});
|
|
73
|
+
return {vertices, indices};
|
|
74
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
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
|
+
// WebGL constants - need to import these separately to make them available in the web worker
|
|
7
|
+
|
|
8
|
+
export const gl: Record<string, number> = {};
|
|
9
|
+
export default gl;
|
|
10
|
+
|
|
11
|
+
/* DataType */
|
|
12
|
+
gl.BYTE = 0x1400;
|
|
13
|
+
gl.UNSIGNED_BYTE = 0x1401;
|
|
14
|
+
gl.SHORT = 0x1402;
|
|
15
|
+
gl.UNSIGNED_SHORT = 0x1403;
|
|
16
|
+
gl.INT = 0x1404;
|
|
17
|
+
gl.UNSIGNED_INT = 0x1405;
|
|
18
|
+
gl.FLOAT = 0x1406;
|
|
@@ -0,0 +1,103 @@
|
|
|
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
|
+
// WebGL context wrapper
|
|
7
|
+
|
|
8
|
+
/** A host scope must return the callback result without taking ownership of its context. */
|
|
9
|
+
type ContextScope = <Result>(callback: () => Result) => Result;
|
|
10
|
+
|
|
11
|
+
interface ContextNamespace {
|
|
12
|
+
configure<Context extends object>(gl: Context, scope?: ContextScope): Context & {_tangram_id: number};
|
|
13
|
+
withContext<T>(gl: object | null | undefined, callback: () => T): T;
|
|
14
|
+
hasContextScope(gl: object | null | undefined): boolean;
|
|
15
|
+
getContext(canvas?: HTMLCanvasElement | null, options?: WebGLContextAttributes & {device_pixel_ratio?: number}): WebGLRenderingContext;
|
|
16
|
+
resize(gl: WebGLRenderingContext, width: number, height: number, devicePixelRatio?: number): void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const Context: ContextNamespace = {configure, withContext, hasContextScope, getContext, resize};
|
|
20
|
+
export default Context;
|
|
21
|
+
|
|
22
|
+
let context_id = 0;
|
|
23
|
+
const context_scopes = new WeakMap<object, ContextScope>();
|
|
24
|
+
|
|
25
|
+
// Register a WebGL context for Tangram use without taking ownership of it.
|
|
26
|
+
function configure<Context extends object>(gl: Context, scope?: ContextScope): Context & {_tangram_id: number}
|
|
27
|
+
{
|
|
28
|
+
const configured = hasContextIdentity(gl) ? gl : Object.assign(gl, {_tangram_id: context_id++});
|
|
29
|
+
if (scope) {
|
|
30
|
+
context_scopes.set(gl, scope);
|
|
31
|
+
}
|
|
32
|
+
return configured;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Preserve already configured contexts without rewriting caller-owned properties. */
|
|
36
|
+
function hasContextIdentity<Context extends object>(context: Context): context is Context & {_tangram_id: number} {
|
|
37
|
+
return '_tangram_id' in context && typeof context._tangram_id === 'number';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Run WebGL work inside an optional host-managed state scope.
|
|
41
|
+
function withContext<T>(gl: object | null | undefined, callback: () => T): T
|
|
42
|
+
{
|
|
43
|
+
const scope = gl && context_scopes.get(gl);
|
|
44
|
+
return scope ? scope(callback) : callback();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function hasContextScope(gl: object | null | undefined): boolean
|
|
48
|
+
{
|
|
49
|
+
return Boolean(gl && context_scopes.has(gl));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Setup a WebGL context
|
|
53
|
+
// If no canvas element is provided, one is created and added to the document body
|
|
54
|
+
function getContext(canvas?: HTMLCanvasElement | null, options: WebGLContextAttributes & {device_pixel_ratio?: number} = {}): WebGLRenderingContext
|
|
55
|
+
{
|
|
56
|
+
var fullscreen = false;
|
|
57
|
+
if (canvas == null) {
|
|
58
|
+
canvas = document.createElement('canvas');
|
|
59
|
+
canvas.style.position = 'absolute';
|
|
60
|
+
canvas.style.top = '0';
|
|
61
|
+
canvas.style.left = '0';
|
|
62
|
+
canvas.style.zIndex = '-1';
|
|
63
|
+
document.body.appendChild(canvas);
|
|
64
|
+
fullscreen = true;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// powerPreference context option spec requires listeners for context loss/restore,
|
|
68
|
+
// though it's not clear these are required in practice.
|
|
69
|
+
// https://www.khronos.org/registry/webgl/specs/latest/1.0/#5.2.1
|
|
70
|
+
canvas.addEventListener('webglcontextlost', () => {});
|
|
71
|
+
canvas.addEventListener('webglcontextrestored', () => {});
|
|
72
|
+
|
|
73
|
+
const gl = canvas.getContext('webgl', options) || canvas.getContext('experimental-webgl', options);
|
|
74
|
+
if (!gl || !('getParameter' in gl)) {
|
|
75
|
+
throw new Error('Couldn\'t create WebGL context.');
|
|
76
|
+
}
|
|
77
|
+
Context.configure(gl);
|
|
78
|
+
|
|
79
|
+
if (!fullscreen) {
|
|
80
|
+
Context.resize(gl, parseFloat(canvas.style.width), parseFloat(canvas.style.height), options.device_pixel_ratio);
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
Context.resize(gl, window.innerWidth, window.innerHeight, options.device_pixel_ratio);
|
|
84
|
+
window.addEventListener('resize', function () {
|
|
85
|
+
Context.resize(gl, window.innerWidth, window.innerHeight, options.device_pixel_ratio);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return gl;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function resize(gl: WebGLRenderingContext, width: number, height: number, device_pixel_ratio?: number): void
|
|
93
|
+
{
|
|
94
|
+
device_pixel_ratio = device_pixel_ratio || window.devicePixelRatio || 1;
|
|
95
|
+
if ('style' in gl.canvas) {
|
|
96
|
+
gl.canvas.style.width = width + 'px';
|
|
97
|
+
gl.canvas.style.height = height + 'px';
|
|
98
|
+
}
|
|
99
|
+
gl.canvas.width = Math.round(width * device_pixel_ratio);
|
|
100
|
+
gl.canvas.height = Math.round(height * device_pixel_ratio);
|
|
101
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
102
|
+
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
|
|
103
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
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
|
+
// WebGL extension wrapper
|
|
7
|
+
// Stores extensions by name and GL context
|
|
8
|
+
|
|
9
|
+
// list of extension arrays, for each entry, 1st element GL context, 2nd map of extensions by name
|
|
10
|
+
type WebGLContext = {
|
|
11
|
+
getExtension(name: string): unknown;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type ExtensionCache = [WebGLContext, Record<string, unknown>];
|
|
15
|
+
|
|
16
|
+
let extensions: ExtensionCache[] = [];
|
|
17
|
+
|
|
18
|
+
export default function getExtension (gl: WebGLContext, name: string): unknown {
|
|
19
|
+
let extensionMap = extensions.find(entry => entry[0] === gl)?.[1];
|
|
20
|
+
|
|
21
|
+
if (!extensionMap) {
|
|
22
|
+
extensionMap = {};
|
|
23
|
+
extensions.push([gl, extensionMap]);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!extensionMap[name]) {
|
|
27
|
+
extensionMap[name] = gl.getExtension(name);
|
|
28
|
+
}
|
|
29
|
+
return extensionMap[name];
|
|
30
|
+
}
|