@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.
- package/README.md +36 -2
- package/dist/experimental/projected-basemaps.js +1310 -0
- package/dist/experimental/projected-basemaps.js.map +6 -0
- package/dist/experimental/terrain.js +379 -0
- package/dist/experimental/terrain.js.map +6 -0
- package/dist/experimental/webxr.js +1423 -0
- package/dist/experimental/webxr.js.map +6 -0
- package/dist/index.js +756 -0
- package/dist/index.js.map +6 -0
- package/dist/types/experimental/projected-basemaps.d.ts +120 -0
- package/dist/types/experimental/projected-detail.d.ts +33 -0
- package/dist/types/experimental/projected-navigation.d.ts +117 -0
- package/dist/types/experimental/terrain-display.d.ts +124 -0
- package/dist/types/experimental/terrain.d.ts +131 -0
- package/dist/types/first_person_view_adapter.d.ts +29 -0
- package/dist/types/globe_view_adapter.d.ts +33 -0
- package/dist/types/index.d.ts +84 -0
- package/dist/types/tangram-layer-lifecycle.d.ts +132 -0
- package/dist/types/tangram-layer.d.ts +157 -0
- package/dist/types/view_adapter_types.d.ts +42 -0
- package/dist/types/web_mercator_view_adapter.d.ts +77 -0
- package/package.json +80 -3
- package/src/experimental/projected-basemaps.ts +301 -0
- package/src/experimental/projected-detail.ts +116 -0
- package/src/experimental/projected-navigation.ts +323 -0
- package/src/experimental/terrain-display.ts +243 -0
- package/src/experimental/terrain.ts +240 -0
- package/src/experimental/webxr/grabbing.ts +166 -0
- package/src/experimental/webxr/index.d.ts +77 -0
- package/src/experimental/webxr/index.ts +72 -0
- package/src/experimental/webxr/interaction.ts +221 -0
- package/src/experimental/webxr/picking.ts +119 -0
- package/src/experimental/webxr/presentation.ts +457 -0
- package/src/experimental/webxr/projection.ts +371 -0
- package/src/experimental/webxr/types.ts +221 -0
- package/src/experimental/webxr/views.ts +190 -0
- package/src/first_person_view_adapter.ts +107 -0
- package/src/globe_view_adapter.ts +103 -0
- package/src/index.ts +64 -0
- package/src/tangram-layer-lifecycle.ts +130 -0
- package/src/tangram-layer.ts +679 -0
- package/src/view_adapter_types.ts +40 -0
- package/src/web_mercator_view_adapter.ts +185 -0
|
@@ -0,0 +1,1423 @@
|
|
|
1
|
+
// src/experimental/webxr/interaction.ts
|
|
2
|
+
import { getWebXRInputRay } from "@luma.gl/experimental";
|
|
3
|
+
var WebXRInputAdapter = class {
|
|
4
|
+
options;
|
|
5
|
+
previousSelect = /* @__PURE__ */ new Map();
|
|
6
|
+
previousSqueeze = /* @__PURE__ */ new Map();
|
|
7
|
+
previousPointers = /* @__PURE__ */ new Map();
|
|
8
|
+
constructor(options = {}) {
|
|
9
|
+
this.options = {
|
|
10
|
+
moveDeadzone: options.moveDeadzone ?? 0.15,
|
|
11
|
+
turnDeadzone: options.turnDeadzone ?? 0.55,
|
|
12
|
+
moveSpeed: options.moveSpeed ?? 1,
|
|
13
|
+
turnSpeed: options.turnSpeed ?? 30
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/** Translate one XR input frame into time-scaled navigation, pointing, and signal intents. */
|
|
17
|
+
update(inputStates, elapsedSeconds = 1 / 60) {
|
|
18
|
+
const intents = [];
|
|
19
|
+
const activeKeys = /* @__PURE__ */ new Set();
|
|
20
|
+
for (const inputState of inputStates) {
|
|
21
|
+
const key = `${inputState.handedness || "none"}:${inputState.index}`;
|
|
22
|
+
activeKeys.add(key);
|
|
23
|
+
const ray = getWebXRInputRay(
|
|
24
|
+
inputState
|
|
25
|
+
);
|
|
26
|
+
if (ray && ray.origin.every(Number.isFinite) && ray.direction.every(Number.isFinite) && Math.hypot(...ray.direction) > 0) {
|
|
27
|
+
const pointer = {
|
|
28
|
+
origin: [ray.origin[0], ray.origin[1], ray.origin[2]],
|
|
29
|
+
direction: [ray.direction[0], ray.direction[1], ray.direction[2]],
|
|
30
|
+
handedness: inputState.handedness
|
|
31
|
+
};
|
|
32
|
+
this.previousPointers.set(key, pointer);
|
|
33
|
+
intents.push({ type: "point", pointer, action: "hover", inputId: key });
|
|
34
|
+
appendActivationIntent({
|
|
35
|
+
intents,
|
|
36
|
+
active: Boolean(inputState.selectActive),
|
|
37
|
+
previous: this.previousSelect.get(key) || false,
|
|
38
|
+
pointer,
|
|
39
|
+
pressedAction: "select",
|
|
40
|
+
releasedAction: "release",
|
|
41
|
+
inputId: key,
|
|
42
|
+
button: "select"
|
|
43
|
+
});
|
|
44
|
+
appendActivationIntent({
|
|
45
|
+
intents,
|
|
46
|
+
active: Boolean(inputState.squeezeActive),
|
|
47
|
+
previous: this.previousSqueeze.get(key) || false,
|
|
48
|
+
pointer,
|
|
49
|
+
pressedAction: "grab",
|
|
50
|
+
releasedAction: "release",
|
|
51
|
+
inputId: key,
|
|
52
|
+
button: "squeeze"
|
|
53
|
+
});
|
|
54
|
+
} else {
|
|
55
|
+
const pointer = this.previousPointers.get(key);
|
|
56
|
+
if (pointer && (this.previousSelect.get(key) || this.previousSqueeze.get(key))) {
|
|
57
|
+
intents.push({ type: "point", pointer, action: "cancel", inputId: key });
|
|
58
|
+
}
|
|
59
|
+
this.previousPointers.delete(key);
|
|
60
|
+
}
|
|
61
|
+
this.previousSelect.set(key, Boolean(inputState.selectActive));
|
|
62
|
+
this.previousSqueeze.set(key, Boolean(inputState.squeezeActive));
|
|
63
|
+
this.appendGamepadIntents(intents, inputState, Math.max(0, elapsedSeconds));
|
|
64
|
+
}
|
|
65
|
+
for (const key of this.previousSelect.keys()) {
|
|
66
|
+
if (!activeKeys.has(key)) {
|
|
67
|
+
const pointer = this.previousPointers.get(key);
|
|
68
|
+
if (pointer && (this.previousSelect.get(key) || this.previousSqueeze.get(key))) {
|
|
69
|
+
intents.push({ type: "point", pointer, action: "cancel", inputId: key });
|
|
70
|
+
}
|
|
71
|
+
this.previousSelect.delete(key);
|
|
72
|
+
this.previousSqueeze.delete(key);
|
|
73
|
+
this.previousPointers.delete(key);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return intents;
|
|
77
|
+
}
|
|
78
|
+
/** Forget action transitions, for example when an XR session ends. */
|
|
79
|
+
reset() {
|
|
80
|
+
this.previousSelect.clear();
|
|
81
|
+
this.previousSqueeze.clear();
|
|
82
|
+
this.previousPointers.clear();
|
|
83
|
+
}
|
|
84
|
+
appendGamepadIntents(intents, inputState, elapsedSeconds) {
|
|
85
|
+
const axes = inputState.gamepad?.axes;
|
|
86
|
+
if (!axes || axes.length < 2) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const horizontal = applyDeadzone(axes[axes.length - 2] || 0, this.options.moveDeadzone);
|
|
90
|
+
const vertical = applyDeadzone(axes[axes.length - 1] || 0, this.options.moveDeadzone);
|
|
91
|
+
if (inputState.handedness === "right") {
|
|
92
|
+
if (Math.abs(horizontal) >= this.options.turnDeadzone) {
|
|
93
|
+
intents.push({
|
|
94
|
+
type: "navigate",
|
|
95
|
+
action: "turn",
|
|
96
|
+
delta: [Math.sign(horizontal) * this.options.turnSpeed * elapsedSeconds],
|
|
97
|
+
handedness: inputState.handedness
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (horizontal || vertical) {
|
|
103
|
+
intents.push({
|
|
104
|
+
type: "navigate",
|
|
105
|
+
action: "move",
|
|
106
|
+
delta: [
|
|
107
|
+
horizontal * this.options.moveSpeed * elapsedSeconds,
|
|
108
|
+
-vertical * this.options.moveSpeed * elapsedSeconds
|
|
109
|
+
],
|
|
110
|
+
handedness: inputState.handedness
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
async function setWebXRSessionWithFallback(manager, session, referenceSpaceTypes = ["local-floor", "local"]) {
|
|
116
|
+
let lastError;
|
|
117
|
+
for (const referenceSpaceType of referenceSpaceTypes) {
|
|
118
|
+
try {
|
|
119
|
+
await manager.setSession(session, { referenceSpaceType });
|
|
120
|
+
return referenceSpaceType;
|
|
121
|
+
} catch (error) {
|
|
122
|
+
lastError = error;
|
|
123
|
+
if (!(error instanceof DOMException) || error.name !== "NotSupportedError") {
|
|
124
|
+
throw error;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
throw lastError || new Error("No requested WebXR reference space is available");
|
|
129
|
+
}
|
|
130
|
+
function appendActivationIntent({
|
|
131
|
+
intents,
|
|
132
|
+
active,
|
|
133
|
+
previous,
|
|
134
|
+
pointer,
|
|
135
|
+
pressedAction,
|
|
136
|
+
releasedAction,
|
|
137
|
+
inputId,
|
|
138
|
+
button
|
|
139
|
+
}) {
|
|
140
|
+
if (active && !previous) {
|
|
141
|
+
intents.push({ type: "point", pointer, action: pressedAction, inputId, button });
|
|
142
|
+
} else if (!active && previous) {
|
|
143
|
+
intents.push({ type: "point", pointer, action: releasedAction, inputId, button });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function applyDeadzone(value, deadzone) {
|
|
147
|
+
return Math.abs(value) < deadzone ? 0 : value;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// src/experimental/webxr/presentation.ts
|
|
151
|
+
import { Matrix4 as Matrix42 } from "@math.gl/core";
|
|
152
|
+
import { DoubleClickDrag, EventManager, Pan, Pinch, Tap } from "mjolnir.js";
|
|
153
|
+
|
|
154
|
+
// src/experimental/webxr/projection.ts
|
|
155
|
+
import { Matrix4 } from "@math.gl/core";
|
|
156
|
+
import { PlaneShape, SphereShape } from "@math.gl/culling";
|
|
157
|
+
import { PROJECTION_CONSTANTS, unprojectGlobePosition } from "@tangram.gl/tangram-renderer/core";
|
|
158
|
+
var TANGRAM_HALF_WORLD_METERS = 20037508342789244e-9;
|
|
159
|
+
var GLOBE_RADIUS = PROJECTION_CONSTANTS.globeRadius;
|
|
160
|
+
var MAP_SURFACE = new PlaneShape();
|
|
161
|
+
var GLOBE_SURFACE = new SphereShape({ radius: GLOBE_RADIUS });
|
|
162
|
+
var MAX_MERCATOR_LATITUDE = 85.05112878;
|
|
163
|
+
var DEGREES_TO_RADIANS = Math.PI / 180;
|
|
164
|
+
var RADIANS_TO_DEGREES = 180 / Math.PI;
|
|
165
|
+
function longitudeLatitudeToMeters(longitude, latitude) {
|
|
166
|
+
const clampedLatitude = Math.max(
|
|
167
|
+
-MAX_MERCATOR_LATITUDE,
|
|
168
|
+
Math.min(MAX_MERCATOR_LATITUDE, latitude)
|
|
169
|
+
);
|
|
170
|
+
return [
|
|
171
|
+
longitude / 180 * TANGRAM_HALF_WORLD_METERS,
|
|
172
|
+
Math.log(Math.tan((90 + clampedLatitude) * Math.PI / 360)) / Math.PI * TANGRAM_HALF_WORLD_METERS
|
|
173
|
+
];
|
|
174
|
+
}
|
|
175
|
+
function metersToLongitudeLatitude(x, y, altitude = 0) {
|
|
176
|
+
return [
|
|
177
|
+
x / TANGRAM_HALF_WORLD_METERS * 180,
|
|
178
|
+
Math.atan(Math.exp(y / TANGRAM_HALF_WORLD_METERS * Math.PI)) * 360 / Math.PI - 90,
|
|
179
|
+
altitude
|
|
180
|
+
];
|
|
181
|
+
}
|
|
182
|
+
function createXRPoseMatrix(pose = {}) {
|
|
183
|
+
const position = pose.position || [0, 0, 0];
|
|
184
|
+
const orientation = pose.orientation || [0, 0, 0, 1];
|
|
185
|
+
return new Matrix4().translate(position).multiplyRight(new Matrix4().fromQuaternion(orientation));
|
|
186
|
+
}
|
|
187
|
+
function createXRPlacementMatrix(placement, viewState = {}) {
|
|
188
|
+
if (placement.type === "globe") {
|
|
189
|
+
const longitude2 = finiteNumber(viewState.longitude, placement.anchor[0]);
|
|
190
|
+
const latitude2 = finiteNumber(viewState.latitude, placement.anchor[1]);
|
|
191
|
+
const rotation = placement.rotation || 0;
|
|
192
|
+
const scale = placement.radius / GLOBE_RADIUS;
|
|
193
|
+
return createXRPoseMatrix(placement.pose).rotateX(latitude2 * DEGREES_TO_RADIANS - Math.PI / 2).rotateZ(-(longitude2 + rotation) * DEGREES_TO_RADIANS).scale([scale, scale, scale]);
|
|
194
|
+
}
|
|
195
|
+
const fallbackAnchor = placement.type === "map" ? placement.anchor : placement.origin;
|
|
196
|
+
const longitude = finiteNumber(viewState.longitude, fallbackAnchor[0]);
|
|
197
|
+
const latitude = finiteNumber(viewState.latitude, fallbackAnchor[1]);
|
|
198
|
+
const [centerX, centerY] = longitudeLatitudeToMeters(longitude, latitude);
|
|
199
|
+
const metersPerMercatorMeter = Math.cos(
|
|
200
|
+
Math.max(-MAX_MERCATOR_LATITUDE, Math.min(MAX_MERCATOR_LATITUDE, latitude)) * DEGREES_TO_RADIANS
|
|
201
|
+
);
|
|
202
|
+
if (placement.type === "map") {
|
|
203
|
+
const scale = 1 / placement.metersPerXRUnit;
|
|
204
|
+
return createXRPoseMatrix(placement.pose).rotateX(-Math.PI / 2).scale([scale * metersPerMercatorMeter, scale * metersPerMercatorMeter, scale]).translate([-centerX, -centerY, -(placement.anchor[2] || 0)]);
|
|
205
|
+
}
|
|
206
|
+
const offset = placement.position || [0, 0, 0];
|
|
207
|
+
const bearing = finiteNumber(viewState.bearing, placement.bearing || 0);
|
|
208
|
+
return createXRPoseMatrix(placement.pose).rotateY(-bearing * DEGREES_TO_RADIANS).rotateX(-Math.PI / 2).translate([-offset[0], -offset[1], -offset[2]]).scale([metersPerMercatorMeter, metersPerMercatorMeter, 1]).translate([-centerX, -centerY, 0]);
|
|
209
|
+
}
|
|
210
|
+
function transformXRRayToContent(ray, placementMatrix) {
|
|
211
|
+
const inverse = new Matrix4().copy(placementMatrix).invert();
|
|
212
|
+
const origin = transformPoint(inverse, ray.origin);
|
|
213
|
+
const farPoint = transformPoint(inverse, [
|
|
214
|
+
ray.origin[0] + ray.direction[0],
|
|
215
|
+
ray.origin[1] + ray.direction[1],
|
|
216
|
+
ray.origin[2] + ray.direction[2]
|
|
217
|
+
]);
|
|
218
|
+
return {
|
|
219
|
+
origin,
|
|
220
|
+
direction: normalizeVector([
|
|
221
|
+
farPoint[0] - origin[0],
|
|
222
|
+
farPoint[1] - origin[1],
|
|
223
|
+
farPoint[2] - origin[2]
|
|
224
|
+
])
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
function intersectXRMap(ray, placement, viewState = {}) {
|
|
228
|
+
const placementMatrix = createXRPlacementMatrix(placement, viewState);
|
|
229
|
+
const localRay = transformXRRayToContent(ray, placementMatrix);
|
|
230
|
+
return intersectContentSurface(localRay, placement, viewState)?.coordinate || null;
|
|
231
|
+
}
|
|
232
|
+
function intersectContentSurface(localRay, placement, viewState = {}) {
|
|
233
|
+
if (!localRay.origin.every(Number.isFinite) || !localRay.direction.every(Number.isFinite) || Math.hypot(...localRay.direction) === 0) return null;
|
|
234
|
+
if (placement.type === "globe") {
|
|
235
|
+
const distance2 = intersectSphereDistance(localRay.origin, localRay.direction);
|
|
236
|
+
if (distance2 === null) return null;
|
|
237
|
+
const point = [
|
|
238
|
+
localRay.origin[0] + localRay.direction[0] * distance2,
|
|
239
|
+
localRay.origin[1] + localRay.direction[1] * distance2,
|
|
240
|
+
localRay.origin[2] + localRay.direction[2] * distance2
|
|
241
|
+
];
|
|
242
|
+
const coordinate = globePositionToLongitudeLatitude(point);
|
|
243
|
+
coordinate[2] = 0;
|
|
244
|
+
return { coordinate, position: point };
|
|
245
|
+
}
|
|
246
|
+
const distance = intersectPlaneDistance(localRay.origin, localRay.direction, 0);
|
|
247
|
+
if (distance === null) {
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
const hit = [
|
|
251
|
+
localRay.origin[0] + localRay.direction[0] * distance,
|
|
252
|
+
localRay.origin[1] + localRay.direction[1] * distance,
|
|
253
|
+
0
|
|
254
|
+
];
|
|
255
|
+
if (!isContentPointWithinMapBounds(hit, placement, viewState)) return null;
|
|
256
|
+
return { coordinate: metersToLongitudeLatitude(hit[0], hit[1]), position: hit };
|
|
257
|
+
}
|
|
258
|
+
function isContentPointWithinMapBounds(hit, placement, viewState = {}) {
|
|
259
|
+
if (placement.type === "map" && placement.surface?.type === "bounded") {
|
|
260
|
+
const longitude = finiteNumber(viewState.longitude, placement.anchor[0]);
|
|
261
|
+
const latitude = finiteNumber(viewState.latitude, placement.anchor[1]);
|
|
262
|
+
const [centerX, centerY] = longitudeLatitudeToMeters(longitude, latitude);
|
|
263
|
+
const latitudeScale = Math.cos(
|
|
264
|
+
Math.max(-MAX_MERCATOR_LATITUDE, Math.min(MAX_MERCATOR_LATITUDE, latitude)) * DEGREES_TO_RADIANS
|
|
265
|
+
);
|
|
266
|
+
const halfWidth = placement.surface.width * placement.metersPerXRUnit / (2 * latitudeScale);
|
|
267
|
+
const halfHeight = placement.surface.height * placement.metersPerXRUnit / (2 * latitudeScale);
|
|
268
|
+
if (Math.abs(hit[0] - centerX) > halfWidth || Math.abs(hit[1] - centerY) > halfHeight) {
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
function intersectXRGlobe(ray, placement, viewState = {}) {
|
|
275
|
+
const placementMatrix = createXRPlacementMatrix(placement, viewState);
|
|
276
|
+
const localRay = transformXRRayToContent(ray, placementMatrix);
|
|
277
|
+
return intersectContentSurface(localRay, placement, viewState)?.coordinate || null;
|
|
278
|
+
}
|
|
279
|
+
function getXRGlobeVisibleBounds({
|
|
280
|
+
views,
|
|
281
|
+
placement,
|
|
282
|
+
viewState
|
|
283
|
+
}) {
|
|
284
|
+
const placementMatrix = createXRPlacementMatrix(placement, viewState);
|
|
285
|
+
const coordinates = [];
|
|
286
|
+
const samples = [-1, 0, 1];
|
|
287
|
+
let crossesHorizon = false;
|
|
288
|
+
for (const view of views) {
|
|
289
|
+
const inverseClip = new Matrix4(view.projectionMatrix).multiplyRight(view.viewMatrix).multiplyRight(placementMatrix).invert();
|
|
290
|
+
for (const x of samples) {
|
|
291
|
+
for (const y of samples) {
|
|
292
|
+
const near = transformHomogeneousPoint(inverseClip, [x, y, -1]);
|
|
293
|
+
const far = transformHomogeneousPoint(inverseClip, [x, y, 1]);
|
|
294
|
+
const direction = normalizeVector([
|
|
295
|
+
far[0] - near[0],
|
|
296
|
+
far[1] - near[1],
|
|
297
|
+
far[2] - near[2]
|
|
298
|
+
]);
|
|
299
|
+
const distance = intersectSphereDistance(near, direction);
|
|
300
|
+
if (distance !== null) {
|
|
301
|
+
coordinates.push(
|
|
302
|
+
globePositionToLongitudeLatitude([
|
|
303
|
+
near[0] + direction[0] * distance,
|
|
304
|
+
near[1] + direction[1] * distance,
|
|
305
|
+
near[2] + direction[2] * distance
|
|
306
|
+
])
|
|
307
|
+
);
|
|
308
|
+
} else crossesHorizon = true;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (crossesHorizon && coordinates.length > 0) {
|
|
313
|
+
return getXRGlobeHorizonBounds(
|
|
314
|
+
views,
|
|
315
|
+
placementMatrix,
|
|
316
|
+
finiteNumber(viewState?.longitude, placement.anchor[0])
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
if (coordinates.length === 0) {
|
|
320
|
+
return [-180, -MAX_MERCATOR_LATITUDE, 180, MAX_MERCATOR_LATITUDE];
|
|
321
|
+
}
|
|
322
|
+
const anchorLongitude = finiteNumber(viewState?.longitude, placement.anchor[0]);
|
|
323
|
+
const unwrappedLongitudes = coordinates.map(
|
|
324
|
+
([longitude]) => unwrapLongitude(longitude, anchorLongitude)
|
|
325
|
+
);
|
|
326
|
+
const latitudes = coordinates.map((coordinate) => coordinate[1]);
|
|
327
|
+
return [
|
|
328
|
+
Math.min(...unwrappedLongitudes),
|
|
329
|
+
Math.max(-MAX_MERCATOR_LATITUDE, Math.min(...latitudes)),
|
|
330
|
+
Math.max(...unwrappedLongitudes),
|
|
331
|
+
Math.min(MAX_MERCATOR_LATITUDE, Math.max(...latitudes))
|
|
332
|
+
];
|
|
333
|
+
}
|
|
334
|
+
function getXRGlobeHorizonBounds(views, placementMatrix, anchorLongitude) {
|
|
335
|
+
let west = Infinity, south = Infinity, east = -Infinity, north = -Infinity;
|
|
336
|
+
for (const view of views) {
|
|
337
|
+
const camera = new Matrix4(view.viewMatrix).multiplyRight(placementMatrix).invert().transformAsPoint([0, 0, 0]);
|
|
338
|
+
const distance = Math.hypot(camera[0], camera[1], camera[2]);
|
|
339
|
+
if (!Number.isFinite(distance) || distance <= GLOBE_RADIUS) {
|
|
340
|
+
return [anchorLongitude - 180, -MAX_MERCATOR_LATITUDE, anchorLongitude + 180, MAX_MERCATOR_LATITUDE];
|
|
341
|
+
}
|
|
342
|
+
const [longitude, latitude] = globePositionToLongitudeLatitude([camera[0], camera[1], camera[2]]);
|
|
343
|
+
const radius = Math.acos(GLOBE_RADIUS / distance);
|
|
344
|
+
const latitudeRadians = latitude * DEGREES_TO_RADIANS;
|
|
345
|
+
const centerLongitude = unwrapLongitude(longitude, anchorLongitude);
|
|
346
|
+
const longitudeRadius = Math.abs(latitudeRadians) + radius >= Math.PI / 2 ? Math.PI : Math.asin(Math.min(1, Math.sin(radius) / Math.cos(latitudeRadians)));
|
|
347
|
+
west = Math.min(west, longitudeRadius === Math.PI ? anchorLongitude - 180 : centerLongitude - longitudeRadius / DEGREES_TO_RADIANS);
|
|
348
|
+
east = Math.max(east, longitudeRadius === Math.PI ? anchorLongitude + 180 : centerLongitude + longitudeRadius / DEGREES_TO_RADIANS);
|
|
349
|
+
south = Math.min(south, latitude - radius / DEGREES_TO_RADIANS);
|
|
350
|
+
north = Math.max(north, latitude + radius / DEGREES_TO_RADIANS);
|
|
351
|
+
}
|
|
352
|
+
return [west, Math.max(-MAX_MERCATOR_LATITUDE, south), east, Math.min(MAX_MERCATOR_LATITUDE, north)];
|
|
353
|
+
}
|
|
354
|
+
function unionGeographicBounds(bounds, anchorLongitude = 0) {
|
|
355
|
+
if (bounds.length === 0) {
|
|
356
|
+
return [-180, -MAX_MERCATOR_LATITUDE, 180, MAX_MERCATOR_LATITUDE];
|
|
357
|
+
}
|
|
358
|
+
const west = bounds.map((value) => unwrapLongitude(value[0], anchorLongitude));
|
|
359
|
+
const east = bounds.map((value) => unwrapLongitude(value[2], anchorLongitude));
|
|
360
|
+
return [
|
|
361
|
+
Math.min(...west),
|
|
362
|
+
Math.min(...bounds.map((value) => value[1])),
|
|
363
|
+
Math.max(...east),
|
|
364
|
+
Math.max(...bounds.map((value) => value[3]))
|
|
365
|
+
];
|
|
366
|
+
}
|
|
367
|
+
function intersectPlaneDistance(origin, direction, planeZ) {
|
|
368
|
+
if (Math.abs(direction[2]) < 1e-8) {
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
return MAP_SURFACE.intersectLocalRay(
|
|
372
|
+
[origin[0], origin[2] - planeZ, origin[1]],
|
|
373
|
+
[direction[0], direction[2], direction[1]]
|
|
374
|
+
)?.distance ?? null;
|
|
375
|
+
}
|
|
376
|
+
function intersectSphereDistance(origin, direction) {
|
|
377
|
+
return GLOBE_SURFACE.intersectLocalRay(origin, direction)?.distance ?? null;
|
|
378
|
+
}
|
|
379
|
+
function globePositionToLongitudeLatitude(point) {
|
|
380
|
+
return unprojectGlobePosition(point);
|
|
381
|
+
}
|
|
382
|
+
function transformPoint(matrix, point) {
|
|
383
|
+
return transformHomogeneousPoint(matrix, point);
|
|
384
|
+
}
|
|
385
|
+
function transformHomogeneousPoint(matrix, point) {
|
|
386
|
+
const x = point[0];
|
|
387
|
+
const y = point[1];
|
|
388
|
+
const z = point[2];
|
|
389
|
+
const w = matrix[3] * x + matrix[7] * y + matrix[11] * z + matrix[15];
|
|
390
|
+
const divisor = w || 1;
|
|
391
|
+
return [
|
|
392
|
+
(matrix[0] * x + matrix[4] * y + matrix[8] * z + matrix[12]) / divisor,
|
|
393
|
+
(matrix[1] * x + matrix[5] * y + matrix[9] * z + matrix[13]) / divisor,
|
|
394
|
+
(matrix[2] * x + matrix[6] * y + matrix[10] * z + matrix[14]) / divisor
|
|
395
|
+
];
|
|
396
|
+
}
|
|
397
|
+
function normalizeVector(vector) {
|
|
398
|
+
const length = Math.hypot(vector[0], vector[1], vector[2]);
|
|
399
|
+
if (!length) {
|
|
400
|
+
return [0, 0, -1];
|
|
401
|
+
}
|
|
402
|
+
return [vector[0] / length, vector[1] / length, vector[2] / length];
|
|
403
|
+
}
|
|
404
|
+
function finiteNumber(value, fallback) {
|
|
405
|
+
return Number.isFinite(value) ? value : fallback;
|
|
406
|
+
}
|
|
407
|
+
function unwrapLongitude(longitude, anchor) {
|
|
408
|
+
let result = longitude;
|
|
409
|
+
while (result - anchor > 180) result -= 360;
|
|
410
|
+
while (result - anchor < -180) result += 360;
|
|
411
|
+
return result;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// src/experimental/webxr/presentation.ts
|
|
415
|
+
var DEFAULT_INTERPUPILLARY_DISTANCE = 0.064;
|
|
416
|
+
var WebXRPresentation = class {
|
|
417
|
+
/** Initialize one logical view without allocating controllers or GPU resources. */
|
|
418
|
+
constructor({ view, viewState, placement, mode = "auto" }) {
|
|
419
|
+
this.view = view;
|
|
420
|
+
this.viewState = { ...viewState };
|
|
421
|
+
this.placement = placement || createDefaultPlacement(view, viewState);
|
|
422
|
+
this.mode = mode;
|
|
423
|
+
this.controllerMode = mode;
|
|
424
|
+
this.controller = null;
|
|
425
|
+
this.rightController = null;
|
|
426
|
+
this.eventManager = null;
|
|
427
|
+
this.controllerSize = { width: 1, height: 1 };
|
|
428
|
+
}
|
|
429
|
+
/** Read the shared state used by mono and both stereo eyes. */
|
|
430
|
+
getViewState() {
|
|
431
|
+
return this.viewState;
|
|
432
|
+
}
|
|
433
|
+
/** Apply a logical navigation patch and synchronize desktop controllers. */
|
|
434
|
+
setViewState(update) {
|
|
435
|
+
const patch = typeof update === "function" ? update(this.viewState) : update;
|
|
436
|
+
this.viewState = { ...this.viewState, ...patch };
|
|
437
|
+
this.updateController(this.controllerSize);
|
|
438
|
+
return this.viewState;
|
|
439
|
+
}
|
|
440
|
+
/** Replace the room placement without changing geographic navigation. */
|
|
441
|
+
setPlacement(placement) {
|
|
442
|
+
this.placement = placement;
|
|
443
|
+
}
|
|
444
|
+
/** Change the requested presentation mode and desktop hit regions. */
|
|
445
|
+
setMode(mode) {
|
|
446
|
+
this.mode = mode;
|
|
447
|
+
this.updateController(this.controllerSize);
|
|
448
|
+
}
|
|
449
|
+
/** Install deck.gl gestures backed by the shared logical navigation state. */
|
|
450
|
+
attachController({ element, timeline, onViewStateChange = () => {
|
|
451
|
+
}, onStateChange = () => {
|
|
452
|
+
} }) {
|
|
453
|
+
const controllerOptions = this.view.controller;
|
|
454
|
+
if (!controllerOptions) return null;
|
|
455
|
+
this.eventManager = new EventManager(element, {
|
|
456
|
+
touchAction: "none",
|
|
457
|
+
recognizers: [
|
|
458
|
+
{ recognizer: new Pan({ event: "multipan", threshold: 10, pointers: 2, trackpad: true }) },
|
|
459
|
+
{ recognizer: new Pinch({ event: "pinch", trackpad: true }), requireFailure: ["multipan"] },
|
|
460
|
+
{
|
|
461
|
+
recognizer: new Pan({ event: "pan", threshold: 1 }),
|
|
462
|
+
recognizeWith: ["pinch"],
|
|
463
|
+
requireFailure: ["multipan"]
|
|
464
|
+
},
|
|
465
|
+
{ recognizer: new Tap({ event: "dblclick", taps: 2, enable: false }) },
|
|
466
|
+
{
|
|
467
|
+
recognizer: new DoubleClickDrag({ event: "dblclickdrag", enable: false }),
|
|
468
|
+
recognizeWith: ["dblclick"]
|
|
469
|
+
},
|
|
470
|
+
{
|
|
471
|
+
recognizer: new Tap({ event: "click" }),
|
|
472
|
+
recognizeWith: ["dblclickdrag"],
|
|
473
|
+
requireFailure: ["dblclick", "dblclickdrag"]
|
|
474
|
+
}
|
|
475
|
+
]
|
|
476
|
+
});
|
|
477
|
+
this.controllerCallbacks = { timeline, onViewStateChange, onStateChange };
|
|
478
|
+
this.controller = this.createController();
|
|
479
|
+
this.updateController(this.controllerSize);
|
|
480
|
+
return this.controller;
|
|
481
|
+
}
|
|
482
|
+
/** Create a controller for one screen region backed by the shared view state. */
|
|
483
|
+
createController() {
|
|
484
|
+
const controllerOptions = this.view.controller;
|
|
485
|
+
if (!controllerOptions || !this.eventManager) {
|
|
486
|
+
throw new Error("Attach a controller before creating an eye controller");
|
|
487
|
+
}
|
|
488
|
+
const { timeline, onViewStateChange, onStateChange } = this.controllerCallbacks;
|
|
489
|
+
const Controller = controllerOptions.type;
|
|
490
|
+
return new Controller({
|
|
491
|
+
timeline,
|
|
492
|
+
eventManager: this.eventManager,
|
|
493
|
+
makeViewport: (viewState) => requireViewport(this.view.makeViewport({
|
|
494
|
+
...this.controllerSize,
|
|
495
|
+
width: this.controllerMode === "stereo-preview" ? this.controllerSize.width / 2 : this.controllerSize.width,
|
|
496
|
+
viewState
|
|
497
|
+
})),
|
|
498
|
+
onViewStateChange: (parameters) => {
|
|
499
|
+
this.viewState = { ...parameters.viewState };
|
|
500
|
+
this.updateController(this.controllerSize);
|
|
501
|
+
onViewStateChange({ ...parameters, viewState: this.viewState });
|
|
502
|
+
},
|
|
503
|
+
onStateChange
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
/** Match controller rectangles to the current mono or stereo dimensions. */
|
|
507
|
+
updateController({ width, height }, mode = this.mode) {
|
|
508
|
+
this.controllerSize = { width, height };
|
|
509
|
+
this.controllerMode = mode;
|
|
510
|
+
if (!this.controller) return;
|
|
511
|
+
if (mode === "stereo-preview") {
|
|
512
|
+
this.rightController ||= this.createController();
|
|
513
|
+
width /= 2;
|
|
514
|
+
} else if (this.rightController) {
|
|
515
|
+
this.rightController.finalize();
|
|
516
|
+
this.rightController = null;
|
|
517
|
+
}
|
|
518
|
+
const viewport = this.view.makeViewport({ width, height, viewState: this.viewState });
|
|
519
|
+
const controllerOptions = this.view.controller;
|
|
520
|
+
if (!viewport || !controllerOptions) return;
|
|
521
|
+
this.controller.setProps({
|
|
522
|
+
...this.viewState,
|
|
523
|
+
...controllerOptions,
|
|
524
|
+
id: this.view.id,
|
|
525
|
+
x: viewport.x,
|
|
526
|
+
y: viewport.y,
|
|
527
|
+
width: viewport.width,
|
|
528
|
+
height: viewport.height
|
|
529
|
+
});
|
|
530
|
+
this.rightController?.setProps({
|
|
531
|
+
...this.viewState,
|
|
532
|
+
...controllerOptions,
|
|
533
|
+
// Keyboard input is global to the shared EventManager. Only the left
|
|
534
|
+
// controller handles it, otherwise FirstPersonView moves twice per key.
|
|
535
|
+
keyboard: false,
|
|
536
|
+
id: `${this.view.id}-right`,
|
|
537
|
+
x: width,
|
|
538
|
+
y: viewport.y,
|
|
539
|
+
width: viewport.width,
|
|
540
|
+
height: viewport.height
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
/** Advance desktop zoom and inertia transitions once per animation frame. */
|
|
544
|
+
updateTransitions() {
|
|
545
|
+
this.controller?.updateTransition();
|
|
546
|
+
this.rightController?.updateTransition();
|
|
547
|
+
}
|
|
548
|
+
/** Release desktop input resources without taking ownership of the host view. */
|
|
549
|
+
finalize() {
|
|
550
|
+
this.controller?.finalize();
|
|
551
|
+
this.rightController?.finalize();
|
|
552
|
+
this.eventManager?.destroy();
|
|
553
|
+
this.controller = null;
|
|
554
|
+
this.rightController = null;
|
|
555
|
+
this.eventManager = null;
|
|
556
|
+
}
|
|
557
|
+
/** Derive one eye viewport and its matching Tangram camera contract. */
|
|
558
|
+
makeRenderView({ id, width, height, eyeOffset = 0, viewportX = 0 }) {
|
|
559
|
+
const deckViewport = this.view.makeEyeViewport({
|
|
560
|
+
width,
|
|
561
|
+
height,
|
|
562
|
+
viewState: this.viewState,
|
|
563
|
+
eyeOffset
|
|
564
|
+
});
|
|
565
|
+
if (!deckViewport) {
|
|
566
|
+
throw new Error(`${this.view.constructor.displayName} produced an empty viewport`);
|
|
567
|
+
}
|
|
568
|
+
const hostFrame = this.view.getHostFrame(deckViewport);
|
|
569
|
+
return {
|
|
570
|
+
id,
|
|
571
|
+
viewport: { x: viewportX, y: 0, width, height },
|
|
572
|
+
camera: hostFrame.camera,
|
|
573
|
+
hostFrame,
|
|
574
|
+
deckViewport
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
/** Derive parallel off-axis desktop eyes with one shared geographic anchor. */
|
|
578
|
+
makeStereoRenderViews({ width, height, interpupillaryDistance = DEFAULT_INTERPUPILLARY_DISTANCE }) {
|
|
579
|
+
return ["left-eye", "right-eye"].map((id, index) => {
|
|
580
|
+
const renderView = this.makeRenderView({ id, width, height });
|
|
581
|
+
const viewport = renderView.deckViewport;
|
|
582
|
+
const viewScale = Math.hypot(
|
|
583
|
+
viewport.viewMatrix[0],
|
|
584
|
+
viewport.viewMatrix[4],
|
|
585
|
+
viewport.viewMatrix[8]
|
|
586
|
+
);
|
|
587
|
+
const unitsPerXR = this.placement.type === "globe" ? viewScale * 256 / this.placement.radius : viewScale * viewport.distanceScales.unitsPerMeter[2] * (this.placement.type === "map" ? this.placement.metersPerXRUnit : 1);
|
|
588
|
+
const eyeOffset = (index === 0 ? -0.5 : 0.5) * interpupillaryDistance * unitsPerXR;
|
|
589
|
+
const anchor = viewport.projectPosition([
|
|
590
|
+
this.viewState.longitude,
|
|
591
|
+
this.viewState.latitude,
|
|
592
|
+
0
|
|
593
|
+
]);
|
|
594
|
+
const anchorInView = new Matrix42().copy(viewport.viewMatrix).transformAsPoint(anchor);
|
|
595
|
+
const anchorDistance = -anchorInView[2];
|
|
596
|
+
const convergence = anchorDistance > 0.01 ? anchorDistance : this.placement.type === "first-person" ? 100 * unitsPerXR : 0.01;
|
|
597
|
+
const viewMatrix = new Matrix42().translate([-eyeOffset, 0, 0]).multiplyRight(renderView.camera.view);
|
|
598
|
+
const projectionMatrix = new Matrix42().copy(viewport.projectionMatrix);
|
|
599
|
+
projectionMatrix[8] -= projectionMatrix[0] * eyeOffset / convergence;
|
|
600
|
+
renderView.camera = {
|
|
601
|
+
...renderView.camera,
|
|
602
|
+
view: viewMatrix,
|
|
603
|
+
projection: this.view.getXRProjectionMatrix({ projectionMatrix, viewMatrix }),
|
|
604
|
+
position: this.placement.type === "globe" ? getCameraPosition(viewMatrix) : renderView.camera.position
|
|
605
|
+
};
|
|
606
|
+
renderView.hostFrame = { ...renderView.hostFrame, camera: renderView.camera };
|
|
607
|
+
if (typeof this.view.getHostFrameForCamera === "function") {
|
|
608
|
+
renderView.hostFrame = this.view.getHostFrameForCamera(viewport, renderView.camera);
|
|
609
|
+
}
|
|
610
|
+
return renderView;
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
/** Compose native XR eye matrices with content placement. */
|
|
614
|
+
makeXRRenderViews({ frameState, placementMatrix }) {
|
|
615
|
+
const logicalViewport = requireViewport(this.view.makeViewport({
|
|
616
|
+
width: Math.max(...frameState.views.map((view) => view.viewport[0] + view.viewport[2])),
|
|
617
|
+
height: Math.max(...frameState.views.map((view) => view.viewport[1] + view.viewport[3])),
|
|
618
|
+
viewState: this.viewState
|
|
619
|
+
}));
|
|
620
|
+
return frameState.views.map((xrView) => {
|
|
621
|
+
const [x, y, width, height] = xrView.viewport;
|
|
622
|
+
const viewMatrix = new Matrix42().copy(xrView.viewMatrix).multiplyRight(placementMatrix);
|
|
623
|
+
const projectionMatrix = new Matrix42().copy(xrView.projectionMatrix);
|
|
624
|
+
const renderView = {
|
|
625
|
+
id: xrView.eye || `eye-${xrView.index}`,
|
|
626
|
+
viewport: { x, y, width, height },
|
|
627
|
+
camera: {
|
|
628
|
+
view: viewMatrix,
|
|
629
|
+
projection: this.view.getXRProjectionMatrix({ projectionMatrix, viewMatrix }),
|
|
630
|
+
position: this.placement.type === "globe" ? getCameraPosition(viewMatrix) : [0, 0, 0]
|
|
631
|
+
},
|
|
632
|
+
deckViewport: logicalViewport,
|
|
633
|
+
xrView,
|
|
634
|
+
view: this.view,
|
|
635
|
+
viewState: this.viewState
|
|
636
|
+
};
|
|
637
|
+
if (typeof this.view.getHostFrameForCamera === "function") {
|
|
638
|
+
renderView.hostFrame = this.view.getHostFrameForCamera(logicalViewport, renderView.camera, { width, height });
|
|
639
|
+
}
|
|
640
|
+
return renderView;
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
/** Prepare a presentation snapshot and renderer-independent host frame. */
|
|
644
|
+
createFrame({ width, height, frameState, mode = this.mode, interpupillaryDistance, tileResources }) {
|
|
645
|
+
const resolvedMode = resolveMode(mode, frameState);
|
|
646
|
+
this.updateController({ width, height }, resolvedMode);
|
|
647
|
+
const logicalViewport = this.view.makeViewport({ width, height, viewState: this.viewState });
|
|
648
|
+
if (!logicalViewport) throw new Error("The logical deck.gl view produced an empty viewport");
|
|
649
|
+
let renderViews;
|
|
650
|
+
if (resolvedMode === "immersive-vr" && frameState?.views?.length) {
|
|
651
|
+
const placementMatrix = createXRPlacementMatrix(this.placement, this.viewState);
|
|
652
|
+
renderViews = this.makeXRRenderViews({ frameState, placementMatrix });
|
|
653
|
+
} else if (resolvedMode === "stereo-preview") {
|
|
654
|
+
const eyeWidth = Math.floor(width / 2);
|
|
655
|
+
renderViews = this.makeStereoRenderViews({
|
|
656
|
+
width: eyeWidth,
|
|
657
|
+
height,
|
|
658
|
+
interpupillaryDistance
|
|
659
|
+
});
|
|
660
|
+
renderViews[1].viewport.x = eyeWidth;
|
|
661
|
+
} else {
|
|
662
|
+
renderViews = [this.makeRenderView({ id: "mono", width, height })];
|
|
663
|
+
}
|
|
664
|
+
return {
|
|
665
|
+
mode: resolvedMode,
|
|
666
|
+
logicalViewport,
|
|
667
|
+
renderViews,
|
|
668
|
+
hostFrame: { ...this.createHostFrame({ width, height, renderViews, frameState }), tileResources }
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
/** Union per-eye visibility while preserving camera-independent host metadata. */
|
|
672
|
+
createHostFrame({ width, height, renderViews, frameState }) {
|
|
673
|
+
const fields = renderViews.find((renderView) => renderView.hostFrame)?.hostFrame;
|
|
674
|
+
const fallback = this.view.getHostFrame(
|
|
675
|
+
requireViewport(this.view.makeViewport({ width, height, viewState: this.viewState }))
|
|
676
|
+
);
|
|
677
|
+
const frameFields = fields || fallback;
|
|
678
|
+
let projection = frameFields.projection;
|
|
679
|
+
if (this.placement.type === "globe" && frameState?.views?.length) {
|
|
680
|
+
projection = {
|
|
681
|
+
...frameFields.projection,
|
|
682
|
+
type: "globe",
|
|
683
|
+
visibleBounds: getXRGlobeVisibleBounds({
|
|
684
|
+
views: frameState.views,
|
|
685
|
+
placement: this.placement,
|
|
686
|
+
viewState: this.viewState
|
|
687
|
+
})
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
return {
|
|
691
|
+
viewport: { x: 0, y: 0, width, height },
|
|
692
|
+
geographicAnchor: frameFields.view,
|
|
693
|
+
projection,
|
|
694
|
+
renderViews: renderViews.map((renderView) => ({
|
|
695
|
+
...renderView,
|
|
696
|
+
// Keep native render-pass rectangles untouched, but HostFrame routes
|
|
697
|
+
// top-origin canvas pixels (desktop CSS, immersive framebuffer units).
|
|
698
|
+
viewport: renderView.xrView ? {
|
|
699
|
+
...renderView.viewport,
|
|
700
|
+
y: height - (renderView.viewport.y ?? 0) - renderView.viewport.height
|
|
701
|
+
} : renderView.viewport,
|
|
702
|
+
geographicAnchor: renderView.hostFrame?.view,
|
|
703
|
+
// Immersive globe bounds already describe the union in room space.
|
|
704
|
+
projection: frameState?.views?.length && this.placement.type === "globe" ? void 0 : renderView.hostFrame?.projection
|
|
705
|
+
})),
|
|
706
|
+
activeRenderViewId: renderViews[0].id,
|
|
707
|
+
tileBuffer: frameFields.tileBuffer,
|
|
708
|
+
globePreloadZoom: frameFields.globePreloadZoom
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
/** Apply navigation intents; non-navigation signals remain owned by the caller. */
|
|
712
|
+
dispatchInteractionIntent(intent) {
|
|
713
|
+
if (intent.type !== "navigate") return this.viewState;
|
|
714
|
+
const [horizontal = 0, vertical = 0] = intent.delta;
|
|
715
|
+
if (intent.action === "turn" || intent.action === "rotate") {
|
|
716
|
+
return this.setViewState({ bearing: (this.viewState.bearing || 0) + horizontal });
|
|
717
|
+
}
|
|
718
|
+
if (intent.action === "pitch") {
|
|
719
|
+
return this.setViewState({ pitch: (this.viewState.pitch || 0) + vertical });
|
|
720
|
+
}
|
|
721
|
+
if (intent.action === "zoom") {
|
|
722
|
+
return this.setViewState({ zoom: (this.viewState.zoom || 0) + vertical });
|
|
723
|
+
}
|
|
724
|
+
if (this.placement.type === "first-person") {
|
|
725
|
+
const position = this.placement.position || [0, 0, 0];
|
|
726
|
+
this.placement = {
|
|
727
|
+
...this.placement,
|
|
728
|
+
position: [position[0] + horizontal, position[1] + vertical, position[2]]
|
|
729
|
+
};
|
|
730
|
+
return this.viewState;
|
|
731
|
+
}
|
|
732
|
+
return this.setViewState({
|
|
733
|
+
longitude: (this.viewState.longitude || 0) + horizontal,
|
|
734
|
+
latitude: (this.viewState.latitude || 0) + vertical
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
};
|
|
738
|
+
var WebXRViewManager = class extends WebXRPresentation {
|
|
739
|
+
};
|
|
740
|
+
function resolveMode(mode, frameState) {
|
|
741
|
+
if (mode === "auto") return frameState?.views?.length ? "immersive-vr" : "mono";
|
|
742
|
+
if (mode === "immersive-vr" && !frameState?.views?.length) return "stereo-preview";
|
|
743
|
+
return mode;
|
|
744
|
+
}
|
|
745
|
+
function createDefaultPlacement(view, viewState) {
|
|
746
|
+
const anchor = [viewState.longitude || 0, viewState.latitude || 0, 0];
|
|
747
|
+
if (view.constructor.displayName === "WebXRGlobeView") {
|
|
748
|
+
return { type: "globe", anchor, pose: { position: [0, 1.35, -2.35] }, radius: 0.72 };
|
|
749
|
+
}
|
|
750
|
+
if (view.constructor.displayName === "WebXRFirstPersonView") {
|
|
751
|
+
return { type: "first-person", origin: anchor, position: [0, 0, 0], bearing: 0 };
|
|
752
|
+
}
|
|
753
|
+
return {
|
|
754
|
+
type: "map",
|
|
755
|
+
anchor,
|
|
756
|
+
pose: { position: [0, 0.72, -1.8] },
|
|
757
|
+
metersPerXRUnit: 2500,
|
|
758
|
+
surface: { type: "unbounded" }
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
function getCameraPosition(matrix) {
|
|
762
|
+
const position = [0, 0, 0];
|
|
763
|
+
new Matrix42(matrix).invert().transformAsPoint([0, 0, 0], position);
|
|
764
|
+
return position;
|
|
765
|
+
}
|
|
766
|
+
function requireViewport(viewport) {
|
|
767
|
+
if (!viewport) throw new Error("The logical deck.gl view produced an empty viewport");
|
|
768
|
+
return viewport;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// src/experimental/webxr/picking.ts
|
|
772
|
+
import { Matrix4 as Matrix43 } from "@math.gl/core";
|
|
773
|
+
|
|
774
|
+
// src/web_mercator_view_adapter.ts
|
|
775
|
+
import { PROJECTION_CONSTANTS as PROJECTION_CONSTANTS2 } from "@tangram.gl/tangram-renderer/core";
|
|
776
|
+
var DECK_TO_TANGRAM_ZOOM_OFFSET = 1;
|
|
777
|
+
var DECK_WORLD_SIZE = 512;
|
|
778
|
+
var TANGRAM_HALF_WORLD_METERS2 = Math.PI * PROJECTION_CONSTANTS2.mercatorRadius;
|
|
779
|
+
var WebMercatorViewAdapter = class _WebMercatorViewAdapter {
|
|
780
|
+
/**
|
|
781
|
+
* Checks that a deck viewport provides the public Web Mercator contract used
|
|
782
|
+
* by TangramLayer.
|
|
783
|
+
* @param viewport deck.gl viewport.
|
|
784
|
+
* @returns Validation error, or null when supported.
|
|
785
|
+
*/
|
|
786
|
+
static validateViewport(viewport) {
|
|
787
|
+
if (viewport?.isGeospatial === false) {
|
|
788
|
+
return new Error("a Web Mercator viewport is required");
|
|
789
|
+
}
|
|
790
|
+
if (_WebMercatorViewAdapter.validateGeographicAnchor(viewport, "Web Mercator viewport")) {
|
|
791
|
+
return new Error("a Web Mercator viewport is required");
|
|
792
|
+
}
|
|
793
|
+
return null;
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
796
|
+
* Validates the geographic anchor shared by all deck.gl view adapters.
|
|
797
|
+
* @param viewport deck.gl viewport.
|
|
798
|
+
* @param viewportName View name used in the error message.
|
|
799
|
+
* @returns Validation error, or null when the anchor is finite.
|
|
800
|
+
*/
|
|
801
|
+
static validateGeographicAnchor(viewport, viewportName = "viewport") {
|
|
802
|
+
if (!Number.isFinite(viewport?.longitude) || !Number.isFinite(viewport?.latitude) || !Number.isFinite(viewport?.zoom)) {
|
|
803
|
+
return new Error(`${viewportName} requires finite longitude, latitude, and zoom`);
|
|
804
|
+
}
|
|
805
|
+
return null;
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* Converts deck.gl camera matrices to Tangram camera matrices.
|
|
809
|
+
*
|
|
810
|
+
* Tangram tile models use absolute EPSG:3857 meters while deck matrices
|
|
811
|
+
* consume zoom-zero common coordinates. Altitude uses deck's
|
|
812
|
+
* latitude-dependent distance scale.
|
|
813
|
+
* @param viewport deck.gl WebMercatorViewport.
|
|
814
|
+
* @returns Tangram camera matrices and eye position.
|
|
815
|
+
*/
|
|
816
|
+
static getCameraFrame(viewport) {
|
|
817
|
+
const distanceScales = typeof viewport.getDistanceScales === "function" ? viewport.getDistanceScales() : viewport.distanceScales;
|
|
818
|
+
const unitsPerMeter = distanceScales && distanceScales.unitsPerMeter;
|
|
819
|
+
const viewMatrix = viewport.viewMatrix;
|
|
820
|
+
const projectionMatrix = viewport.projectionMatrix;
|
|
821
|
+
if (!viewMatrix || viewMatrix.length !== 16 || !projectionMatrix || projectionMatrix.length !== 16 || !unitsPerMeter || !Number.isFinite(unitsPerMeter[2])) {
|
|
822
|
+
throw new Error("deck viewport camera matrices and distance scales are required");
|
|
823
|
+
}
|
|
824
|
+
const xyScale = DECK_WORLD_SIZE / (TANGRAM_HALF_WORLD_METERS2 * 2);
|
|
825
|
+
const metersToCommon = new Float64Array(16);
|
|
826
|
+
metersToCommon[0] = xyScale;
|
|
827
|
+
metersToCommon[5] = xyScale;
|
|
828
|
+
metersToCommon[10] = unitsPerMeter[2];
|
|
829
|
+
metersToCommon[12] = DECK_WORLD_SIZE / 2;
|
|
830
|
+
metersToCommon[13] = DECK_WORLD_SIZE / 2;
|
|
831
|
+
metersToCommon[15] = 1;
|
|
832
|
+
return {
|
|
833
|
+
view: multiplyMatrices(viewMatrix, metersToCommon),
|
|
834
|
+
projection: new Float32Array(projectionMatrix),
|
|
835
|
+
// The view matrix places the camera at the origin in eye coordinates.
|
|
836
|
+
position: [0, 0, 0]
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
840
|
+
* Builds Tangram's external-camera frame for a deck.gl MapView.
|
|
841
|
+
* @param viewport deck.gl WebMercatorViewport.
|
|
842
|
+
* @param dimensions Render-target size.
|
|
843
|
+
* @returns Tangram HostFrame legacy shape.
|
|
844
|
+
*/
|
|
845
|
+
static getFrame(viewport, { width, height }) {
|
|
846
|
+
const longitude = viewport.longitude;
|
|
847
|
+
const latitude = viewport.latitude;
|
|
848
|
+
const zoom = viewport.zoom;
|
|
849
|
+
if (typeof longitude !== "number" || !Number.isFinite(longitude) || typeof latitude !== "number" || !Number.isFinite(latitude) || typeof zoom !== "number" || !Number.isFinite(zoom)) {
|
|
850
|
+
throw new Error("a Web Mercator viewport is required");
|
|
851
|
+
}
|
|
852
|
+
const pitch = Math.abs(viewport.pitch || 0) * Math.PI / 180;
|
|
853
|
+
return {
|
|
854
|
+
viewport: { width, height },
|
|
855
|
+
view: {
|
|
856
|
+
longitude,
|
|
857
|
+
latitude,
|
|
858
|
+
zoom: zoom + DECK_TO_TANGRAM_ZOOM_OFFSET
|
|
859
|
+
},
|
|
860
|
+
projection: { type: "web-mercator" },
|
|
861
|
+
camera: _WebMercatorViewAdapter.getCameraFrame(viewport),
|
|
862
|
+
tileBuffer: Math.min(4, Math.ceil(Math.tan(pitch) * (viewport.height || height) / 256))
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
};
|
|
866
|
+
function multiplyMatrices(left, right) {
|
|
867
|
+
const result = new Float64Array(16);
|
|
868
|
+
for (let column = 0; column < 4; column++) {
|
|
869
|
+
for (let row = 0; row < 4; row++) {
|
|
870
|
+
let value = 0;
|
|
871
|
+
for (let index = 0; index < 4; index++) {
|
|
872
|
+
value += left[index * 4 + row] * right[column * 4 + index];
|
|
873
|
+
}
|
|
874
|
+
result[column * 4 + row] = value;
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
return result;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// src/experimental/webxr/picking.ts
|
|
881
|
+
function pickXRSurface(options) {
|
|
882
|
+
const { pointer, placement, frame } = options;
|
|
883
|
+
const viewState = options.viewState || {
|
|
884
|
+
longitude: frame?.logicalViewport.longitude,
|
|
885
|
+
latitude: frame?.logicalViewport.latitude,
|
|
886
|
+
bearing: frame?.logicalViewport.bearing
|
|
887
|
+
};
|
|
888
|
+
let ray;
|
|
889
|
+
let farPoint;
|
|
890
|
+
let renderViewId;
|
|
891
|
+
if ("origin" in pointer) {
|
|
892
|
+
if (!isFiniteVector(pointer.origin) || !isFiniteVector(pointer.direction) || Math.hypot(...pointer.direction) === 0) return null;
|
|
893
|
+
const matrix = options.placementMatrix || createXRPlacementMatrix(placement, viewState);
|
|
894
|
+
if (!isInvertibleMatrix(matrix)) return null;
|
|
895
|
+
const magnitude = Math.hypot(...pointer.direction);
|
|
896
|
+
if (!Number.isFinite(magnitude)) return null;
|
|
897
|
+
ray = transformXRRayToContent({ origin: pointer.origin, direction: [
|
|
898
|
+
pointer.direction[0] / magnitude,
|
|
899
|
+
pointer.direction[1] / magnitude,
|
|
900
|
+
pointer.direction[2] / magnitude
|
|
901
|
+
] }, matrix);
|
|
902
|
+
} else {
|
|
903
|
+
if (!frame || !Number.isFinite(pointer.x) || !Number.isFinite(pointer.y)) return null;
|
|
904
|
+
let camera;
|
|
905
|
+
let viewport;
|
|
906
|
+
if (pointer.eye === "center") {
|
|
907
|
+
if (frame.mode === "immersive-vr") return null;
|
|
908
|
+
viewport = frame.logicalViewport;
|
|
909
|
+
camera = placement.type === "globe" ? { view: viewport.viewMatrix, projection: new Matrix43().copy(viewport.projectionMatrix).multiplyRight(viewport.viewMatrix) } : WebMercatorViewAdapter.getCameraFrame(viewport);
|
|
910
|
+
} else {
|
|
911
|
+
const eye = frame.renderViews.find((view) => view.viewport && containsPixel(getScreenViewport(view, frame), pointer.x, pointer.y) && (!pointer.eye || view.id === `${pointer.eye}-eye` || view.xrView?.eye === pointer.eye));
|
|
912
|
+
if (!eye?.camera) return null;
|
|
913
|
+
camera = eye.camera;
|
|
914
|
+
viewport = getScreenViewport(eye, frame);
|
|
915
|
+
renderViewId = eye.id;
|
|
916
|
+
}
|
|
917
|
+
if (!viewport || !containsPixel(viewport, pointer.x, pointer.y) || !isInvertibleMatrix(camera.projection) || !isInvertibleMatrix(camera.view)) return null;
|
|
918
|
+
const clipMatrix = new Matrix43().copy(camera.projection);
|
|
919
|
+
if (placement.type !== "globe") clipMatrix.multiplyRight(camera.view);
|
|
920
|
+
if (!isInvertibleMatrix(clipMatrix)) return null;
|
|
921
|
+
const inverse = clipMatrix.invert();
|
|
922
|
+
const horizontal = 2 * (pointer.x - (viewport.x ?? 0)) / viewport.width - 1;
|
|
923
|
+
const vertical = 1 - 2 * (pointer.y - (viewport.y ?? 0)) / viewport.height;
|
|
924
|
+
const nearPoint = unprojectPoint(inverse, horizontal, vertical, -1);
|
|
925
|
+
farPoint = unprojectPoint(inverse, horizontal, vertical, 1) || void 0;
|
|
926
|
+
if (!nearPoint || !farPoint) return null;
|
|
927
|
+
ray = { origin: nearPoint, direction: [
|
|
928
|
+
farPoint[0] - nearPoint[0],
|
|
929
|
+
farPoint[1] - nearPoint[1],
|
|
930
|
+
farPoint[2] - nearPoint[2]
|
|
931
|
+
] };
|
|
932
|
+
}
|
|
933
|
+
const length = Math.hypot(...ray.direction);
|
|
934
|
+
if (!isFiniteVector(ray.origin) || !Number.isFinite(length) || length === 0) return null;
|
|
935
|
+
const direction = [ray.direction[0] / length, ray.direction[1] / length, ray.direction[2] / length];
|
|
936
|
+
if (options.terrain && options.terrain.projection !== (placement.type === "globe" ? "globe" : "web-mercator")) return null;
|
|
937
|
+
const terrainHit = options.terrain?.intersectRay({ origin: ray.origin, direction }, farPoint ? length : Infinity);
|
|
938
|
+
const hit = options.terrain ? terrainHit ? {
|
|
939
|
+
position: terrainHit.position,
|
|
940
|
+
coordinate: terrainHit.coordinate,
|
|
941
|
+
terrain: {
|
|
942
|
+
triangleIndex: terrainHit.triangleIndex,
|
|
943
|
+
normal: terrainHit.normal,
|
|
944
|
+
barycentric: terrainHit.barycentric
|
|
945
|
+
}
|
|
946
|
+
} : null : intersectContentSurface({ origin: ray.origin, direction }, placement, viewState);
|
|
947
|
+
if (!hit || !isFiniteVector(hit.position)) return null;
|
|
948
|
+
if (!isContentPointWithinMapBounds(hit.position, placement, viewState)) return null;
|
|
949
|
+
if (farPoint && Math.hypot(
|
|
950
|
+
hit.position[0] - ray.origin[0],
|
|
951
|
+
hit.position[1] - ray.origin[1],
|
|
952
|
+
hit.position[2] - ray.origin[2]
|
|
953
|
+
) > length * (1 + 1e-9)) return null;
|
|
954
|
+
return renderViewId ? { ...hit, renderViewId } : hit;
|
|
955
|
+
}
|
|
956
|
+
function getScreenViewport(view, frame) {
|
|
957
|
+
const viewport = view.viewport;
|
|
958
|
+
return view.xrView ? {
|
|
959
|
+
...viewport,
|
|
960
|
+
y: frame.hostFrame.viewport.height - (viewport.y ?? 0) - viewport.height
|
|
961
|
+
} : viewport;
|
|
962
|
+
}
|
|
963
|
+
function containsPixel(viewport, x, y) {
|
|
964
|
+
const left = viewport.x ?? 0;
|
|
965
|
+
const top = viewport.y ?? 0;
|
|
966
|
+
return Number.isFinite(viewport.width) && Number.isFinite(viewport.height) && x >= left && y >= top && x < left + viewport.width && y < top + viewport.height;
|
|
967
|
+
}
|
|
968
|
+
function isInvertibleMatrix(matrix) {
|
|
969
|
+
if (matrix.length !== 16 || !Array.from(matrix).every(Number.isFinite)) return false;
|
|
970
|
+
const determinant = new Matrix43(Array.from(matrix)).determinant();
|
|
971
|
+
return Number.isFinite(determinant) && determinant !== 0;
|
|
972
|
+
}
|
|
973
|
+
function unprojectPoint(matrix, x, y, z) {
|
|
974
|
+
const point = matrix.transform([x, y, z, 1]);
|
|
975
|
+
if (!point.every(Number.isFinite) || point[3] === 0) return null;
|
|
976
|
+
return [point[0] / point[3], point[1] / point[3], point[2] / point[3]];
|
|
977
|
+
}
|
|
978
|
+
function isFiniteVector(vector) {
|
|
979
|
+
return vector.length === 3 && vector.every(Number.isFinite);
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
// src/experimental/webxr/grabbing.ts
|
|
983
|
+
import { Matrix4 as Matrix44, Quaternion } from "@math.gl/core";
|
|
984
|
+
var WebXRSurfaceGrabber = class {
|
|
985
|
+
grab = null;
|
|
986
|
+
/** Whether one input currently owns a grab, including paused off-surface movement. */
|
|
987
|
+
isGrabbing() {
|
|
988
|
+
return this.grab !== null;
|
|
989
|
+
}
|
|
990
|
+
/** End a gesture without rolling back the application's last accepted placement. */
|
|
991
|
+
reset() {
|
|
992
|
+
this.grab = null;
|
|
993
|
+
}
|
|
994
|
+
/**
|
|
995
|
+
* Consume grab/hover/release/cancel intents and return a new placement on movement.
|
|
996
|
+
* Supply the actual rendered placement when starting; subsequent movement uses
|
|
997
|
+
* that frozen baseline, avoiding feedback as the application moves the object.
|
|
998
|
+
* Missing input IDs share a single implicit owner. Select-button releases never
|
|
999
|
+
* terminate a squeeze grab. First-person ground and screen grabs are unsupported.
|
|
1000
|
+
*/
|
|
1001
|
+
dispatchInteractionIntent(intent, context) {
|
|
1002
|
+
if (intent.type !== "point") return null;
|
|
1003
|
+
const inputId = intent.inputId ?? "pointer";
|
|
1004
|
+
if (this.grab && this.grab.inputId !== inputId) return null;
|
|
1005
|
+
if (intent.action === "cancel" || intent.action === "release" && intent.button !== "select") {
|
|
1006
|
+
this.reset();
|
|
1007
|
+
return null;
|
|
1008
|
+
}
|
|
1009
|
+
if (!("origin" in intent.pointer)) return null;
|
|
1010
|
+
if (intent.action === "grab" && !this.grab) {
|
|
1011
|
+
this.beginGrab(inputId, intent.pointer, context);
|
|
1012
|
+
return null;
|
|
1013
|
+
}
|
|
1014
|
+
if (intent.action !== "hover" || !this.grab) return null;
|
|
1015
|
+
return this.moveGrab(intent.pointer);
|
|
1016
|
+
}
|
|
1017
|
+
/** Freeze geographic state, pose and the actual room matrix after a valid surface hit. */
|
|
1018
|
+
beginGrab(inputId, pointer, context) {
|
|
1019
|
+
if (context.placement.type === "first-person") return;
|
|
1020
|
+
const hit = pickXRSurface({ ...context, pointer });
|
|
1021
|
+
if (!hit) return;
|
|
1022
|
+
const placement = context.placement;
|
|
1023
|
+
const position = placement.pose?.position ?? [0, 0, 0];
|
|
1024
|
+
const orientation = placement.pose?.orientation ?? [0, 0, 0, 1];
|
|
1025
|
+
const scale = placement.type === "map" ? placement.metersPerXRUnit : placement.radius;
|
|
1026
|
+
if (!position.every(Number.isFinite) || !orientation.every(Number.isFinite) || !(Math.hypot(...orientation) > 0) || !Number.isFinite(scale) || scale <= 0) return;
|
|
1027
|
+
const snapshot = copyPlacement(placement);
|
|
1028
|
+
const viewState = { ...context.viewState ?? {
|
|
1029
|
+
longitude: context.frame?.logicalViewport.longitude,
|
|
1030
|
+
latitude: context.frame?.logicalViewport.latitude
|
|
1031
|
+
} };
|
|
1032
|
+
const matrix = new Matrix44().copy(context.placementMatrix ?? createXRPlacementMatrix(placement, viewState));
|
|
1033
|
+
const normal = normalize([
|
|
1034
|
+
matrix[1] * matrix[6] - matrix[2] * matrix[5],
|
|
1035
|
+
matrix[2] * matrix[4] - matrix[0] * matrix[6],
|
|
1036
|
+
matrix[0] * matrix[5] - matrix[1] * matrix[4]
|
|
1037
|
+
]);
|
|
1038
|
+
if (!normal || Math.abs(matrix[3]) + Math.abs(matrix[7]) + Math.abs(matrix[11]) > 1e-9 || Math.abs(matrix[15] - 1) > 1e-9) return;
|
|
1039
|
+
this.grab = {
|
|
1040
|
+
inputId,
|
|
1041
|
+
placement: snapshot,
|
|
1042
|
+
context: { ...context, placement: snapshot, viewState, placementMatrix: matrix },
|
|
1043
|
+
matrix,
|
|
1044
|
+
hit: transformPoint2(matrix, hit.position),
|
|
1045
|
+
normal,
|
|
1046
|
+
center: transformPoint2(matrix, [0, 0, 0])
|
|
1047
|
+
};
|
|
1048
|
+
}
|
|
1049
|
+
/** Resolve movement against the start plane or sphere, never the already moved object. */
|
|
1050
|
+
moveGrab(pointer) {
|
|
1051
|
+
const grab = this.grab;
|
|
1052
|
+
if (!grab || !pointer.origin.every(Number.isFinite)) return null;
|
|
1053
|
+
const direction = normalize(pointer.direction);
|
|
1054
|
+
if (!direction) return null;
|
|
1055
|
+
if (grab.placement.type === "map") {
|
|
1056
|
+
const denominator = dot(direction, grab.normal);
|
|
1057
|
+
if (Math.abs(denominator) < 1e-7) return null;
|
|
1058
|
+
const distance = dot(subtract(grab.hit, pointer.origin), grab.normal) / denominator;
|
|
1059
|
+
if (!Number.isFinite(distance) || distance < 0) return null;
|
|
1060
|
+
const hit2 = [
|
|
1061
|
+
pointer.origin[0] + direction[0] * distance,
|
|
1062
|
+
pointer.origin[1] + direction[1] * distance,
|
|
1063
|
+
pointer.origin[2] + direction[2] * distance
|
|
1064
|
+
];
|
|
1065
|
+
const position = grab.placement.pose?.position ?? [0, 0, 0];
|
|
1066
|
+
const nextPosition = [
|
|
1067
|
+
position[0] + hit2[0] - grab.hit[0],
|
|
1068
|
+
position[1] + hit2[1] - grab.hit[1],
|
|
1069
|
+
position[2] + hit2[2] - grab.hit[2]
|
|
1070
|
+
];
|
|
1071
|
+
if (!nextPosition.every(Number.isFinite)) return null;
|
|
1072
|
+
const placement2 = copyPlacement(grab.placement);
|
|
1073
|
+
return { ...placement2, pose: {
|
|
1074
|
+
...placement2.pose,
|
|
1075
|
+
position: nextPosition
|
|
1076
|
+
} };
|
|
1077
|
+
}
|
|
1078
|
+
const hit = pickXRSurface({ ...grab.context, pointer });
|
|
1079
|
+
if (!hit) return null;
|
|
1080
|
+
const start = normalize(subtract(grab.hit, grab.center));
|
|
1081
|
+
const end = normalize(subtract(transformPoint2(grab.matrix, hit.position), grab.center));
|
|
1082
|
+
if (!start || !end) return null;
|
|
1083
|
+
const rotation = new Quaternion().rotationTo([...start], [...end]);
|
|
1084
|
+
const orientation = new Quaternion().copy(grab.placement.pose?.orientation ?? [0, 0, 0, 1]).multiplyLeft(rotation).normalize();
|
|
1085
|
+
const placement = copyPlacement(grab.placement);
|
|
1086
|
+
return { ...placement, pose: {
|
|
1087
|
+
...placement.pose,
|
|
1088
|
+
orientation: [orientation[0], orientation[1], orientation[2], orientation[3]]
|
|
1089
|
+
} };
|
|
1090
|
+
}
|
|
1091
|
+
};
|
|
1092
|
+
function copyPlacement(placement) {
|
|
1093
|
+
const position = placement.pose?.position ?? [0, 0, 0];
|
|
1094
|
+
const orientation = placement.pose?.orientation ?? [0, 0, 0, 1];
|
|
1095
|
+
const pose = {
|
|
1096
|
+
position: [position[0], position[1], position[2]],
|
|
1097
|
+
orientation: [orientation[0], orientation[1], orientation[2], orientation[3]]
|
|
1098
|
+
};
|
|
1099
|
+
return placement.type === "map" ? {
|
|
1100
|
+
...placement,
|
|
1101
|
+
anchor: [...placement.anchor],
|
|
1102
|
+
pose,
|
|
1103
|
+
...placement.surface ? { surface: { ...placement.surface } } : {}
|
|
1104
|
+
} : { ...placement, anchor: [...placement.anchor], pose };
|
|
1105
|
+
}
|
|
1106
|
+
function transformPoint2(matrix, point) {
|
|
1107
|
+
const result = matrix.transform([point[0], point[1], point[2], 1]);
|
|
1108
|
+
return [result[0], result[1], result[2]];
|
|
1109
|
+
}
|
|
1110
|
+
function normalize(vector) {
|
|
1111
|
+
const length = Math.hypot(...vector);
|
|
1112
|
+
return length > 0 && Number.isFinite(length) ? [vector[0] / length, vector[1] / length, vector[2] / length] : null;
|
|
1113
|
+
}
|
|
1114
|
+
function subtract(first, second) {
|
|
1115
|
+
return [first[0] - second[0], first[1] - second[1], first[2] - second[2]];
|
|
1116
|
+
}
|
|
1117
|
+
function dot(first, second) {
|
|
1118
|
+
return first[0] * second[0] + first[1] * second[1] + first[2] * second[2];
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
// src/experimental/webxr/views.ts
|
|
1122
|
+
import {
|
|
1123
|
+
_GlobeController as GlobeController,
|
|
1124
|
+
FirstPersonView,
|
|
1125
|
+
FirstPersonController,
|
|
1126
|
+
MapController,
|
|
1127
|
+
MapView,
|
|
1128
|
+
_GlobeView as GlobeView
|
|
1129
|
+
} from "@deck.gl/core";
|
|
1130
|
+
|
|
1131
|
+
// src/first_person_view_adapter.ts
|
|
1132
|
+
import { Matrix4 as Matrix45 } from "@math.gl/core";
|
|
1133
|
+
import { calculatePlanarVolumeBounds } from "@tangram.gl/tangram-renderer/core";
|
|
1134
|
+
var DECK_WORLD_SIZE2 = 512;
|
|
1135
|
+
var HALF_WORLD_METERS = 20037508342789244e-9;
|
|
1136
|
+
var TILE_SIZE = 256;
|
|
1137
|
+
var MAX_MERCATOR_LATITUDE2 = 85.05112878;
|
|
1138
|
+
var DEFAULT_GROUND_EXTENT = 2e4;
|
|
1139
|
+
var FirstPersonViewAdapter = class {
|
|
1140
|
+
/** Converts the finite frustum/ground intersection without changing camera matrices. */
|
|
1141
|
+
static getFrame = getFrame;
|
|
1142
|
+
};
|
|
1143
|
+
function getFrame(viewport, options = {}) {
|
|
1144
|
+
return getFirstPersonFrameForCamera(viewport, WebMercatorViewAdapter.getCameraFrame(viewport), options);
|
|
1145
|
+
}
|
|
1146
|
+
function getFirstPersonFrameForCamera(viewport, camera, options = {}) {
|
|
1147
|
+
const width = options.width ?? viewport.width;
|
|
1148
|
+
const height = options.height ?? viewport.height;
|
|
1149
|
+
const maxGroundExtent = options.maxGroundExtent ?? DEFAULT_GROUND_EXTENT;
|
|
1150
|
+
if (!Number.isFinite(width) || width <= 0 || !Number.isFinite(height) || height <= 0) {
|
|
1151
|
+
throw new Error("FirstPersonViewport requires positive width and height");
|
|
1152
|
+
}
|
|
1153
|
+
if (!Number.isFinite(maxGroundExtent) || maxGroundExtent <= 0) {
|
|
1154
|
+
throw new Error("FirstPersonViewport maxGroundExtent must be finite and positive");
|
|
1155
|
+
}
|
|
1156
|
+
const { longitude, latitude } = viewport;
|
|
1157
|
+
const unprojectFlat = viewport.unprojectFlat;
|
|
1158
|
+
if (typeof longitude !== "number" || !Number.isFinite(longitude) || typeof latitude !== "number" || !Number.isFinite(latitude) || typeof unprojectFlat !== "function") {
|
|
1159
|
+
throw new Error("FirstPersonViewport geographic center and ground projection method are required");
|
|
1160
|
+
}
|
|
1161
|
+
const inverseView = new Matrix45().copy(camera.view);
|
|
1162
|
+
if (!Number.isFinite(inverseView.determinant()) || inverseView.determinant() === 0) {
|
|
1163
|
+
throw new Error("FirstPersonViewport camera is singular");
|
|
1164
|
+
}
|
|
1165
|
+
const eye = inverseView.invert().transformAsPoint([0, 0, 0]);
|
|
1166
|
+
if (!eye.every(Number.isFinite)) throw new Error("FirstPersonViewport eye position is invalid");
|
|
1167
|
+
const latitudeScale = Math.cos(Math.max(
|
|
1168
|
+
-MAX_MERCATOR_LATITUDE2,
|
|
1169
|
+
Math.min(MAX_MERCATOR_LATITUDE2, latitude)
|
|
1170
|
+
) * Math.PI / 180);
|
|
1171
|
+
const extent = maxGroundExtent / latitudeScale;
|
|
1172
|
+
const bounds = calculatePlanarVolumeBounds(camera, options.elevationRange ?? [0, 0], {
|
|
1173
|
+
sw: { x: eye[0] - extent, y: eye[1] - extent },
|
|
1174
|
+
ne: { x: eye[0] + extent, y: eye[1] + extent }
|
|
1175
|
+
});
|
|
1176
|
+
const xyScale = DECK_WORLD_SIZE2 / (HALF_WORLD_METERS * 2);
|
|
1177
|
+
const toGeographic = (x, y) => unprojectFlat.call(viewport, [
|
|
1178
|
+
x * xyScale + DECK_WORLD_SIZE2 / 2,
|
|
1179
|
+
y * xyScale + DECK_WORLD_SIZE2 / 2
|
|
1180
|
+
]);
|
|
1181
|
+
const center = bounds ? toGeographic(
|
|
1182
|
+
(bounds.sw.x + bounds.ne.x) / 2,
|
|
1183
|
+
(bounds.sw.y + bounds.ne.y) / 2
|
|
1184
|
+
) : [longitude, latitude];
|
|
1185
|
+
const metersPerPixel = bounds ? Math.max(
|
|
1186
|
+
(bounds.ne.x - bounds.sw.x) / width,
|
|
1187
|
+
(bounds.ne.y - bounds.sw.y) / height
|
|
1188
|
+
) : extent * 2 / Math.min(width, height);
|
|
1189
|
+
let visibleBounds = null;
|
|
1190
|
+
if (bounds) {
|
|
1191
|
+
const southwest = toGeographic(bounds.sw.x, bounds.sw.y);
|
|
1192
|
+
const northeast = toGeographic(bounds.ne.x, bounds.ne.y);
|
|
1193
|
+
visibleBounds = [southwest[0], southwest[1], northeast[0], northeast[1]];
|
|
1194
|
+
}
|
|
1195
|
+
if (!center.every(Number.isFinite) || !Number.isFinite(metersPerPixel) || metersPerPixel <= 0 || visibleBounds && !visibleBounds.every(Number.isFinite)) {
|
|
1196
|
+
throw new Error("FirstPersonViewport ground footprint is invalid");
|
|
1197
|
+
}
|
|
1198
|
+
return {
|
|
1199
|
+
viewport: { width, height },
|
|
1200
|
+
view: {
|
|
1201
|
+
longitude: center[0],
|
|
1202
|
+
latitude: center[1],
|
|
1203
|
+
altitude: eye[2],
|
|
1204
|
+
zoom: Math.max(0, Math.log2(HALF_WORLD_METERS * 2 / (TILE_SIZE * metersPerPixel)))
|
|
1205
|
+
},
|
|
1206
|
+
projection: { type: "web-mercator", visibleBounds },
|
|
1207
|
+
camera,
|
|
1208
|
+
tileBuffer: 1
|
|
1209
|
+
};
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
// src/globe_view_adapter.ts
|
|
1213
|
+
var DECK_TO_TANGRAM_ZOOM_OFFSET2 = 1;
|
|
1214
|
+
var GlobeViewAdapter = class {
|
|
1215
|
+
/** Converts globe common-space matrices and visible geographic bounds. */
|
|
1216
|
+
static getFrame = getFrame2;
|
|
1217
|
+
};
|
|
1218
|
+
function getFrame2(viewport, options = {}) {
|
|
1219
|
+
if (options.maxElevation !== void 0 && (!Number.isFinite(options.maxElevation) || options.maxElevation < 0)) {
|
|
1220
|
+
throw new Error("GlobeView maxElevation must be a finite non-negative number");
|
|
1221
|
+
}
|
|
1222
|
+
if (!viewport || !Number.isFinite(viewport.width) || !Number.isFinite(viewport.height) || !viewport.viewMatrix || viewport.viewMatrix.length !== 16 || !viewport.projectionMatrix || viewport.projectionMatrix.length !== 16 || !viewport.cameraPosition || viewport.cameraPosition.length !== 3 || !Array.from(viewport.cameraPosition).every(Number.isFinite) || typeof viewport.getBounds !== "function") {
|
|
1223
|
+
throw new Error("deck GlobeViewport matrices, camera position, size, and visible bounds are required");
|
|
1224
|
+
}
|
|
1225
|
+
const visibleBounds = options.visibleBounds ?? viewport.getBounds({ z: 0 });
|
|
1226
|
+
if (!Array.isArray(visibleBounds) || visibleBounds.length !== 4 || visibleBounds.some((value) => !Number.isFinite(value))) {
|
|
1227
|
+
throw new Error("deck GlobeViewport must provide finite geographic bounds");
|
|
1228
|
+
}
|
|
1229
|
+
return {
|
|
1230
|
+
viewport: { width: viewport.width, height: viewport.height },
|
|
1231
|
+
view: {
|
|
1232
|
+
longitude: viewport.longitude,
|
|
1233
|
+
latitude: viewport.latitude,
|
|
1234
|
+
zoom: Math.max(0, viewport.zoom + DECK_TO_TANGRAM_ZOOM_OFFSET2)
|
|
1235
|
+
},
|
|
1236
|
+
projection: {
|
|
1237
|
+
type: "globe",
|
|
1238
|
+
visibleBounds: [visibleBounds[0], visibleBounds[1], visibleBounds[2], visibleBounds[3]],
|
|
1239
|
+
...options.maxElevation === void 0 ? {} : { maxElevation: options.maxElevation }
|
|
1240
|
+
},
|
|
1241
|
+
camera: {
|
|
1242
|
+
view: new Float64Array(viewport.viewMatrix),
|
|
1243
|
+
projection: new Float32Array(
|
|
1244
|
+
multiplyMatrices2(viewport.projectionMatrix, viewport.viewMatrix)
|
|
1245
|
+
),
|
|
1246
|
+
position: [viewport.cameraPosition[0], viewport.cameraPosition[1], viewport.cameraPosition[2]]
|
|
1247
|
+
},
|
|
1248
|
+
tileBuffer: 0,
|
|
1249
|
+
globePreloadZoom: options.preloadZoom
|
|
1250
|
+
};
|
|
1251
|
+
}
|
|
1252
|
+
function multiplyMatrices2(left, right) {
|
|
1253
|
+
const result = new Float64Array(16);
|
|
1254
|
+
for (let column = 0; column < 4; column++) {
|
|
1255
|
+
for (let row = 0; row < 4; row++) {
|
|
1256
|
+
let value = 0;
|
|
1257
|
+
for (let index = 0; index < 4; index++) {
|
|
1258
|
+
value += left[index * 4 + row] * right[column * 4 + index];
|
|
1259
|
+
}
|
|
1260
|
+
result[column * 4 + row] = value;
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
return result;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
// src/experimental/webxr/views.ts
|
|
1267
|
+
var WebXRMapController = class extends MapController {
|
|
1268
|
+
_onMultiPanStart(event) {
|
|
1269
|
+
const multiTouchDrag = this.multiTouchDrag;
|
|
1270
|
+
if (event.pointerType === "trackpad") {
|
|
1271
|
+
this.multiTouchDrag = "pan";
|
|
1272
|
+
}
|
|
1273
|
+
const handled = super._onMultiPanStart(event);
|
|
1274
|
+
this.multiTouchDrag = multiTouchDrag;
|
|
1275
|
+
return handled;
|
|
1276
|
+
}
|
|
1277
|
+
};
|
|
1278
|
+
var WebXRFirstPersonController = class extends FirstPersonController {
|
|
1279
|
+
_onMultiPanStart(event) {
|
|
1280
|
+
const multiTouchDrag = this.multiTouchDrag;
|
|
1281
|
+
if (event.pointerType === "trackpad") {
|
|
1282
|
+
this.multiTouchDrag = "pan";
|
|
1283
|
+
}
|
|
1284
|
+
const handled = super._onMultiPanStart(event);
|
|
1285
|
+
this.multiTouchDrag = multiTouchDrag;
|
|
1286
|
+
return handled;
|
|
1287
|
+
}
|
|
1288
|
+
};
|
|
1289
|
+
var WebXRGlobeController = class extends GlobeController {
|
|
1290
|
+
_onMultiPanStart(event) {
|
|
1291
|
+
const multiTouchDrag = this.multiTouchDrag;
|
|
1292
|
+
if (event.pointerType === "trackpad") {
|
|
1293
|
+
this.multiTouchDrag = "pan";
|
|
1294
|
+
}
|
|
1295
|
+
const handled = super._onMultiPanStart(event);
|
|
1296
|
+
this.multiTouchDrag = multiTouchDrag;
|
|
1297
|
+
return handled;
|
|
1298
|
+
}
|
|
1299
|
+
};
|
|
1300
|
+
var WebXRMapView = class extends MapView {
|
|
1301
|
+
static displayName = "WebXRMapView";
|
|
1302
|
+
makeEyeViewport({ width, height, viewState, eyeOffset = 0 }) {
|
|
1303
|
+
const position = viewState.position || [0, 0, 0];
|
|
1304
|
+
return this.makeViewport({
|
|
1305
|
+
width,
|
|
1306
|
+
height,
|
|
1307
|
+
viewState: { ...viewState, position: [position[0] + eyeOffset, position[1], position[2]] }
|
|
1308
|
+
});
|
|
1309
|
+
}
|
|
1310
|
+
getHostFrame(viewport) {
|
|
1311
|
+
return {
|
|
1312
|
+
view: {
|
|
1313
|
+
longitude: viewport.longitude,
|
|
1314
|
+
latitude: viewport.latitude,
|
|
1315
|
+
zoom: viewport.zoom + 1
|
|
1316
|
+
},
|
|
1317
|
+
projection: { type: "web-mercator" },
|
|
1318
|
+
camera: WebMercatorViewAdapter.getCameraFrame(viewport),
|
|
1319
|
+
tileBuffer: Math.min(
|
|
1320
|
+
4,
|
|
1321
|
+
Math.ceil(
|
|
1322
|
+
Math.tan(Math.abs(viewport.pitch || 0) * Math.PI / 180) * viewport.height / 256
|
|
1323
|
+
)
|
|
1324
|
+
)
|
|
1325
|
+
};
|
|
1326
|
+
}
|
|
1327
|
+
getXRProjectionMatrix({ projectionMatrix }) {
|
|
1328
|
+
return projectionMatrix;
|
|
1329
|
+
}
|
|
1330
|
+
};
|
|
1331
|
+
var WebXRFirstPersonView = class extends FirstPersonView {
|
|
1332
|
+
static displayName = "WebXRFirstPersonView";
|
|
1333
|
+
/** Configure the logical first-person view and optional ground extent. */
|
|
1334
|
+
constructor(props = {}) {
|
|
1335
|
+
super(props);
|
|
1336
|
+
}
|
|
1337
|
+
makeEyeViewport({ width, height, viewState, eyeOffset = 0 }) {
|
|
1338
|
+
const position = viewState.position || [0, 0, 0];
|
|
1339
|
+
return this.makeViewport({
|
|
1340
|
+
width,
|
|
1341
|
+
height,
|
|
1342
|
+
viewState: { ...viewState, position: [position[0] + eyeOffset, position[1], position[2]] }
|
|
1343
|
+
});
|
|
1344
|
+
}
|
|
1345
|
+
getHostFrame(viewport) {
|
|
1346
|
+
const frame = FirstPersonViewAdapter.getFrame(viewport, {
|
|
1347
|
+
maxGroundExtent: this.props.firstPersonMaxGroundExtent ?? void 0,
|
|
1348
|
+
elevationRange: this.props.firstPersonElevationRange
|
|
1349
|
+
});
|
|
1350
|
+
return {
|
|
1351
|
+
view: frame.view,
|
|
1352
|
+
projection: frame.projection,
|
|
1353
|
+
camera: frame.camera,
|
|
1354
|
+
tileBuffer: frame.tileBuffer
|
|
1355
|
+
};
|
|
1356
|
+
}
|
|
1357
|
+
/** Recomputes ground visibility after stereo or immersive camera transforms. */
|
|
1358
|
+
getHostFrameForCamera(viewport, camera, options = {}) {
|
|
1359
|
+
return getFirstPersonFrameForCamera(viewport, camera, {
|
|
1360
|
+
...options,
|
|
1361
|
+
maxGroundExtent: this.props.firstPersonMaxGroundExtent ?? void 0,
|
|
1362
|
+
elevationRange: this.props.firstPersonElevationRange
|
|
1363
|
+
});
|
|
1364
|
+
}
|
|
1365
|
+
getXRProjectionMatrix({ projectionMatrix }) {
|
|
1366
|
+
return projectionMatrix;
|
|
1367
|
+
}
|
|
1368
|
+
};
|
|
1369
|
+
var WebXRGlobeView = class extends GlobeView {
|
|
1370
|
+
static displayName = "WebXRGlobeView";
|
|
1371
|
+
/** Configure a globe view and its optional globally resident loading fallback. */
|
|
1372
|
+
constructor(props = {}) {
|
|
1373
|
+
super(props);
|
|
1374
|
+
}
|
|
1375
|
+
makeEyeViewport({ width, height, viewState, eyeOffset = 0 }) {
|
|
1376
|
+
return this.makeViewport({
|
|
1377
|
+
width,
|
|
1378
|
+
height,
|
|
1379
|
+
viewState: { ...viewState, longitude: viewState.longitude + eyeOffset * 0.06 }
|
|
1380
|
+
});
|
|
1381
|
+
}
|
|
1382
|
+
getHostFrame(viewport) {
|
|
1383
|
+
const frame = GlobeViewAdapter.getFrame(viewport, {
|
|
1384
|
+
maxElevation: this.props.globeMaxElevation ?? void 0,
|
|
1385
|
+
preloadZoom: this.props.globePreloadZoom ?? void 0
|
|
1386
|
+
});
|
|
1387
|
+
return {
|
|
1388
|
+
view: frame.view,
|
|
1389
|
+
projection: frame.projection,
|
|
1390
|
+
camera: frame.camera,
|
|
1391
|
+
tileBuffer: frame.tileBuffer,
|
|
1392
|
+
globePreloadZoom: frame.globePreloadZoom
|
|
1393
|
+
};
|
|
1394
|
+
}
|
|
1395
|
+
getXRProjectionMatrix({ projectionMatrix, viewMatrix }) {
|
|
1396
|
+
return projectionMatrix.clone().multiplyRight(viewMatrix);
|
|
1397
|
+
}
|
|
1398
|
+
};
|
|
1399
|
+
export {
|
|
1400
|
+
DEFAULT_INTERPUPILLARY_DISTANCE,
|
|
1401
|
+
WebXRFirstPersonController,
|
|
1402
|
+
WebXRFirstPersonView,
|
|
1403
|
+
WebXRGlobeController,
|
|
1404
|
+
WebXRGlobeView,
|
|
1405
|
+
WebXRInputAdapter,
|
|
1406
|
+
WebXRMapController,
|
|
1407
|
+
WebXRMapView,
|
|
1408
|
+
WebXRPresentation,
|
|
1409
|
+
WebXRSurfaceGrabber,
|
|
1410
|
+
WebXRViewManager,
|
|
1411
|
+
createXRPlacementMatrix,
|
|
1412
|
+
createXRPoseMatrix,
|
|
1413
|
+
getXRGlobeVisibleBounds,
|
|
1414
|
+
intersectXRGlobe,
|
|
1415
|
+
intersectXRMap,
|
|
1416
|
+
longitudeLatitudeToMeters,
|
|
1417
|
+
metersToLongitudeLatitude,
|
|
1418
|
+
pickXRSurface,
|
|
1419
|
+
setWebXRSessionWithFallback,
|
|
1420
|
+
transformXRRayToContent,
|
|
1421
|
+
unionGeographicBounds
|
|
1422
|
+
};
|
|
1423
|
+
//# sourceMappingURL=webxr.js.map
|