@tangram.gl/tangram-renderer 0.0.0-stage → 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -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,326 @@
|
|
|
1
|
+
// tangram-layers
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import type {GeographicAnchor, HostCamera, HostFrameOptions, HostProjection, HostTileLODOptions, HostTileResourceOptions, Viewport, FeatureSelectionOptions} from '../types';
|
|
6
|
+
|
|
7
|
+
/** Validated per-eye state; optional visibility overrides never change scene/style state. */
|
|
8
|
+
export interface NormalizedRenderView {
|
|
9
|
+
readonly id: string;
|
|
10
|
+
readonly viewport: Required<Viewport>;
|
|
11
|
+
readonly camera: HostCamera;
|
|
12
|
+
readonly geographicAnchor?: Required<GeographicAnchor>;
|
|
13
|
+
readonly projection?: HostProjection;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Host-owned logical frame, shared tile visibility, and independently drawable eyes. */
|
|
17
|
+
export default class HostFrame {
|
|
18
|
+
/** Resolve a full-target or view-local CSS pixel without changing the active camera. */
|
|
19
|
+
resolveSelectionPoint(pixel: {x: number; y: number}, options: FeatureSelectionOptions = {},
|
|
20
|
+
activeViewId = this.activeRenderViewId): {view: NormalizedRenderView; pixel: {x: number; y: number}} | null {
|
|
21
|
+
if (![pixel.x, pixel.y].every(Number.isFinite)) throw new Error('Selection coordinates must be finite');
|
|
22
|
+
if (options.coordinateSpace !== undefined && !['view', 'canvas'].includes(options.coordinateSpace)) {
|
|
23
|
+
throw new Error('Selection coordinateSpace must be view or canvas');
|
|
24
|
+
}
|
|
25
|
+
const contains = (view: NormalizedRenderView) => pixel.x >= view.viewport.x && pixel.y >= view.viewport.y &&
|
|
26
|
+
pixel.x < view.viewport.x + view.viewport.width && pixel.y < view.viewport.y + view.viewport.height;
|
|
27
|
+
const view = options.renderViewId !== undefined ? this.getRenderView(options.renderViewId) :
|
|
28
|
+
options.coordinateSpace === 'canvas' ? [...this.renderViews].reverse().find(contains) : this.getRenderView(activeViewId);
|
|
29
|
+
if (!view) return null;
|
|
30
|
+
const local = options.coordinateSpace === 'canvas' ? {x: pixel.x - view.viewport.x, y: pixel.y - view.viewport.y} : {...pixel};
|
|
31
|
+
if (local.x < 0 || local.y < 0 || local.x >= view.viewport.width || local.y >= view.viewport.height) return null;
|
|
32
|
+
return {view, pixel: local};
|
|
33
|
+
}
|
|
34
|
+
/** Full target dimensions in CSS pixels; origins are top-left. */
|
|
35
|
+
readonly viewport: Required<Viewport>;
|
|
36
|
+
/** Shared geographic/style anchor in degrees, altitude meters, and Tangram zoom. */
|
|
37
|
+
readonly geographicAnchor: Required<GeographicAnchor>;
|
|
38
|
+
/** Shared geographic projection. */
|
|
39
|
+
readonly projection: HostProjection;
|
|
40
|
+
/** Per-eye camera and footprint state. */
|
|
41
|
+
readonly renderViews: readonly NormalizedRenderView[];
|
|
42
|
+
/** Default eye to draw. */
|
|
43
|
+
readonly activeRenderViewId: string;
|
|
44
|
+
/** Conservative neighboring tile buffer. */
|
|
45
|
+
readonly tileBuffer: number;
|
|
46
|
+
/** Shared data-tile level, independent of scene/style zoom, when supplied. */
|
|
47
|
+
readonly tileZoom?: number;
|
|
48
|
+
/** Validated projected-scale LOD settings, when enabled. */
|
|
49
|
+
readonly tileLOD?: Readonly<Required<HostTileLODOptions>>;
|
|
50
|
+
/** Shared opt-in worker and completed off-screen mesh cache limits. */
|
|
51
|
+
readonly tileResources?: Readonly<HostTileResourceOptions>;
|
|
52
|
+
/** Optional globally resident globe fallback level, capped at 64 coordinates per source. */
|
|
53
|
+
readonly globePreloadZoom?: number;
|
|
54
|
+
/** Shared elapsed scene animation time in seconds, when supplied. */
|
|
55
|
+
readonly animationTime?: number;
|
|
56
|
+
|
|
57
|
+
/** Copies and validates a host frame without retaining caller matrix arrays. */
|
|
58
|
+
constructor(options: HostFrameOptions) {
|
|
59
|
+
const record = requireRecord(options, 'HostFrame');
|
|
60
|
+
this.viewport = normalizeViewport(record.viewport, 'HostFrame viewport');
|
|
61
|
+
this.geographicAnchor = normalizeAnchor(record.geographicAnchor);
|
|
62
|
+
this.projection = normalizeProjection(record.projection);
|
|
63
|
+
this.tileBuffer = normalizeNonNegative(record.tileBuffer ?? 0, 'tileBuffer');
|
|
64
|
+
this.tileZoom = normalizeTileZoom(record.tileZoom, this.geographicAnchor.zoom);
|
|
65
|
+
this.tileLOD = normalizeTileLOD(record.tileLOD);
|
|
66
|
+
if (this.projection.type === 'projected' && (this.tileLOD || this.tileZoom === undefined || this.tileZoom > 6)) {
|
|
67
|
+
throw new Error('HostFrame CPU projection requires explicit tileZoom <= 6 and does not support automatic LOD');
|
|
68
|
+
}
|
|
69
|
+
this.tileResources = normalizeTileResources(record.tileResources);
|
|
70
|
+
this.globePreloadZoom = normalizeGlobePreloadZoom(record.globePreloadZoom);
|
|
71
|
+
if (this.globePreloadZoom !== undefined && this.projection.type !== 'globe') {
|
|
72
|
+
throw new Error('HostFrame globePreloadZoom requires a globe projection');
|
|
73
|
+
}
|
|
74
|
+
if (this.tileZoom !== undefined && this.tileLOD !== undefined) {
|
|
75
|
+
throw new Error('HostFrame tileZoom and tileLOD are mutually exclusive');
|
|
76
|
+
}
|
|
77
|
+
this.animationTime = record.animationTime === undefined ? undefined :
|
|
78
|
+
normalizeNonNegative(record.animationTime, 'animationTime');
|
|
79
|
+
if (!Array.isArray(record.renderViews) || record.renderViews.length === 0) {
|
|
80
|
+
throw new Error('HostFrame requires at least one render view');
|
|
81
|
+
}
|
|
82
|
+
const ids = new Set<string>();
|
|
83
|
+
this.renderViews = record.renderViews.map((value: unknown, index: number) => {
|
|
84
|
+
const view = requireRecord(value, `HostFrame render view ${index}`);
|
|
85
|
+
const id = typeof view.id === 'string' && view.id ? view.id : index === 0 ? 'default' : `view-${index}`;
|
|
86
|
+
if (ids.has(id)) {
|
|
87
|
+
throw new Error(`HostFrame render view id '${id}' is duplicated`);
|
|
88
|
+
}
|
|
89
|
+
ids.add(id);
|
|
90
|
+
const projection = view.projection === undefined ? undefined : normalizeProjection(view.projection);
|
|
91
|
+
if (projection && projection.type !== this.projection.type) {
|
|
92
|
+
throw new Error('HostFrame render views must share a projection type');
|
|
93
|
+
}
|
|
94
|
+
if (projection?.type === 'globe' && this.projection.type === 'globe' &&
|
|
95
|
+
this.projection.maxElevation !== undefined) {
|
|
96
|
+
if (projection.maxElevation === undefined) {
|
|
97
|
+
projection.maxElevation = this.projection.maxElevation;
|
|
98
|
+
}
|
|
99
|
+
else if (projection.maxElevation < this.projection.maxElevation) {
|
|
100
|
+
throw new Error('HostFrame render-view maxElevation cannot lower the shared scene bound');
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
id,
|
|
105
|
+
viewport: normalizeViewport(view.viewport ?? this.viewport, `HostFrame render view '${id}' viewport`),
|
|
106
|
+
camera: normalizeCamera(view.camera, id),
|
|
107
|
+
geographicAnchor: view.geographicAnchor === undefined ? undefined : normalizeAnchor(view.geographicAnchor),
|
|
108
|
+
projection
|
|
109
|
+
};
|
|
110
|
+
});
|
|
111
|
+
this.activeRenderViewId = typeof record.activeRenderViewId === 'string' && record.activeRenderViewId ||
|
|
112
|
+
this.renderViews[0].id;
|
|
113
|
+
this.getRenderView();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Validates untrusted application input, including the original single-view shape. */
|
|
117
|
+
static from(frame: unknown): HostFrame {
|
|
118
|
+
if (frame instanceof HostFrame) {
|
|
119
|
+
return frame;
|
|
120
|
+
}
|
|
121
|
+
const record = requireRecord(frame, 'HostFrame');
|
|
122
|
+
// Normalize unknown inputs before constructing the typed frame.
|
|
123
|
+
if (record.renderViews !== undefined || record.geographicAnchor !== undefined) {
|
|
124
|
+
return HostFrame.createValidated(record);
|
|
125
|
+
}
|
|
126
|
+
return HostFrame.fromLegacy(record);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Converts the original viewport/view/camera contract. */
|
|
130
|
+
static fromLegacy(frame: unknown): HostFrame {
|
|
131
|
+
const record = requireRecord(frame, 'HostFrame');
|
|
132
|
+
return HostFrame.createValidated({
|
|
133
|
+
...record,
|
|
134
|
+
geographicAnchor: record.view,
|
|
135
|
+
renderViews: [{id: 'default', viewport: record.viewport, camera: record.camera}]
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Returns the selected eye or throws before any scene state is modified. */
|
|
140
|
+
getRenderView(id = this.activeRenderViewId): NormalizedRenderView {
|
|
141
|
+
const view = this.renderViews.find(candidate => candidate.id === id);
|
|
142
|
+
if (!view) {
|
|
143
|
+
throw new Error(`HostFrame render view '${id}' was not found`);
|
|
144
|
+
}
|
|
145
|
+
return view;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private static createValidated(record: Record<string, unknown>): HostFrame {
|
|
149
|
+
const viewport = normalizeViewport(record.viewport, 'HostFrame viewport');
|
|
150
|
+
if (!Array.isArray(record.renderViews) || record.renderViews.length === 0) {
|
|
151
|
+
throw new Error('HostFrame requires at least one render view');
|
|
152
|
+
}
|
|
153
|
+
return new HostFrame({
|
|
154
|
+
viewport,
|
|
155
|
+
geographicAnchor: normalizeAnchor(record.geographicAnchor),
|
|
156
|
+
projection: normalizeProjection(record.projection),
|
|
157
|
+
renderViews: record.renderViews.map((value: unknown, index: number) => {
|
|
158
|
+
const view = requireRecord(value, `HostFrame render view ${index}`);
|
|
159
|
+
return {
|
|
160
|
+
id: typeof view.id === 'string' ? view.id : undefined,
|
|
161
|
+
viewport: normalizeViewport(view.viewport ?? viewport, 'HostFrame render view viewport'),
|
|
162
|
+
camera: normalizeCamera(view.camera, String(index)),
|
|
163
|
+
geographicAnchor: view.geographicAnchor === undefined ? undefined : normalizeAnchor(view.geographicAnchor),
|
|
164
|
+
projection: view.projection === undefined ? undefined : normalizeProjection(view.projection)
|
|
165
|
+
};
|
|
166
|
+
}),
|
|
167
|
+
activeRenderViewId: typeof record.activeRenderViewId === 'string' ? record.activeRenderViewId : undefined,
|
|
168
|
+
tileBuffer: normalizeNonNegative(record.tileBuffer ?? 0, 'tileBuffer'),
|
|
169
|
+
tileZoom: normalizeTileZoom(record.tileZoom, normalizeAnchor(record.geographicAnchor).zoom),
|
|
170
|
+
tileLOD: normalizeTileLOD(record.tileLOD),
|
|
171
|
+
tileResources: normalizeTileResources(record.tileResources),
|
|
172
|
+
globePreloadZoom: normalizeGlobePreloadZoom(record.globePreloadZoom),
|
|
173
|
+
animationTime: record.animationTime === undefined ? undefined : normalizeNonNegative(record.animationTime, 'animationTime')
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Reject unsafe limits before any camera or worker state is modified. */
|
|
179
|
+
function normalizeTileResources(value: unknown): HostTileResourceOptions | undefined {
|
|
180
|
+
if (value === undefined) return undefined;
|
|
181
|
+
const record = requireRecord(value, 'HostFrame tileResources');
|
|
182
|
+
const result: HostTileResourceOptions = {};
|
|
183
|
+
for (const name of ['maxConcurrentBuilds', 'maxCachedTiles', 'maxCachedMeshBytes'] as const) {
|
|
184
|
+
const limit = record[name];
|
|
185
|
+
if (limit === undefined) continue;
|
|
186
|
+
if (typeof limit !== 'number' || !Number.isSafeInteger(limit) || limit < (name === 'maxConcurrentBuilds' ? 1 : 0)) {
|
|
187
|
+
throw new Error(`HostFrame tileResources ${name} must be a ${name === 'maxConcurrentBuilds' ? 'positive' : 'non-negative'} safe integer`);
|
|
188
|
+
}
|
|
189
|
+
result[name] = limit;
|
|
190
|
+
}
|
|
191
|
+
return Object.freeze(result);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Bound global residency before any tile enumeration or scene mutation. */
|
|
195
|
+
function normalizeGlobePreloadZoom(value: unknown): number | undefined {
|
|
196
|
+
if (value === undefined) return undefined;
|
|
197
|
+
if (typeof value !== 'number' || !Number.isInteger(value) || value < 0 || value > 3) {
|
|
198
|
+
throw new Error('HostFrame globePreloadZoom must be an integer from 0 to 3');
|
|
199
|
+
}
|
|
200
|
+
return value;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Copy and validate all policy inputs before camera or scene state is touched. */
|
|
204
|
+
function normalizeTileLOD(value: unknown): Required<HostTileLODOptions> | undefined {
|
|
205
|
+
if (value === undefined) return undefined;
|
|
206
|
+
const record = requireRecord(value, 'HostFrame tileLOD');
|
|
207
|
+
const targetTilePixels = record.targetTilePixels ?? 512;
|
|
208
|
+
const pixelRatio = record.pixelRatio ?? 1;
|
|
209
|
+
const maxTiles = record.maxTiles ?? 256;
|
|
210
|
+
const hysteresis = record.hysteresis ?? 0.2;
|
|
211
|
+
if (!isFiniteNumber(targetTilePixels) || targetTilePixels <= 0 ||
|
|
212
|
+
!isFiniteNumber(pixelRatio) || pixelRatio <= 0 ||
|
|
213
|
+
!isFiniteNumber(maxTiles) || !Number.isSafeInteger(maxTiles) || maxTiles < 1 ||
|
|
214
|
+
!isFiniteNumber(hysteresis) || hysteresis < 0 || hysteresis >= 1) {
|
|
215
|
+
throw new Error('HostFrame tileLOD requires positive pixel scales, a positive integer maxTiles, and hysteresis in [0, 1)');
|
|
216
|
+
}
|
|
217
|
+
return {targetTilePixels, pixelRatio, maxTiles, hysteresis};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Validate coarsening without introducing unsupported geometry underzoom. */
|
|
221
|
+
function normalizeTileZoom(value: unknown, styleZoom: number): number | undefined {
|
|
222
|
+
if (value === undefined) {
|
|
223
|
+
return undefined;
|
|
224
|
+
}
|
|
225
|
+
if (typeof value !== 'number' || !Number.isInteger(value) || value < 0 || value > 22 ||
|
|
226
|
+
value > Math.floor(styleZoom)) {
|
|
227
|
+
throw new Error('HostFrame tileZoom must be an integer from 0 to 22, no higher than the shared style zoom');
|
|
228
|
+
}
|
|
229
|
+
return value;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function requireRecord(value: unknown, label: string): Record<string, unknown> {
|
|
233
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
234
|
+
throw new Error(`${label} requires an object`);
|
|
235
|
+
}
|
|
236
|
+
return Object.fromEntries(Object.entries(value));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function normalizeViewport(value: unknown, label: string): Required<Viewport> {
|
|
240
|
+
const record = value && typeof value === 'object' ? requireRecord(value, label) : {};
|
|
241
|
+
if (!isFiniteNumber(record.width) || record.width <= 0 ||
|
|
242
|
+
!isFiniteNumber(record.height) || record.height <= 0) {
|
|
243
|
+
throw new Error(`${label} requires positive width and height`);
|
|
244
|
+
}
|
|
245
|
+
return {x: finiteOr(record.x, 0), y: finiteOr(record.y, 0), width: record.width, height: record.height};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function normalizeAnchor(value: unknown): Required<GeographicAnchor> {
|
|
249
|
+
const record = value && typeof value === 'object' ? requireRecord(value, 'HostFrame geographic anchor') : {};
|
|
250
|
+
if (!isFiniteNumber(record.longitude) || !isFiniteNumber(record.latitude) || !isFiniteNumber(record.zoom)) {
|
|
251
|
+
throw new Error('HostFrame geographic anchor requires finite longitude, latitude, and zoom');
|
|
252
|
+
}
|
|
253
|
+
return {longitude: record.longitude, latitude: record.latitude, zoom: record.zoom, altitude: finiteOr(record.altitude, 0)};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function normalizeProjection(value: unknown): HostProjection {
|
|
257
|
+
const record = value === undefined ? {} : requireRecord(value, 'HostFrame projection');
|
|
258
|
+
const type = value === undefined ? 'web-mercator' : record.type;
|
|
259
|
+
if (type === 'web-mercator' || type === 'projected') {
|
|
260
|
+
if (type === 'projected' && (record.visibleBounds === undefined || record.visibleBounds === null)) {
|
|
261
|
+
throw new Error('HostFrame CPU projection requires an explicit geographic footprint');
|
|
262
|
+
}
|
|
263
|
+
if (type === 'web-mercator' && record.visibleBounds === undefined) return {type};
|
|
264
|
+
if (type === 'web-mercator' && record.visibleBounds === null) return {type, visibleBounds: null};
|
|
265
|
+
const bounds = record.visibleBounds;
|
|
266
|
+
if (!Array.isArray(bounds) || bounds.length !== 4 || !bounds.every(isFiniteNumber) ||
|
|
267
|
+
bounds[0] > bounds[2] || bounds[1] > bounds[3] || bounds[1] <= -90 || bounds[3] >= 90) {
|
|
268
|
+
throw new Error('HostFrame planar projection requires finite ordered visibleBounds with latitudes strictly between -90 and 90');
|
|
269
|
+
}
|
|
270
|
+
if (type === 'projected' && (bounds[0] < -180 || bounds[2] > 180 || bounds[1] < -85.0511287798066 || bounds[3] > 85.0511287798066)) {
|
|
271
|
+
throw new Error('HostFrame CPU projection footprint must stay inside the finite single-world tile domain');
|
|
272
|
+
}
|
|
273
|
+
return {type, visibleBounds: [bounds[0], bounds[1], bounds[2], bounds[3]]};
|
|
274
|
+
}
|
|
275
|
+
if (type !== 'globe') {
|
|
276
|
+
throw new Error(`HostFrame projection type '${String(type)}' is invalid`);
|
|
277
|
+
}
|
|
278
|
+
const bounds = record.visibleBounds;
|
|
279
|
+
if (!Array.isArray(bounds) || bounds.length !== 4 || !bounds.every(isFiniteNumber)) {
|
|
280
|
+
throw new Error('HostFrame globe projection requires finite visibleBounds');
|
|
281
|
+
}
|
|
282
|
+
return {
|
|
283
|
+
type, visibleBounds: [bounds[0], bounds[1], bounds[2], bounds[3]],
|
|
284
|
+
...(record.maxElevation === undefined ? {} : {
|
|
285
|
+
maxElevation: normalizeNonNegative(record.maxElevation, 'maxElevation')
|
|
286
|
+
})
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function normalizeCamera(value: unknown, id: string): HostCamera {
|
|
291
|
+
const record = value && typeof value === 'object' ? requireRecord(value, 'HostFrame camera') : {};
|
|
292
|
+
const view = readFiniteArray(record.view, 16);
|
|
293
|
+
const projection = readFiniteArray(record.projection, 16);
|
|
294
|
+
const position = readFiniteArray(record.position, 3);
|
|
295
|
+
if (!view || !projection || !position) {
|
|
296
|
+
throw new Error(`HostFrame render view '${id}' requires finite camera matrices and position`);
|
|
297
|
+
}
|
|
298
|
+
const projectionMatrix = new Float32Array(projection);
|
|
299
|
+
if (!projectionMatrix.every(Number.isFinite)) {
|
|
300
|
+
throw new Error(`HostFrame render view '${id}' requires finite camera matrices and position`);
|
|
301
|
+
}
|
|
302
|
+
return {view: new Float64Array(view), projection: projectionMatrix, position: [position[0], position[1], position[2]]};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function readFiniteArray(value: unknown, length: number): number[] | null {
|
|
306
|
+
if (!Array.isArray(value) && !(value instanceof Float32Array) && !(value instanceof Float64Array)) {
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
const values: unknown[] = Array.from(value);
|
|
310
|
+
return values.length === length && values.every(isFiniteNumber) ? values : null;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function isFiniteNumber(value: unknown): value is number {
|
|
314
|
+
return typeof value === 'number' && Number.isFinite(value);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function finiteOr(value: unknown, fallback: number): number {
|
|
318
|
+
return isFiniteNumber(value) ? value : fallback;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function normalizeNonNegative(value: unknown, label: string): number {
|
|
322
|
+
if (!isFiniteNumber(value) || value < 0) {
|
|
323
|
+
throw new Error(`HostFrame ${label} must be a finite non-negative number`);
|
|
324
|
+
}
|
|
325
|
+
return value;
|
|
326
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// tangram-layers
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import {Matrix4} from '@math.gl/core';
|
|
6
|
+
import Geo, {type Bounds} from '../utils/geo';
|
|
7
|
+
import type {HostCamera, HostProjection, Viewport} from '../types';
|
|
8
|
+
import type HostFrame from './host_frame';
|
|
9
|
+
import {getProjectionSurface} from './projection_math';
|
|
10
|
+
|
|
11
|
+
/** One eye's surface footprint and projection, independent of the embedding library. */
|
|
12
|
+
export interface ProjectedLODEye {
|
|
13
|
+
/** Matrices in the renderer's planar/globe host convention. */
|
|
14
|
+
readonly camera: HostCamera;
|
|
15
|
+
/** Geographic projection of the surface. */
|
|
16
|
+
readonly projection: HostProjection;
|
|
17
|
+
/** Eye viewport in CSS pixels. */
|
|
18
|
+
readonly viewport: Required<Viewport>;
|
|
19
|
+
/** Unwrapped Mercator-meter footprint, or null when no surface is visible. */
|
|
20
|
+
readonly bounds: Bounds | null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Stateful, opt-in uniform surface-scale LOD; never changes scene/style zoom. */
|
|
24
|
+
export default class ProjectedTileLOD {
|
|
25
|
+
private previousZoom?: number;
|
|
26
|
+
private policyKey = '';
|
|
27
|
+
|
|
28
|
+
/** Selects one level for every eye before allocating candidates or installing a frame. */
|
|
29
|
+
select(frame: HostFrame, eyes: readonly ProjectedLODEye[], countCandidates: (zoom: number) => number): number | undefined {
|
|
30
|
+
const options = frame.tileLOD;
|
|
31
|
+
if (!options) {
|
|
32
|
+
this.previousZoom = undefined;
|
|
33
|
+
this.policyKey = '';
|
|
34
|
+
return frame.tileZoom;
|
|
35
|
+
}
|
|
36
|
+
const key = JSON.stringify([options, frame.projection.type]);
|
|
37
|
+
const maximumZoom = Math.min(22, Math.max(0, Math.floor(frame.geographicAnchor.zoom)));
|
|
38
|
+
let scale = 0;
|
|
39
|
+
for (const eye of eyes) {
|
|
40
|
+
scale = Math.max(scale, getProjectedScale(eye, options.pixelRatio));
|
|
41
|
+
}
|
|
42
|
+
// Missing visible samples are not evidence that coarse data is sufficient.
|
|
43
|
+
const desiredZoom = scale > 0
|
|
44
|
+
? Math.log2(Geo.circumference_meters * scale / options.targetTilePixels)
|
|
45
|
+
: maximumZoom;
|
|
46
|
+
let zoom = Math.min(maximumZoom, Math.max(0, Math.ceil(desiredZoom)));
|
|
47
|
+
const previous = key === this.policyKey ? this.previousZoom : undefined;
|
|
48
|
+
if (previous !== undefined && previous <= maximumZoom &&
|
|
49
|
+
desiredZoom >= previous - 1 - options.hysteresis && desiredZoom <= previous + options.hysteresis) {
|
|
50
|
+
zoom = previous;
|
|
51
|
+
}
|
|
52
|
+
for (;;) {
|
|
53
|
+
const count = countCandidates(zoom);
|
|
54
|
+
if (!Number.isFinite(count) || count < 0) {
|
|
55
|
+
throw new Error('Automatic tile LOD requires a finite non-negative candidate count');
|
|
56
|
+
}
|
|
57
|
+
if (count <= options.maxTiles) break;
|
|
58
|
+
if (zoom === 0) {
|
|
59
|
+
throw new Error('HostFrame tileLOD maxTiles cannot fit the buffered eye footprints, even at zoom 0');
|
|
60
|
+
}
|
|
61
|
+
zoom--;
|
|
62
|
+
}
|
|
63
|
+
// Failed budget checks must not affect the next successful frame's hysteresis.
|
|
64
|
+
this.previousZoom = zoom;
|
|
65
|
+
this.policyKey = key;
|
|
66
|
+
return zoom;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Estimates the strongest local surface magnification at a deterministic 3x3 footprint grid. */
|
|
71
|
+
function getProjectedScale(eye: ProjectedLODEye, pixelRatio: number): number {
|
|
72
|
+
if (!eye.bounds) return 0;
|
|
73
|
+
const matrix = new Matrix4().copy(eye.camera.projection);
|
|
74
|
+
if (eye.projection.type === 'web-mercator') matrix.multiplyRight(eye.camera.view);
|
|
75
|
+
const {sw, ne} = eye.bounds;
|
|
76
|
+
let scale = 0;
|
|
77
|
+
for (const horizontal of [0, 0.5, 1]) {
|
|
78
|
+
for (const vertical of [0, 0.5, 1]) {
|
|
79
|
+
const x = sw.x + (ne.x - sw.x) * horizontal;
|
|
80
|
+
const y = sw.y + (ne.y - sw.y) * vertical;
|
|
81
|
+
const surface = getSurface(x, y, eye.projection);
|
|
82
|
+
const clip = transform(matrix, surface.position, 1);
|
|
83
|
+
if (!clip.every(Number.isFinite) || clip[3] <= 0 ||
|
|
84
|
+
Math.abs(clip[0]) > clip[3] * 1.001 || Math.abs(clip[1]) > clip[3] * 1.001 ||
|
|
85
|
+
Math.abs(clip[2]) > clip[3] * 1.001) continue;
|
|
86
|
+
const derivativeX = transform(matrix, surface.derivativeX, 0);
|
|
87
|
+
const derivativeY = transform(matrix, surface.derivativeY, 0);
|
|
88
|
+
const width = eye.viewport.width * pixelRatio / 2;
|
|
89
|
+
const height = eye.viewport.height * pixelRatio / 2;
|
|
90
|
+
const horizontalX = projectedDerivative(clip, derivativeX, 0) * width;
|
|
91
|
+
const horizontalY = projectedDerivative(clip, derivativeY, 0) * width;
|
|
92
|
+
const verticalX = projectedDerivative(clip, derivativeX, 1) * height;
|
|
93
|
+
const verticalY = projectedDerivative(clip, derivativeY, 1) * height;
|
|
94
|
+
const diagonalX = horizontalX ** 2 + verticalX ** 2;
|
|
95
|
+
const diagonalY = horizontalY ** 2 + verticalY ** 2;
|
|
96
|
+
const offDiagonal = horizontalX * horizontalY + verticalX * verticalY;
|
|
97
|
+
const largestEigenvalue = (diagonalX + diagonalY +
|
|
98
|
+
Math.hypot(diagonalX - diagonalY, 2 * offDiagonal)) / 2;
|
|
99
|
+
const candidate = Math.sqrt(largestEigenvalue);
|
|
100
|
+
if (Number.isFinite(candidate)) scale = Math.max(scale, candidate);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return scale;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Differential of normalized-device coordinates, including perspective division. */
|
|
107
|
+
function projectedDerivative(clip: number[], derivative: number[], axis: number): number {
|
|
108
|
+
return (derivative[axis] * clip[3] - clip[axis] * derivative[3]) / (clip[3] ** 2);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Column-major homogeneous transform; derivatives use w=0, positions use w=1. */
|
|
112
|
+
function transform(matrix: ArrayLike<number>, vector: number[], homogeneous: number): number[] {
|
|
113
|
+
return [0, 1, 2, 3].map(row => matrix[row] * vector[0] + matrix[row + 4] * vector[1] +
|
|
114
|
+
matrix[row + 8] * vector[2] + matrix[row + 12] * homogeneous);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Surface position and derivatives per EPSG:3857 meter, matching the globe shaders. */
|
|
118
|
+
function getSurface(x: number, y: number, projection: HostProjection): {
|
|
119
|
+
position: number[]; derivativeX: number[]; derivativeY: number[];
|
|
120
|
+
} {
|
|
121
|
+
if (projection.type === 'web-mercator') {
|
|
122
|
+
return {position: [x, y, 0], derivativeX: [1, 0, 0], derivativeY: [0, 1, 0]};
|
|
123
|
+
}
|
|
124
|
+
return getProjectionSurface(x, y, projection.type);
|
|
125
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// tangram-layers
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import type {HostProjection} from '../types';
|
|
6
|
+
import {unprojectMetersToLngLatLegacy} from '../procedures/web-mercator-legacy';
|
|
7
|
+
|
|
8
|
+
/** Neutral units shared by CPU geometry, lighting, shader generation and host adapters. */
|
|
9
|
+
export const PROJECTION_CONSTANTS = Object.freeze({
|
|
10
|
+
/** EPSG:3857 spherical radius in meters; not the geographic altitude radius. */
|
|
11
|
+
mercatorRadius: 6378137,
|
|
12
|
+
/** Geographic altitude scale matching deck.gl's public GlobeViewport. */
|
|
13
|
+
earthRadius: 6370972,
|
|
14
|
+
/** Globe common-space radius; north is +Z and longitude zero is -Y. */
|
|
15
|
+
globeRadius: 256,
|
|
16
|
+
/** Tangram's reference tile size, distinct from deck's 512-unit world. */
|
|
17
|
+
tileSize: 256,
|
|
18
|
+
/** Mercator's finite square-world latitude cutoff in degrees. */
|
|
19
|
+
maxMercatorLatitude: 85.0511287798066
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
/** Longitude/latitude in degrees and geographic altitude in meters. */
|
|
23
|
+
export type GeographicProjectionPosition = readonly [number, number, number];
|
|
24
|
+
|
|
25
|
+
/** Renderer-owned geographic boundary; cameras and tile-row conversion are separate. */
|
|
26
|
+
export interface GeographicProjectionProcedure {
|
|
27
|
+
/** Existing HostFrame projection discriminator; this is not a CRS identifier. */
|
|
28
|
+
readonly type: HostProjection['type'];
|
|
29
|
+
/** Absolute position units before the host's view/placement matrices. */
|
|
30
|
+
readonly positionUnits: 'meters' | 'globe-common-units';
|
|
31
|
+
/** Maximum represented latitude in degrees, after Mercator clamping. */
|
|
32
|
+
readonly latitudeLimit: number;
|
|
33
|
+
/** Whether longitude selects a nearest anchored world copy or repeats around a sphere. */
|
|
34
|
+
readonly longitudePolicy: 'nearest-anchor-or-unwrapped' | 'periodic';
|
|
35
|
+
/** Project finite degree/degree/meter positions; an anchor affects only Mercator longitude. */
|
|
36
|
+
readonly project: (position: GeographicProjectionPosition, anchorLongitude?: number) => [number, number, number];
|
|
37
|
+
/** Invert an absolute position; globe center is undefined and longitude becomes canonical. */
|
|
38
|
+
readonly unproject: (position: readonly [number, number, number]) => [number, number, number];
|
|
39
|
+
/** Rotate an ENU direction without converting its length to common-space units. */
|
|
40
|
+
readonly projectVector: (position: GeographicProjectionPosition, direction: readonly [number, number, number]) => [number, number, number];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Project geography to absolute Mercator meters or globe common coordinates, never eye space. */
|
|
44
|
+
export function projectGeographicPosition(position: GeographicProjectionPosition, projection: HostProjection['type'], anchorLongitude?: number): [number, number, number] {
|
|
45
|
+
let [longitude, latitude, altitude] = position;
|
|
46
|
+
if (projection === 'projected') throw new Error('CPU-projected basemaps require the experimental worker procedure');
|
|
47
|
+
if (!position.every(Number.isFinite) || Math.abs(latitude) > 90) throw new Error('Projection requires finite geographic coordinates and latitude within +/-90 degrees');
|
|
48
|
+
if (projection === 'globe') {
|
|
49
|
+
longitude *= Math.PI / 180; latitude *= Math.PI / 180;
|
|
50
|
+
const radius = PROJECTION_CONSTANTS.globeRadius * (1 + altitude / PROJECTION_CONSTANTS.earthRadius);
|
|
51
|
+
return [Math.sin(longitude) * Math.cos(latitude) * radius,
|
|
52
|
+
-Math.cos(longitude) * Math.cos(latitude) * radius, Math.sin(latitude) * radius];
|
|
53
|
+
}
|
|
54
|
+
if (anchorLongitude != null && Number.isFinite(anchorLongitude)) longitude += Math.round((anchorLongitude - longitude) / 360) * 360;
|
|
55
|
+
latitude = Math.max(-PROJECTION_CONSTANTS.maxMercatorLatitude, Math.min(PROJECTION_CONSTANTS.maxMercatorLatitude, latitude));
|
|
56
|
+
return [PROJECTION_CONSTANTS.mercatorRadius * longitude * Math.PI / 180,
|
|
57
|
+
PROJECTION_CONSTANTS.mercatorRadius * Math.log(Math.tan(Math.PI / 4 + latitude * Math.PI / 360)), altitude];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Rotate an ENU direction into globe axes without camera transforms or changing its length. */
|
|
61
|
+
export function projectGeographicVector(position: GeographicProjectionPosition, direction: readonly [number, number, number], projection: HostProjection['type']): [number, number, number] {
|
|
62
|
+
if (projection === 'projected') throw new Error('CPU-projected basemaps do not support geographic lighting');
|
|
63
|
+
const [east, north, up] = direction;
|
|
64
|
+
if (projection === 'web-mercator') return [east, north, up];
|
|
65
|
+
const longitude = position[0] * Math.PI / 180, latitude = position[1] * Math.PI / 180;
|
|
66
|
+
return [east * Math.cos(longitude) - north * Math.sin(longitude) * Math.sin(latitude) + up * Math.sin(longitude) * Math.cos(latitude),
|
|
67
|
+
east * Math.sin(longitude) + north * Math.cos(longitude) * Math.sin(latitude) - up * Math.cos(longitude) * Math.cos(latitude),
|
|
68
|
+
north * Math.cos(latitude) + up * Math.sin(latitude)];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Invert globe common coordinates for spatial picking, rejecting the undefined sphere center. */
|
|
72
|
+
export function unprojectGlobePosition(position: readonly [number, number, number]): [number, number, number] {
|
|
73
|
+
const radius = Math.hypot(...position);
|
|
74
|
+
if (!position.every(Number.isFinite) || radius === 0) throw new Error('Globe position must be finite and non-zero');
|
|
75
|
+
return [Math.atan2(position[0], -position[1]) * 180 / Math.PI,
|
|
76
|
+
Math.asin(Math.max(-1, Math.min(1, position[2] / radius))) * 180 / Math.PI,
|
|
77
|
+
(radius / PROJECTION_CONSTANTS.globeRadius - 1) * PROJECTION_CONSTANTS.earthRadius];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Style pixels in the zoom-zero Mercator convention, not perspective screen-pixel error. */
|
|
81
|
+
export function getMercatorMetersPerPixel(zoom: number): number {
|
|
82
|
+
return 2 * Math.PI * PROJECTION_CONSTANTS.mercatorRadius / PROJECTION_CONSTANTS.tileSize / Math.pow(2, zoom);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Surface differential per EPSG:3857 meter for projected LOD; elevation is intentionally zero. */
|
|
86
|
+
export function getProjectionSurface(x: number, y: number, projection: HostProjection['type']): {
|
|
87
|
+
/** Common-space ground position. */
|
|
88
|
+
position: number[];
|
|
89
|
+
/** Change in position per eastward Mercator meter. */
|
|
90
|
+
derivativeX: number[];
|
|
91
|
+
/** Change in position per northward Mercator meter. */
|
|
92
|
+
derivativeY: number[];
|
|
93
|
+
} {
|
|
94
|
+
if (projection === 'projected') throw new Error('CPU-projected basemaps require explicit LOD');
|
|
95
|
+
if (projection === 'web-mercator') return {position: [x, y, 0], derivativeX: [1, 0, 0], derivativeY: [0, 1, 0]};
|
|
96
|
+
const longitude = x / PROJECTION_CONSTANTS.mercatorRadius;
|
|
97
|
+
const latitude = Math.atan(Math.sinh(y / PROJECTION_CONSTANTS.mercatorRadius));
|
|
98
|
+
const cosine = Math.cos(latitude), sine = Math.sin(latitude);
|
|
99
|
+
const sineLongitude = Math.sin(longitude), cosineLongitude = Math.cos(longitude);
|
|
100
|
+
const radius = PROJECTION_CONSTANTS.globeRadius, derivativeScale = radius / PROJECTION_CONSTANTS.mercatorRadius;
|
|
101
|
+
return {position: [radius * sineLongitude * cosine, -radius * cosineLongitude * cosine, radius * sine],
|
|
102
|
+
derivativeX: [derivativeScale * cosineLongitude * cosine, derivativeScale * sineLongitude * cosine, 0],
|
|
103
|
+
derivativeY: [-derivativeScale * sineLongitude * sine * cosine,
|
|
104
|
+
derivativeScale * cosineLongitude * sine * cosine, derivativeScale * cosine ** 2]};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Invert finite absolute Mercator meters without wrapping longitude or discarding altitude. */
|
|
108
|
+
function unprojectMercatorPosition(position: readonly [number, number, number]): [number, number, number] {
|
|
109
|
+
if (!position.every(Number.isFinite)) throw new Error('Mercator position must be finite');
|
|
110
|
+
const [longitude, latitude] = unprojectMetersToLngLatLegacy([position[0], position[1]]);
|
|
111
|
+
return [longitude, latitude, position[2]];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Immutable procedures delegate to existing formulas; no runtime math.gl candidate switch. */
|
|
115
|
+
const PROJECTION_PROCEDURES: Readonly<Record<'web-mercator' | 'globe', GeographicProjectionProcedure>> = Object.freeze({
|
|
116
|
+
'web-mercator': Object.freeze({
|
|
117
|
+
type: 'web-mercator',
|
|
118
|
+
positionUnits: 'meters',
|
|
119
|
+
latitudeLimit: PROJECTION_CONSTANTS.maxMercatorLatitude,
|
|
120
|
+
longitudePolicy: 'nearest-anchor-or-unwrapped',
|
|
121
|
+
project: (position: GeographicProjectionPosition, anchorLongitude?: number) => projectGeographicPosition(position, 'web-mercator', anchorLongitude),
|
|
122
|
+
unproject: unprojectMercatorPosition,
|
|
123
|
+
projectVector: (position: GeographicProjectionPosition, direction: readonly [number, number, number]) => projectGeographicVector(position, direction, 'web-mercator')
|
|
124
|
+
}),
|
|
125
|
+
globe: Object.freeze({
|
|
126
|
+
type: 'globe',
|
|
127
|
+
positionUnits: 'globe-common-units',
|
|
128
|
+
latitudeLimit: 90,
|
|
129
|
+
longitudePolicy: 'periodic',
|
|
130
|
+
project: (position: GeographicProjectionPosition) => projectGeographicPosition(position, 'globe'),
|
|
131
|
+
unproject: unprojectGlobePosition,
|
|
132
|
+
projectVector: (position: GeographicProjectionPosition, direction: readonly [number, number, number]) => projectGeographicVector(position, direction, 'globe')
|
|
133
|
+
})
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
/** Select the existing renderer's CPU procedure and conventions for a supported HostFrame projection. */
|
|
137
|
+
export function getGeographicProjectionProcedure(projection: HostProjection['type']): GeographicProjectionProcedure {
|
|
138
|
+
if (projection !== 'web-mercator' && projection !== 'globe') {
|
|
139
|
+
throw new Error('Unsupported geographic projection; expected web-mercator or globe');
|
|
140
|
+
}
|
|
141
|
+
return PROJECTION_PROCEDURES[projection];
|
|
142
|
+
}
|