@tangram.gl/tangram-layers 0.0.0-stage → 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +36 -2
  2. package/dist/experimental/projected-basemaps.js +1310 -0
  3. package/dist/experimental/projected-basemaps.js.map +6 -0
  4. package/dist/experimental/terrain.js +379 -0
  5. package/dist/experimental/terrain.js.map +6 -0
  6. package/dist/experimental/webxr.js +1423 -0
  7. package/dist/experimental/webxr.js.map +6 -0
  8. package/dist/index.js +756 -0
  9. package/dist/index.js.map +6 -0
  10. package/dist/types/experimental/projected-basemaps.d.ts +120 -0
  11. package/dist/types/experimental/projected-detail.d.ts +33 -0
  12. package/dist/types/experimental/projected-navigation.d.ts +117 -0
  13. package/dist/types/experimental/terrain-display.d.ts +124 -0
  14. package/dist/types/experimental/terrain.d.ts +131 -0
  15. package/dist/types/first_person_view_adapter.d.ts +29 -0
  16. package/dist/types/globe_view_adapter.d.ts +33 -0
  17. package/dist/types/index.d.ts +84 -0
  18. package/dist/types/tangram-layer-lifecycle.d.ts +132 -0
  19. package/dist/types/tangram-layer.d.ts +157 -0
  20. package/dist/types/view_adapter_types.d.ts +42 -0
  21. package/dist/types/web_mercator_view_adapter.d.ts +77 -0
  22. package/package.json +80 -3
  23. package/src/experimental/projected-basemaps.ts +301 -0
  24. package/src/experimental/projected-detail.ts +116 -0
  25. package/src/experimental/projected-navigation.ts +323 -0
  26. package/src/experimental/terrain-display.ts +243 -0
  27. package/src/experimental/terrain.ts +240 -0
  28. package/src/experimental/webxr/grabbing.ts +166 -0
  29. package/src/experimental/webxr/index.d.ts +77 -0
  30. package/src/experimental/webxr/index.ts +72 -0
  31. package/src/experimental/webxr/interaction.ts +221 -0
  32. package/src/experimental/webxr/picking.ts +119 -0
  33. package/src/experimental/webxr/presentation.ts +457 -0
  34. package/src/experimental/webxr/projection.ts +371 -0
  35. package/src/experimental/webxr/types.ts +221 -0
  36. package/src/experimental/webxr/views.ts +190 -0
  37. package/src/first_person_view_adapter.ts +107 -0
  38. package/src/globe_view_adapter.ts +103 -0
  39. package/src/index.ts +64 -0
  40. package/src/tangram-layer-lifecycle.ts +130 -0
  41. package/src/tangram-layer.ts +679 -0
  42. package/src/view_adapter_types.ts +40 -0
  43. package/src/web_mercator_view_adapter.ts +185 -0
@@ -0,0 +1,371 @@
1
+ // tangram-layers
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ import {Matrix4} from '@math.gl/core';
6
+ import {PlaneShape, SphereShape} from '@math.gl/culling';
7
+ import {PROJECTION_CONSTANTS, unprojectGlobePosition} from '@tangram.gl/tangram-renderer/core';
8
+ import type {
9
+ XRGeographicPosition,
10
+ XRGlobePlacement,
11
+ XRMapPlacement,
12
+ XRPlacement,
13
+ XRPlacementPose,
14
+ XRSpatialRay,
15
+ XRSurfaceHit,
16
+ XRVector3
17
+ } from './types.ts';
18
+
19
+ const TANGRAM_HALF_WORLD_METERS = 20037508.342789244;
20
+ const GLOBE_RADIUS = PROJECTION_CONSTANTS.globeRadius;
21
+ // Content-space queries need no shape transform: placement is inverted before picking.
22
+ const MAP_SURFACE = new PlaneShape();
23
+ const GLOBE_SURFACE = new SphereShape({radius: GLOBE_RADIUS});
24
+ const MAX_MERCATOR_LATITUDE = 85.05112878;
25
+ const DEGREES_TO_RADIANS = Math.PI / 180;
26
+ const RADIANS_TO_DEGREES = 180 / Math.PI;
27
+
28
+ /** Convert longitude and latitude to Tangram EPSG:3857 meters. */
29
+ export function longitudeLatitudeToMeters(longitude: number, latitude: number): [number, number] {
30
+ const clampedLatitude = Math.max(
31
+ -MAX_MERCATOR_LATITUDE,
32
+ Math.min(MAX_MERCATOR_LATITUDE, latitude)
33
+ );
34
+ return [
35
+ (longitude / 180) * TANGRAM_HALF_WORLD_METERS,
36
+ (Math.log(Math.tan(((90 + clampedLatitude) * Math.PI) / 360)) / Math.PI) *
37
+ TANGRAM_HALF_WORLD_METERS
38
+ ];
39
+ }
40
+
41
+ /** Convert a Tangram EPSG:3857 position to longitude, latitude, and altitude. */
42
+ export function metersToLongitudeLatitude(
43
+ x: number,
44
+ y: number,
45
+ altitude = 0
46
+ ): [number, number, number] {
47
+ return [
48
+ (x / TANGRAM_HALF_WORLD_METERS) * 180,
49
+ (Math.atan(Math.exp((y / TANGRAM_HALF_WORLD_METERS) * Math.PI)) * 360) / Math.PI - 90,
50
+ altitude
51
+ ];
52
+ }
53
+
54
+ /** Create an affine matrix from an XR-space pose. */
55
+ export function createXRPoseMatrix(pose: XRPlacementPose = {}): Matrix4 {
56
+ const position = pose.position || [0, 0, 0];
57
+ const orientation = pose.orientation || [0, 0, 0, 1];
58
+ return new Matrix4()
59
+ .translate(position)
60
+ .multiplyRight(new Matrix4().fromQuaternion(orientation));
61
+ }
62
+
63
+ /** Create the world-to-reference-space placement matrix for geospatial content. */
64
+ export function createXRPlacementMatrix(
65
+ placement: XRPlacement,
66
+ viewState: Record<string, unknown> = {}
67
+ ): Matrix4 {
68
+ if (placement.type === 'globe') {
69
+ const longitude = finiteNumber(viewState.longitude, placement.anchor[0]);
70
+ const latitude = finiteNumber(viewState.latitude, placement.anchor[1]);
71
+ const rotation = placement.rotation || 0;
72
+ const scale = placement.radius / GLOBE_RADIUS;
73
+ return createXRPoseMatrix(placement.pose)
74
+ // deck.gl globe coordinates have north on +Z and longitude zero on -Y.
75
+ // Face the anchor toward +Z in XR room space, with north toward +Y.
76
+ .rotateX(latitude * DEGREES_TO_RADIANS - Math.PI / 2)
77
+ .rotateZ(-(longitude + rotation) * DEGREES_TO_RADIANS)
78
+ .scale([scale, scale, scale]);
79
+ }
80
+
81
+ const fallbackAnchor = placement.type === 'map' ? placement.anchor : placement.origin;
82
+ const longitude = finiteNumber(viewState.longitude, fallbackAnchor[0]);
83
+ const latitude = finiteNumber(viewState.latitude, fallbackAnchor[1]);
84
+ const [centerX, centerY] = longitudeLatitudeToMeters(longitude, latitude);
85
+ const metersPerMercatorMeter = Math.cos(
86
+ Math.max(-MAX_MERCATOR_LATITUDE, Math.min(MAX_MERCATOR_LATITUDE, latitude)) *
87
+ DEGREES_TO_RADIANS
88
+ );
89
+
90
+ if (placement.type === 'map') {
91
+ const scale = 1 / placement.metersPerXRUnit;
92
+ return createXRPoseMatrix(placement.pose)
93
+ .rotateX(-Math.PI / 2)
94
+ .scale([scale * metersPerMercatorMeter, scale * metersPerMercatorMeter, scale])
95
+ .translate([-centerX, -centerY, -(placement.anchor[2] || 0)]);
96
+ }
97
+
98
+ const offset = placement.position || [0, 0, 0];
99
+ const bearing = finiteNumber(viewState.bearing, placement.bearing || 0);
100
+ return createXRPoseMatrix(placement.pose)
101
+ .rotateY(-bearing * DEGREES_TO_RADIANS)
102
+ .rotateX(-Math.PI / 2)
103
+ .translate([-offset[0], -offset[1], -offset[2]])
104
+ .scale([metersPerMercatorMeter, metersPerMercatorMeter, 1])
105
+ .translate([-centerX, -centerY, 0]);
106
+ }
107
+
108
+ /** Transform an XR-space ray back into the content coordinate system. */
109
+ export function transformXRRayToContent(ray: XRSpatialRay, placementMatrix: readonly number[] | Float32Array | Float64Array) {
110
+ const inverse = new Matrix4().copy(placementMatrix).invert();
111
+ const origin = transformPoint(inverse, ray.origin);
112
+ const farPoint = transformPoint(inverse, [
113
+ ray.origin[0] + ray.direction[0],
114
+ ray.origin[1] + ray.direction[1],
115
+ ray.origin[2] + ray.direction[2]
116
+ ]);
117
+ return {
118
+ origin,
119
+ direction: normalizeVector([
120
+ farPoint[0] - origin[0],
121
+ farPoint[1] - origin[1],
122
+ farPoint[2] - origin[2]
123
+ ])
124
+ };
125
+ }
126
+
127
+ /** Intersect an XR-space ray with a placed Web Mercator map. */
128
+ export function intersectXRMap(
129
+ ray: XRSpatialRay,
130
+ placement: XRMapPlacement,
131
+ viewState: Record<string, unknown> = {}
132
+ ): XRGeographicPosition | null {
133
+ const placementMatrix = createXRPlacementMatrix(placement, viewState);
134
+ const localRay = transformXRRayToContent(ray, placementMatrix);
135
+ return intersectContentSurface(localRay, placement, viewState)?.coordinate || null;
136
+ }
137
+
138
+ /** Shared ground/sphere intersection in the renderer's projected content space. */
139
+ export function intersectContentSurface(
140
+ localRay: XRSpatialRay,
141
+ placement: XRPlacement,
142
+ viewState: Record<string, unknown> = {}
143
+ ): XRSurfaceHit | null {
144
+ if (!localRay.origin.every(Number.isFinite) || !localRay.direction.every(Number.isFinite) ||
145
+ Math.hypot(...localRay.direction) === 0) return null;
146
+ if (placement.type === 'globe') {
147
+ const distance = intersectSphereDistance(localRay.origin, localRay.direction);
148
+ if (distance === null) return null;
149
+ const point: XRVector3 = [
150
+ localRay.origin[0] + localRay.direction[0] * distance,
151
+ localRay.origin[1] + localRay.direction[1] * distance,
152
+ localRay.origin[2] + localRay.direction[2] * distance
153
+ ];
154
+ const coordinate = globePositionToLongitudeLatitude(point);
155
+ coordinate[2] = 0;
156
+ return {coordinate, position: point};
157
+ }
158
+ const distance = intersectPlaneDistance(localRay.origin, localRay.direction, 0);
159
+ if (distance === null) {
160
+ return null;
161
+ }
162
+ const hit: XRVector3 = [
163
+ localRay.origin[0] + localRay.direction[0] * distance,
164
+ localRay.origin[1] + localRay.direction[1] * distance,
165
+ 0
166
+ ];
167
+ if (!isContentPointWithinMapBounds(hit, placement, viewState)) return null;
168
+ return {coordinate: metersToLongitudeLatitude(hit[0], hit[1]), position: hit};
169
+ }
170
+
171
+ /** Apply physical tabletop XY bounds equally to flat and terrain surface hits. */
172
+ export function isContentPointWithinMapBounds(hit: XRVector3, placement: XRPlacement,
173
+ viewState: Record<string, unknown> = {}): boolean {
174
+ if (placement.type === 'map' && placement.surface?.type === 'bounded') {
175
+ const longitude = finiteNumber(viewState.longitude, placement.anchor[0]);
176
+ const latitude = finiteNumber(viewState.latitude, placement.anchor[1]);
177
+ const [centerX, centerY] = longitudeLatitudeToMeters(longitude, latitude);
178
+ const latitudeScale = Math.cos(
179
+ Math.max(-MAX_MERCATOR_LATITUDE, Math.min(MAX_MERCATOR_LATITUDE, latitude)) *
180
+ DEGREES_TO_RADIANS
181
+ );
182
+ const halfWidth = (placement.surface.width * placement.metersPerXRUnit) / (2 * latitudeScale);
183
+ const halfHeight = (placement.surface.height * placement.metersPerXRUnit) / (2 * latitudeScale);
184
+ if (Math.abs(hit[0] - centerX) > halfWidth || Math.abs(hit[1] - centerY) > halfHeight) {
185
+ return false;
186
+ }
187
+ }
188
+ return true;
189
+ }
190
+
191
+ /** Intersect an XR-space ray with a placed deck.gl-compatible globe. */
192
+ export function intersectXRGlobe(
193
+ ray: XRSpatialRay,
194
+ placement: XRGlobePlacement,
195
+ viewState: Record<string, unknown> = {}
196
+ ): XRGeographicPosition | null {
197
+ const placementMatrix = createXRPlacementMatrix(placement, viewState);
198
+ const localRay = transformXRRayToContent(ray, placementMatrix);
199
+ return intersectContentSurface(localRay, placement, viewState)?.coordinate || null;
200
+ }
201
+
202
+ /** Derive geographic bounds from the union of XR eye frusta intersecting a globe. */
203
+ export function getXRGlobeVisibleBounds({
204
+ views,
205
+ placement,
206
+ viewState
207
+ }: {
208
+ views: readonly {viewMatrix: readonly number[]; projectionMatrix: readonly number[]}[];
209
+ placement: XRGlobePlacement;
210
+ viewState?: Record<string, unknown>;
211
+ }): [number, number, number, number] {
212
+ const placementMatrix = createXRPlacementMatrix(placement, viewState);
213
+ const coordinates: XRGeographicPosition[] = [];
214
+ const samples = [-1, 0, 1];
215
+ let crossesHorizon = false;
216
+ for (const view of views) {
217
+ const inverseClip = new Matrix4(view.projectionMatrix)
218
+ .multiplyRight(view.viewMatrix)
219
+ .multiplyRight(placementMatrix)
220
+ .invert();
221
+ for (const x of samples) {
222
+ for (const y of samples) {
223
+ const near = transformHomogeneousPoint(inverseClip, [x, y, -1]);
224
+ const far = transformHomogeneousPoint(inverseClip, [x, y, 1]);
225
+ const direction = normalizeVector([
226
+ far[0] - near[0],
227
+ far[1] - near[1],
228
+ far[2] - near[2]
229
+ ]);
230
+ const distance = intersectSphereDistance(near, direction);
231
+ if (distance !== null) {
232
+ coordinates.push(
233
+ globePositionToLongitudeLatitude([
234
+ near[0] + direction[0] * distance,
235
+ near[1] + direction[1] * distance,
236
+ near[2] + direction[2] * distance
237
+ ])
238
+ );
239
+ }
240
+ else crossesHorizon = true;
241
+ }
242
+ }
243
+ }
244
+ // When rays miss a room-sized globe, the visible silhouette lies between
245
+ // samples. Hit-only extrema are not a conservative footprint: finer tiles
246
+ // can omit visible ground (including the geographic anchor) entirely.
247
+ if (crossesHorizon && coordinates.length > 0) {
248
+ return getXRGlobeHorizonBounds(views, placementMatrix,
249
+ finiteNumber(viewState?.longitude, placement.anchor[0]));
250
+ }
251
+ if (coordinates.length === 0) {
252
+ return [-180, -MAX_MERCATOR_LATITUDE, 180, MAX_MERCATOR_LATITUDE];
253
+ }
254
+ const anchorLongitude = finiteNumber(viewState?.longitude, placement.anchor[0]);
255
+ const unwrappedLongitudes = coordinates.map(([longitude]) =>
256
+ unwrapLongitude(longitude, anchorLongitude)
257
+ );
258
+ const latitudes = coordinates.map((coordinate) => coordinate[1]);
259
+ return [
260
+ Math.min(...unwrappedLongitudes),
261
+ Math.max(-MAX_MERCATOR_LATITUDE, Math.min(...latitudes)),
262
+ Math.max(...unwrappedLongitudes),
263
+ Math.min(MAX_MERCATOR_LATITUDE, Math.max(...latitudes))
264
+ ];
265
+ }
266
+
267
+ /** Bound each eye's analytically visible spherical cap, independent of screen sampling density. */
268
+ function getXRGlobeHorizonBounds(
269
+ views: readonly {viewMatrix: readonly number[]}[], placementMatrix: Matrix4, anchorLongitude: number
270
+ ): [number, number, number, number] {
271
+ let west = Infinity, south = Infinity, east = -Infinity, north = -Infinity;
272
+ for (const view of views) {
273
+ const camera = new Matrix4(view.viewMatrix).multiplyRight(placementMatrix).invert().transformAsPoint([0, 0, 0]);
274
+ const distance = Math.hypot(camera[0], camera[1], camera[2]);
275
+ if (!Number.isFinite(distance) || distance <= GLOBE_RADIUS) {
276
+ return [anchorLongitude - 180, -MAX_MERCATOR_LATITUDE, anchorLongitude + 180, MAX_MERCATOR_LATITUDE];
277
+ }
278
+ const [longitude, latitude] = globePositionToLongitudeLatitude([camera[0], camera[1], camera[2]]);
279
+ const radius = Math.acos(GLOBE_RADIUS / distance);
280
+ const latitudeRadians = latitude * DEGREES_TO_RADIANS;
281
+ const centerLongitude = unwrapLongitude(longitude, anchorLongitude);
282
+ const longitudeRadius = Math.abs(latitudeRadians) + radius >= Math.PI / 2 ? Math.PI :
283
+ Math.asin(Math.min(1, Math.sin(radius) / Math.cos(latitudeRadians)));
284
+ west = Math.min(west, longitudeRadius === Math.PI ? anchorLongitude - 180 : centerLongitude - longitudeRadius / DEGREES_TO_RADIANS);
285
+ east = Math.max(east, longitudeRadius === Math.PI ? anchorLongitude + 180 : centerLongitude + longitudeRadius / DEGREES_TO_RADIANS);
286
+ south = Math.min(south, latitude - radius / DEGREES_TO_RADIANS);
287
+ north = Math.max(north, latitude + radius / DEGREES_TO_RADIANS);
288
+ }
289
+ return [west, Math.max(-MAX_MERCATOR_LATITUDE, south), east, Math.min(MAX_MERCATOR_LATITUDE, north)];
290
+ }
291
+
292
+ /** Merge geographic bounds, preserving ranges that cross the antimeridian. */
293
+ export function unionGeographicBounds(
294
+ bounds: readonly (readonly [number, number, number, number])[],
295
+ anchorLongitude = 0
296
+ ): [number, number, number, number] {
297
+ if (bounds.length === 0) {
298
+ return [-180, -MAX_MERCATOR_LATITUDE, 180, MAX_MERCATOR_LATITUDE];
299
+ }
300
+ const west = bounds.map((value) => unwrapLongitude(value[0], anchorLongitude));
301
+ const east = bounds.map((value) => unwrapLongitude(value[2], anchorLongitude));
302
+ return [
303
+ Math.min(...west),
304
+ Math.min(...bounds.map((value) => value[1])),
305
+ Math.max(...east),
306
+ Math.max(...bounds.map((value) => value[3]))
307
+ ];
308
+ }
309
+
310
+ /** Adapt Tangram's +Z ground normal to math.gl's +Y analytic plane. */
311
+ function intersectPlaneDistance(
312
+ origin: XRVector3,
313
+ direction: XRVector3,
314
+ planeZ: number
315
+ ): number | null {
316
+ if (Math.abs(direction[2]) < 1e-8) {
317
+ return null;
318
+ }
319
+ return MAP_SURFACE.intersectLocalRay(
320
+ [origin[0], origin[2] - planeZ, origin[1]],
321
+ [direction[0], direction[2], direction[1]]
322
+ )?.distance ?? null;
323
+ }
324
+
325
+ /** Return the nearest nonnegative ray parameter, including an inside-sphere exit. */
326
+ function intersectSphereDistance(
327
+ origin: XRVector3,
328
+ direction: XRVector3
329
+ ): number | null {
330
+ return GLOBE_SURFACE.intersectLocalRay(origin, direction)?.distance ?? null;
331
+ }
332
+
333
+ function globePositionToLongitudeLatitude(point: XRVector3): [number, number, number] {
334
+ return unprojectGlobePosition(point);
335
+ }
336
+
337
+ function transformPoint(matrix: readonly number[], point: XRVector3): XRVector3 {
338
+ return transformHomogeneousPoint(matrix, point);
339
+ }
340
+
341
+ function transformHomogeneousPoint(matrix: readonly number[], point: XRVector3): XRVector3 {
342
+ const x = point[0];
343
+ const y = point[1];
344
+ const z = point[2];
345
+ const w = matrix[3] * x + matrix[7] * y + matrix[11] * z + matrix[15];
346
+ const divisor = w || 1;
347
+ return [
348
+ (matrix[0] * x + matrix[4] * y + matrix[8] * z + matrix[12]) / divisor,
349
+ (matrix[1] * x + matrix[5] * y + matrix[9] * z + matrix[13]) / divisor,
350
+ (matrix[2] * x + matrix[6] * y + matrix[10] * z + matrix[14]) / divisor
351
+ ];
352
+ }
353
+
354
+ function normalizeVector(vector: readonly number[]): XRVector3 {
355
+ const length = Math.hypot(vector[0], vector[1], vector[2]);
356
+ if (!length) {
357
+ return [0, 0, -1];
358
+ }
359
+ return [vector[0] / length, vector[1] / length, vector[2] / length];
360
+ }
361
+
362
+ function finiteNumber(value: unknown, fallback: number): number {
363
+ return Number.isFinite(value) ? (value as number) : fallback;
364
+ }
365
+
366
+ function unwrapLongitude(longitude: number, anchor: number): number {
367
+ let result = longitude;
368
+ while (result - anchor > 180) result -= 360;
369
+ while (result - anchor < -180) result += 360;
370
+ return result;
371
+ }
@@ -0,0 +1,221 @@
1
+ // tangram-layers
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ import type {HostFrameOptions, HostRenderView} from '@tangram.gl/tangram-renderer';
6
+ import type {TerrainSurface, TerrainSurfaceHit} from '@tangram.gl/tangram-renderer/core';
7
+ import type {Controller, FirstPersonViewState, MapViewState, View, Viewport} from '@deck.gl/core';
8
+ import type {Matrix4} from '@math.gl/core';
9
+
10
+ /** Shared geographic navigation state, independent of the rendered eye. */
11
+ export type XRViewState = Omit<MapViewState, 'zoom'> & FirstPersonViewState & {zoom?: number};
12
+
13
+ /** Canvas dimensions used to create a logical or per-eye viewport. */
14
+ export type XRViewportSize = {width: number; height: number};
15
+
16
+ /** Parameters used to derive an eye from the shared logical navigation state. */
17
+ export type XREyeViewportOptions<State = XRViewState> = XRViewportSize & {viewState: State; eyeOffset?: number};
18
+
19
+ /** Matrices supplied to a view-specific projection adapter. */
20
+ export type XRProjectionOptions = {projectionMatrix: Matrix4; viewMatrix: Matrix4};
21
+
22
+ /** Three-dimensional coordinate expressed in meters unless documented otherwise. */
23
+ export type XRVector3 = readonly [number, number, number];
24
+
25
+ /** Quaternion in `[x, y, z, w]` order. */
26
+ export type XRQuaternion = readonly [number, number, number, number];
27
+
28
+ /** Geographic coordinate in `[longitude, latitude, altitude]` order. */
29
+ export type XRGeographicPosition = readonly [number, number, number?];
30
+
31
+ /** Placement pose in an XR reference space. */
32
+ export type XRPlacementPose = {
33
+ position?: XRVector3;
34
+ orientation?: XRQuaternion;
35
+ };
36
+
37
+ /** Finite tabletop dimensions in physical XR meters. */
38
+ export type XRBoundedMapSurface = {
39
+ type: 'bounded';
40
+ width: number;
41
+ height: number;
42
+ };
43
+
44
+ /** Map plane without a finite interaction boundary. */
45
+ export type XRUnboundedMapSurface = {type: 'unbounded'};
46
+
47
+ /** Placement of a Web Mercator map in an XR reference space. */
48
+ export type XRMapPlacement = {
49
+ type: 'map';
50
+ anchor: XRGeographicPosition;
51
+ pose?: XRPlacementPose;
52
+ /** Geographic meters represented by one physical XR meter. */
53
+ metersPerXRUnit: number;
54
+ surface?: XRBoundedMapSurface | XRUnboundedMapSurface;
55
+ };
56
+
57
+ /** Placement of a globe in an XR reference space. */
58
+ export type XRGlobePlacement = {
59
+ type: 'globe';
60
+ anchor: XRGeographicPosition;
61
+ pose?: XRPlacementPose;
62
+ /** Physical globe radius in XR meters. */
63
+ radius: number;
64
+ /** Additional rotation around the globe's polar axis, in degrees. */
65
+ rotation?: number;
66
+ };
67
+
68
+ /** One-to-one local tangent placement for immersive first-person navigation. */
69
+ export type XRFirstPersonPlacement = {
70
+ type: 'first-person';
71
+ origin: XRGeographicPosition;
72
+ pose?: XRPlacementPose;
73
+ /** East, north, and up locomotion offset in geographic meters. */
74
+ position?: XRVector3;
75
+ /** Body heading in degrees. Head pitch and roll remain owned by WebXR. */
76
+ bearing?: number;
77
+ };
78
+
79
+ /** Supported geospatial content placements. */
80
+ export type XRPlacement = XRMapPlacement | XRGlobePlacement | XRFirstPersonPlacement;
81
+
82
+ /** Runtime presentation mode. */
83
+ export type XRPresentationMode = 'auto' | 'mono' | 'stereo-preview' | 'immersive-vr';
84
+
85
+ /** Minimal luma.gl WebXR view state consumed by the presentation. */
86
+ export type XRFrameView = {
87
+ eye?: string;
88
+ index: number;
89
+ viewport: readonly [number, number, number, number];
90
+ viewMatrix: readonly number[];
91
+ projectionMatrix: readonly number[];
92
+ framebuffer?: unknown;
93
+ };
94
+
95
+ /** Minimal luma.gl WebXR frame state consumed by the presentation. */
96
+ export type XRFrameState = {
97
+ views: readonly XRFrameView[];
98
+ framebuffer?: unknown;
99
+ };
100
+
101
+ /** Deck view contract required by {@link WebXRPresentation}. */
102
+ export type XRDeckView = {
103
+ id: string;
104
+ constructor: Function & {displayName?: string};
105
+ controller: View['controller'];
106
+ makeViewport: View['makeViewport'];
107
+ makeEyeViewport(options: XREyeViewportOptions<Parameters<View['makeViewport']>[0]['viewState']>): XRDeckViewport | null;
108
+ getHostFrame(viewport: XRDeckViewport): XRHostFrameFields;
109
+ /** Optional visibility recalculation after actual per-eye camera transforms. */
110
+ getHostFrameForCamera?(viewport: XRDeckViewport, camera: HostRenderView['camera'],
111
+ options?: {width?: number; height?: number}): XRHostFrameFields;
112
+ getXRProjectionMatrix(options: XRProjectionOptions): Matrix4;
113
+ };
114
+
115
+ /** Deck viewport surface used by the package without importing private deck internals. */
116
+ export type XRDeckViewport = Viewport & {longitude?: number; latitude?: number; pitch?: number; bearing?: number};
117
+
118
+ /** Deck controller methods used by the shared logical view. */
119
+ export type XRDeckController = Pick<Controller<never>, 'setProps' | 'finalize' | 'updateTransition'>;
120
+
121
+ /** Host-frame fields derived from one logical deck viewport. */
122
+ export type XRHostFrameFields = {
123
+ view: {
124
+ longitude: number;
125
+ latitude: number;
126
+ altitude?: number;
127
+ zoom: number;
128
+ };
129
+ projection?: HostFrameOptions['projection'];
130
+ camera: HostRenderView['camera'];
131
+ tileBuffer?: number;
132
+ /** Optional globally resident coarse globe fallback level. */
133
+ globePreloadZoom?: number;
134
+ };
135
+
136
+ /** One rendered eye or mono view and its corresponding deck viewport. */
137
+ export type XRPresentationRenderView = HostRenderView & {
138
+ id: string;
139
+ /** Eye rectangle is always defined by a prepared presentation. */
140
+ viewport: HostFrameOptions['viewport'];
141
+ hostFrame?: XRHostFrameFields;
142
+ deckViewport: XRDeckViewport;
143
+ xrView?: XRFrameView;
144
+ /** Logical view attached to native XR eyes for host integration. */
145
+ view?: XRDeckView;
146
+ /** Shared navigation snapshot attached to native XR eyes. */
147
+ viewState?: XRViewState;
148
+ };
149
+
150
+ /** Result of preparing one mono, stereo-preview, or immersive frame. */
151
+ export type XRPresentationFrame = {
152
+ mode: Exclude<XRPresentationMode, 'auto'>;
153
+ logicalViewport: XRDeckViewport;
154
+ renderViews: readonly XRPresentationRenderView[];
155
+ hostFrame: HostFrameOptions;
156
+ };
157
+
158
+ /** Ray expressed in the active XR reference space. */
159
+ export type XRSpatialRay = {
160
+ origin: XRVector3;
161
+ direction: XRVector3;
162
+ handedness?: string;
163
+ };
164
+
165
+ /** Top-origin canvas pointer in frame viewport units: CSS pixels for desktop, framebuffer pixels for XR. */
166
+ export type XRScreenPointer = {
167
+ x: number;
168
+ y: number;
169
+ eye?: 'left' | 'right' | 'center';
170
+ };
171
+
172
+ /** Geographic analytic or explicit terrain surface hit, not a rendered feature selection. */
173
+ export type XRSurfaceHit = {
174
+ /** Longitude/latitude degrees; zero altitude by default, physical terrain altitude when supplied. */
175
+ coordinate: readonly [number, number, number];
176
+ /** EPSG:3857 meters for planar content, or radius-256 globe common coordinates. */
177
+ position: XRVector3;
178
+ /** Rendered eye used by a screen pointer; absent for room rays and logical center-eye picks. */
179
+ renderViewId?: string;
180
+ /** Triangle and normal metadata when the explicit terrain surface was intersected. */
181
+ terrain?: Pick<TerrainSurfaceHit, 'triangleIndex' | 'normal' | 'barycentric'>;
182
+ };
183
+
184
+ /** Snapshot used to resolve a screen pointer or XR reference-space ray. */
185
+ export type XRSurfacePickingOptions = {
186
+ /** Host-owned mesh in content coordinates; a miss never falls back to the zero-height surface. */
187
+ terrain?: TerrainSurface;
188
+ /** Top-origin canvas pointer in frame viewport units, or reference-space ray. */
189
+ pointer: XRScreenPointer | XRSpatialRay;
190
+ /** Content placement and optional finite map interaction boundary. */
191
+ placement: XRPlacement;
192
+ /** Actual rendered frame; required for screen pointers. */
193
+ frame?: XRPresentationFrame;
194
+ /** Logical geographic state at the time the frame was rendered. */
195
+ viewState?: Record<string, unknown>;
196
+ /** Actual room placement matrix, including application animation; used only for room rays. */
197
+ placementMatrix?: HostRenderView['camera']['view'];
198
+ };
199
+
200
+ /** Input-independent navigation, pointing, and signal contract. */
201
+ export type XRInteractionIntent =
202
+ | {
203
+ type: 'navigate';
204
+ action: 'pan' | 'zoom' | 'rotate' | 'pitch' | 'move' | 'turn';
205
+ delta: readonly number[];
206
+ handedness?: string;
207
+ }
208
+ | {
209
+ type: 'point';
210
+ pointer: XRSpatialRay | XRScreenPointer;
211
+ action?: 'hover' | 'select' | 'grab' | 'release' | 'cancel';
212
+ /** Stable controller/pointer owner during a gesture; omitted for single-pointer clients. */
213
+ inputId?: string;
214
+ /** Activation source, so select release cannot end an independent squeeze grab. */
215
+ button?: 'select' | 'squeeze';
216
+ }
217
+ | {
218
+ type: 'signal';
219
+ action: string;
220
+ data?: unknown;
221
+ };