@multisetai/vps 1.0.6 → 1.0.7-beta.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 +89 -15
- package/dist/core/index.d.ts +40 -4
- package/dist/core/index.js +75 -15
- package/dist/core/index.js.map +1 -1
- package/dist/index.js +535 -130
- package/dist/index.js.map +1 -1
- package/dist/webxr/index.d.ts +23 -5
- package/dist/webxr/index.js +461 -116
- package/dist/webxr/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import axios from 'axios';
|
|
2
|
-
import * as
|
|
2
|
+
import * as THREE3 from 'three';
|
|
3
3
|
import { ARButton } from 'three/examples/jsm/webxr/ARButton.js';
|
|
4
|
+
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js';
|
|
5
|
+
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
|
|
4
6
|
|
|
5
|
-
// src/lib/core/
|
|
7
|
+
// src/lib/core/config.ts
|
|
6
8
|
var DEFAULT_ENDPOINTS = {
|
|
7
9
|
authUrl: "https://api.multiset.ai/v1/m2m/token",
|
|
8
10
|
queryUrl: "https://api.multiset.ai/v1/vps/map/query-form",
|
|
@@ -14,6 +16,7 @@ var MultisetClient = class {
|
|
|
14
16
|
constructor(config) {
|
|
15
17
|
this.config = config;
|
|
16
18
|
this.accessToken = null;
|
|
19
|
+
this.mapDetailsCache = {};
|
|
17
20
|
this.config = config;
|
|
18
21
|
this.endpoints = {
|
|
19
22
|
...DEFAULT_ENDPOINTS,
|
|
@@ -23,6 +26,28 @@ var MultisetClient = class {
|
|
|
23
26
|
get token() {
|
|
24
27
|
return this.accessToken;
|
|
25
28
|
}
|
|
29
|
+
getConfig() {
|
|
30
|
+
return this.config;
|
|
31
|
+
}
|
|
32
|
+
async downloadFile(key) {
|
|
33
|
+
if (!this.accessToken || !key) {
|
|
34
|
+
return "";
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
const response = await axios.get(
|
|
38
|
+
`${this.endpoints.fileDownloadUrl}?key=${encodeURIComponent(key)}`,
|
|
39
|
+
{
|
|
40
|
+
headers: {
|
|
41
|
+
Authorization: `Bearer ${this.accessToken}`
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
);
|
|
45
|
+
return response.status === 200 ? response.data.url : "";
|
|
46
|
+
} catch (error) {
|
|
47
|
+
this.handleError(error);
|
|
48
|
+
return "";
|
|
49
|
+
}
|
|
50
|
+
}
|
|
26
51
|
async authorize() {
|
|
27
52
|
var _a, _b, _c, _d, _e;
|
|
28
53
|
try {
|
|
@@ -70,22 +95,52 @@ var MultisetClient = class {
|
|
|
70
95
|
}
|
|
71
96
|
return queryResult;
|
|
72
97
|
}
|
|
98
|
+
async getGeoPoseComponents() {
|
|
99
|
+
if (typeof navigator === "undefined" || !("geolocation" in navigator) || !navigator.geolocation) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
const position = await new Promise((resolve, reject) => {
|
|
104
|
+
navigator.geolocation.getCurrentPosition(resolve, reject, {
|
|
105
|
+
enableHighAccuracy: true,
|
|
106
|
+
timeout: 1e4,
|
|
107
|
+
maximumAge: 0
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
const { latitude, longitude, altitude } = position.coords;
|
|
111
|
+
const safeAltitude = typeof altitude === "number" && !Number.isNaN(altitude) ? altitude : 0;
|
|
112
|
+
return { latitude, longitude, altitude: safeAltitude };
|
|
113
|
+
} catch {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
73
117
|
async queryLocalization(frame, intrinsics) {
|
|
74
118
|
var _a;
|
|
75
119
|
const formData = new FormData();
|
|
76
|
-
formData.append("isRightHanded", "true");
|
|
77
|
-
formData.append("width", `${frame.width}`);
|
|
78
|
-
formData.append("height", `${frame.height}`);
|
|
79
|
-
formData.append("px", `${intrinsics.px}`);
|
|
80
|
-
formData.append("py", `${intrinsics.py}`);
|
|
81
|
-
formData.append("fx", `${intrinsics.fx}`);
|
|
82
|
-
formData.append("fy", `${intrinsics.fy}`);
|
|
83
|
-
formData.append("queryImage", frame.blob);
|
|
84
120
|
if (this.config.mapType === "map") {
|
|
85
121
|
formData.append("mapCode", this.config.code);
|
|
86
122
|
} else {
|
|
87
123
|
formData.append("mapSetCode", this.config.code);
|
|
88
124
|
}
|
|
125
|
+
formData.append("isRightHanded", "true");
|
|
126
|
+
formData.append("fx", `${intrinsics.fx}`);
|
|
127
|
+
formData.append("fy", `${intrinsics.fy}`);
|
|
128
|
+
formData.append("px", `${intrinsics.px}`);
|
|
129
|
+
formData.append("py", `${intrinsics.py}`);
|
|
130
|
+
formData.append("width", `${frame.width}`);
|
|
131
|
+
formData.append("height", `${frame.height}`);
|
|
132
|
+
formData.append("queryImage", frame.blob);
|
|
133
|
+
if (this.config.geoCoordinatesInResponse) {
|
|
134
|
+
formData.append("geoCoordinatesInResponse", "true");
|
|
135
|
+
}
|
|
136
|
+
if (this.config.passGeoPose) {
|
|
137
|
+
const components = await this.getGeoPoseComponents();
|
|
138
|
+
if (components) {
|
|
139
|
+
const { latitude, longitude, altitude } = components;
|
|
140
|
+
const geoHint = `${latitude},${longitude},${altitude}`;
|
|
141
|
+
formData.append("geoHint", geoHint);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
89
144
|
try {
|
|
90
145
|
const response = await axios.post(
|
|
91
146
|
this.endpoints.queryUrl,
|
|
@@ -103,10 +158,17 @@ var MultisetClient = class {
|
|
|
103
158
|
const result = {
|
|
104
159
|
localizeData: data
|
|
105
160
|
};
|
|
106
|
-
if ((_a = data.
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
161
|
+
if (this.config.showMesh && this.config.mapType === "map" && ((_a = data.mapCodes) == null ? void 0 : _a.length)) {
|
|
162
|
+
const mapCode = data.mapCodes[0];
|
|
163
|
+
const cached = this.mapDetailsCache[mapCode];
|
|
164
|
+
if (cached) {
|
|
165
|
+
result.mapDetails = cached;
|
|
166
|
+
} else {
|
|
167
|
+
const mapDetails = await this.fetchMapDetails(mapCode);
|
|
168
|
+
if (mapDetails) {
|
|
169
|
+
this.mapDetailsCache[mapCode] = mapDetails;
|
|
170
|
+
result.mapDetails = mapDetails;
|
|
171
|
+
}
|
|
110
172
|
}
|
|
111
173
|
}
|
|
112
174
|
return result;
|
|
@@ -115,10 +177,10 @@ var MultisetClient = class {
|
|
|
115
177
|
return null;
|
|
116
178
|
}
|
|
117
179
|
}
|
|
118
|
-
async fetchMapDetails(
|
|
180
|
+
async fetchMapDetails(mapCode) {
|
|
119
181
|
try {
|
|
120
182
|
const response = await axios.get(
|
|
121
|
-
`${this.endpoints.mapDetailsUrl}${
|
|
183
|
+
`${this.endpoints.mapDetailsUrl}${mapCode}`,
|
|
122
184
|
{
|
|
123
185
|
headers: {
|
|
124
186
|
Authorization: `Bearer ${this.accessToken}`
|
|
@@ -148,59 +210,373 @@ var MultisetClient = class {
|
|
|
148
210
|
}
|
|
149
211
|
}
|
|
150
212
|
};
|
|
213
|
+
|
|
214
|
+
// src/lib/webxr/internal/cameraIntrinsics.ts
|
|
215
|
+
function getCameraIntrinsics(projectionMatrix, viewport) {
|
|
216
|
+
const p = projectionMatrix;
|
|
217
|
+
const u0 = (1 - p[8]) * viewport.width / 2 + viewport.x;
|
|
218
|
+
const v0 = (1 - p[9]) * viewport.height / 2 + viewport.y;
|
|
219
|
+
const ax = viewport.width / 2 * p[0];
|
|
220
|
+
const ay = viewport.height / 2 * p[5];
|
|
221
|
+
return {
|
|
222
|
+
fx: ax,
|
|
223
|
+
fy: ay,
|
|
224
|
+
px: u0,
|
|
225
|
+
py: v0,
|
|
226
|
+
width: viewport.width,
|
|
227
|
+
height: viewport.height
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// src/lib/webxr/internal/frameCapture.ts
|
|
232
|
+
async function compressToJpeg(buffer, width, height, quality = 0.8) {
|
|
233
|
+
const canvas = document.createElement("canvas");
|
|
234
|
+
canvas.width = width;
|
|
235
|
+
canvas.height = height;
|
|
236
|
+
const ctx = canvas.getContext("2d");
|
|
237
|
+
if (!ctx) return new Blob();
|
|
238
|
+
const imageData = new ImageData(new Uint8ClampedArray(buffer), width, height);
|
|
239
|
+
ctx.putImageData(imageData, 0, 0);
|
|
240
|
+
return new Promise((resolve) => {
|
|
241
|
+
canvas.toBlob((blob) => resolve(blob != null ? blob : new Blob()), "image/jpeg", quality);
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
async function getCameraTextureAsImage(renderer, webGLTexture, width, height) {
|
|
245
|
+
const gl = renderer.getContext();
|
|
246
|
+
if (!gl) return null;
|
|
247
|
+
const framebuffer = gl.createFramebuffer();
|
|
248
|
+
if (!framebuffer) return null;
|
|
249
|
+
let pixelBuffer;
|
|
250
|
+
try {
|
|
251
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
|
|
252
|
+
gl.framebufferTexture2D(
|
|
253
|
+
gl.FRAMEBUFFER,
|
|
254
|
+
gl.COLOR_ATTACHMENT0,
|
|
255
|
+
gl.TEXTURE_2D,
|
|
256
|
+
webGLTexture,
|
|
257
|
+
0
|
|
258
|
+
);
|
|
259
|
+
pixelBuffer = new Uint8Array(width * height * 4);
|
|
260
|
+
gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixelBuffer);
|
|
261
|
+
} finally {
|
|
262
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
263
|
+
gl.deleteFramebuffer(framebuffer);
|
|
264
|
+
}
|
|
265
|
+
const flippedData = new Uint8ClampedArray(pixelBuffer.length);
|
|
266
|
+
for (let row = 0; row < height; row += 1) {
|
|
267
|
+
const sourceStart = row * width * 4;
|
|
268
|
+
const destStart = (height - row - 1) * width * 4;
|
|
269
|
+
flippedData.set(
|
|
270
|
+
pixelBuffer.subarray(sourceStart, sourceStart + width * 4),
|
|
271
|
+
destStart
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
const blob = await compressToJpeg(flippedData.buffer, width, height, 0.7);
|
|
275
|
+
if (!blob.size) {
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
return {
|
|
279
|
+
blob,
|
|
280
|
+
width,
|
|
281
|
+
height
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
var Experience = class {
|
|
285
|
+
constructor(canvas) {
|
|
286
|
+
this.renderer = new THREE3.WebGLRenderer({
|
|
287
|
+
canvas,
|
|
288
|
+
antialias: true,
|
|
289
|
+
alpha: true
|
|
290
|
+
});
|
|
291
|
+
this.renderer.setSize(window.innerWidth, window.innerHeight);
|
|
292
|
+
this.renderer.setPixelRatio(window.devicePixelRatio);
|
|
293
|
+
this.renderer.xr.enabled = true;
|
|
294
|
+
this.camera = new THREE3.PerspectiveCamera(
|
|
295
|
+
45,
|
|
296
|
+
window.innerWidth / window.innerHeight,
|
|
297
|
+
0.2,
|
|
298
|
+
1e4
|
|
299
|
+
);
|
|
300
|
+
this.scene = new THREE3.Scene();
|
|
301
|
+
this.setupLights();
|
|
302
|
+
}
|
|
303
|
+
setupLights() {
|
|
304
|
+
const light = new THREE3.HemisphereLight(16777215, 16314623, 1);
|
|
305
|
+
light.position.set(0.5, 2, 0.25);
|
|
306
|
+
this.scene.add(light);
|
|
307
|
+
const diLight = new THREE3.DirectionalLight("#7B2CBF");
|
|
308
|
+
diLight.position.set(0, 2, 0);
|
|
309
|
+
this.scene.add(diLight);
|
|
310
|
+
}
|
|
311
|
+
getScene() {
|
|
312
|
+
return this.scene;
|
|
313
|
+
}
|
|
314
|
+
getCamera() {
|
|
315
|
+
return this.camera;
|
|
316
|
+
}
|
|
317
|
+
getRenderer() {
|
|
318
|
+
return this.renderer;
|
|
319
|
+
}
|
|
320
|
+
resize() {
|
|
321
|
+
this.camera.aspect = window.innerWidth / window.innerHeight;
|
|
322
|
+
this.camera.updateProjectionMatrix();
|
|
323
|
+
this.renderer.setSize(window.innerWidth, window.innerHeight);
|
|
324
|
+
}
|
|
325
|
+
dispose() {
|
|
326
|
+
this.renderer.dispose();
|
|
327
|
+
this.scene = null;
|
|
328
|
+
this.camera = null;
|
|
329
|
+
this.renderer = null;
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
var VERTEX = `
|
|
333
|
+
varying vec3 vWorldPosition;
|
|
334
|
+
|
|
335
|
+
void main() {
|
|
336
|
+
vec4 worldPos = modelMatrix * vec4(position, 1.0);
|
|
337
|
+
vWorldPosition = worldPos.xyz;
|
|
338
|
+
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
|
339
|
+
}
|
|
340
|
+
`;
|
|
341
|
+
var FRAGMENT = `
|
|
342
|
+
uniform vec3 uColor;
|
|
343
|
+
uniform float uOpacity;
|
|
344
|
+
uniform vec3 uGridColor;
|
|
345
|
+
uniform float uGridScale;
|
|
346
|
+
|
|
347
|
+
varying vec3 vWorldPosition;
|
|
348
|
+
|
|
349
|
+
void main() {
|
|
350
|
+
vec2 coord = vWorldPosition.xz * uGridScale;
|
|
351
|
+
vec2 grid = abs(fract(coord - 0.5) - 0.5) / fwidth(coord);
|
|
352
|
+
float line = min(grid.x, grid.y) / 2.0;
|
|
353
|
+
float gridLine = 1.0 - min(line, 1.0);
|
|
354
|
+
|
|
355
|
+
vec3 base = uColor;
|
|
356
|
+
vec3 gridCol = uGridColor;
|
|
357
|
+
float alpha = mix(uOpacity, 1.0, gridLine);
|
|
358
|
+
vec3 color = mix(base, gridCol, gridLine);
|
|
359
|
+
gl_FragColor = vec4(color, alpha);
|
|
360
|
+
}
|
|
361
|
+
`;
|
|
362
|
+
function createGridMaterial(options = {}) {
|
|
363
|
+
var _a, _b, _c, _d;
|
|
364
|
+
const color = (_a = options.color) != null ? _a : "#7B2CBF";
|
|
365
|
+
const opacity = (_b = options.opacity) != null ? _b : 0.6;
|
|
366
|
+
const gridColor = (_c = options.gridColor) != null ? _c : "#ffeb3b";
|
|
367
|
+
const gridScale = (_d = options.gridScale) != null ? _d : 2;
|
|
368
|
+
return new THREE3.ShaderMaterial({
|
|
369
|
+
vertexShader: VERTEX,
|
|
370
|
+
fragmentShader: FRAGMENT,
|
|
371
|
+
transparent: true,
|
|
372
|
+
side: THREE3.DoubleSide,
|
|
373
|
+
uniforms: {
|
|
374
|
+
uColor: { value: typeof color === "string" ? new THREE3.Color(color) : color },
|
|
375
|
+
uOpacity: { value: opacity },
|
|
376
|
+
uGridColor: { value: typeof gridColor === "string" ? new THREE3.Color(gridColor) : gridColor },
|
|
377
|
+
uGridScale: { value: gridScale }
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// src/lib/webxr/mapMeshVisualizer.ts
|
|
383
|
+
var LARGE_MAP_THRESHOLD = 50;
|
|
384
|
+
var MapMeshVisualizer = class {
|
|
385
|
+
constructor(scene, client) {
|
|
386
|
+
this.scene = scene;
|
|
387
|
+
this.client = client;
|
|
388
|
+
this.meshGroup = new THREE3.Group();
|
|
389
|
+
this.meshGroup.visible = false;
|
|
390
|
+
this.scene.add(this.meshGroup);
|
|
391
|
+
this.dracoLoader = new DRACOLoader();
|
|
392
|
+
this.dracoLoader.setDecoderPath("/draco/");
|
|
393
|
+
this.gltfLoader = new GLTFLoader();
|
|
394
|
+
this.gltfLoader.setDRACOLoader(this.dracoLoader);
|
|
395
|
+
}
|
|
396
|
+
getMeshGroup() {
|
|
397
|
+
return this.meshGroup;
|
|
398
|
+
}
|
|
399
|
+
async ensureMeshLoaded(mapDetails) {
|
|
400
|
+
var _a, _b;
|
|
401
|
+
if (this.scene.getObjectByName(mapDetails._id)) {
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
const meshKey = (_b = (_a = mapDetails.mapMesh) == null ? void 0 : _a.rawMesh) == null ? void 0 : _b.meshLink;
|
|
405
|
+
if (!meshKey) {
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
const url = await this.client.downloadFile(meshKey);
|
|
409
|
+
if (!url) {
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
const meshMaterial = createGridMaterial({
|
|
413
|
+
color: "#7B2CBF",
|
|
414
|
+
opacity: 0.6,
|
|
415
|
+
gridColor: "#ffeb3b",
|
|
416
|
+
gridScale: 2
|
|
417
|
+
});
|
|
418
|
+
await new Promise((resolve, reject) => {
|
|
419
|
+
this.gltfLoader.load(
|
|
420
|
+
url,
|
|
421
|
+
(gltf) => {
|
|
422
|
+
gltf.scene.traverse((child) => {
|
|
423
|
+
if (child.isMesh) {
|
|
424
|
+
child.material = meshMaterial;
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
const box = new THREE3.Box3().setFromObject(gltf.scene);
|
|
428
|
+
let size = box.getSize(new THREE3.Vector3()).length();
|
|
429
|
+
box.getCenter(new THREE3.Vector3());
|
|
430
|
+
if (size > LARGE_MAP_THRESHOLD) {
|
|
431
|
+
size = LARGE_MAP_THRESHOLD;
|
|
432
|
+
new THREE3.Vector3();
|
|
433
|
+
}
|
|
434
|
+
gltf.scene.name = mapDetails._id;
|
|
435
|
+
this.meshGroup.add(gltf.scene);
|
|
436
|
+
resolve();
|
|
437
|
+
},
|
|
438
|
+
void 0,
|
|
439
|
+
(error) => {
|
|
440
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
441
|
+
}
|
|
442
|
+
);
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
applyMeshTransform(best, trackerSpace) {
|
|
446
|
+
const pose = best.localizeData;
|
|
447
|
+
const resPosition = new THREE3.Vector3(
|
|
448
|
+
pose.position.x,
|
|
449
|
+
pose.position.y,
|
|
450
|
+
pose.position.z
|
|
451
|
+
);
|
|
452
|
+
const resRotation = new THREE3.Quaternion(
|
|
453
|
+
pose.rotation.x,
|
|
454
|
+
pose.rotation.y,
|
|
455
|
+
pose.rotation.z,
|
|
456
|
+
pose.rotation.w
|
|
457
|
+
);
|
|
458
|
+
const responseMatrix = new THREE3.Matrix4();
|
|
459
|
+
responseMatrix.compose(resPosition, resRotation, new THREE3.Vector3(1, 1, 1));
|
|
460
|
+
const inverseResponseMatrix = responseMatrix.clone().invert();
|
|
461
|
+
const resultantMatrix = new THREE3.Matrix4();
|
|
462
|
+
resultantMatrix.multiplyMatrices(trackerSpace, inverseResponseMatrix);
|
|
463
|
+
const position = new THREE3.Vector3();
|
|
464
|
+
const rotation = new THREE3.Quaternion();
|
|
465
|
+
const scale = new THREE3.Vector3();
|
|
466
|
+
resultantMatrix.decompose(position, rotation, scale);
|
|
467
|
+
this.meshGroup.position.copy(position);
|
|
468
|
+
this.meshGroup.quaternion.copy(rotation);
|
|
469
|
+
this.meshGroup.scale.set(1, 1, 1);
|
|
470
|
+
this.meshGroup.visible = true;
|
|
471
|
+
this.meshGroup.updateMatrix();
|
|
472
|
+
}
|
|
473
|
+
dispose() {
|
|
474
|
+
this.meshGroup.traverse((child) => {
|
|
475
|
+
const mesh = child;
|
|
476
|
+
if (mesh.isMesh) {
|
|
477
|
+
mesh.geometry.dispose();
|
|
478
|
+
if (Array.isArray(mesh.material)) {
|
|
479
|
+
mesh.material.forEach((m) => m.dispose());
|
|
480
|
+
} else {
|
|
481
|
+
mesh.material.dispose();
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
this.scene.remove(this.meshGroup);
|
|
486
|
+
this.dracoLoader.dispose();
|
|
487
|
+
}
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
// src/lib/webxr/world/World.ts
|
|
491
|
+
var World = class {
|
|
492
|
+
constructor(scene, client) {
|
|
493
|
+
this.meshVisualizer = new MapMeshVisualizer(scene, client);
|
|
494
|
+
}
|
|
495
|
+
async ensureMeshLoaded(mapDetails) {
|
|
496
|
+
await this.meshVisualizer.ensureMeshLoaded(mapDetails);
|
|
497
|
+
}
|
|
498
|
+
applyMeshTransform(best, trackerSpace) {
|
|
499
|
+
this.meshVisualizer.applyMeshTransform(best, trackerSpace);
|
|
500
|
+
}
|
|
501
|
+
dispose() {
|
|
502
|
+
this.meshVisualizer.dispose();
|
|
503
|
+
}
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
// src/lib/webxr/controller.ts
|
|
507
|
+
function clamp(value, min, max) {
|
|
508
|
+
return Math.max(min, Math.min(max, value));
|
|
509
|
+
}
|
|
510
|
+
var REQUEST_ATTEMPTS_MIN = 1;
|
|
511
|
+
var REQUEST_ATTEMPTS_MAX = 5;
|
|
512
|
+
var LOCALIZATION_INTERVAL_MIN = 1;
|
|
513
|
+
var LOCALIZATION_INTERVAL_MAX = 5;
|
|
514
|
+
var CONFIDENCE_THRESHOLD_MIN = 0.2;
|
|
515
|
+
var CONFIDENCE_THRESHOLD_MAX = 0.8;
|
|
516
|
+
var TRACKING_LOSS_FRAME_THRESHOLD = 60;
|
|
151
517
|
var WebxrController = class {
|
|
152
518
|
constructor(options) {
|
|
153
519
|
this.options = options;
|
|
154
|
-
this.
|
|
155
|
-
this.
|
|
156
|
-
this.scene = null;
|
|
157
|
-
this.animationLoop = null;
|
|
520
|
+
this.experience = null;
|
|
521
|
+
this.world = null;
|
|
158
522
|
this.arButton = null;
|
|
159
523
|
this.resizeHandler = null;
|
|
160
524
|
this.isSessionActive = false;
|
|
525
|
+
this.trackingLossFrames = 0;
|
|
526
|
+
this.isLocalizing = false;
|
|
527
|
+
this.trackerSpace = null;
|
|
161
528
|
}
|
|
162
529
|
async initialize(buttonContainer) {
|
|
163
530
|
var _a, _b, _c, _d;
|
|
164
|
-
if (this.
|
|
531
|
+
if (this.experience) {
|
|
165
532
|
return this.arButton;
|
|
166
533
|
}
|
|
167
534
|
if (!window.isSecureContext) {
|
|
168
535
|
throw new Error("WebXR requires a secure context (HTTPS).");
|
|
169
536
|
}
|
|
170
537
|
const canvas = (_a = this.options.canvas) != null ? _a : document.createElement("canvas");
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
177
|
-
renderer.setPixelRatio(window.devicePixelRatio);
|
|
178
|
-
renderer.xr.enabled = true;
|
|
538
|
+
this.experience = new Experience(canvas);
|
|
539
|
+
this.world = new World(this.experience.getScene(), this.options.client);
|
|
540
|
+
const renderer = this.experience.getRenderer();
|
|
541
|
+
const camera = this.experience.getCamera();
|
|
542
|
+
const scene = this.experience.getScene();
|
|
179
543
|
renderer.xr.addEventListener("sessionstart", () => {
|
|
180
544
|
var _a2, _b2;
|
|
181
545
|
this.isSessionActive = true;
|
|
182
546
|
(_b2 = (_a2 = this.options).onSessionStart) == null ? void 0 : _b2.call(_a2);
|
|
547
|
+
const cfg = this.options.client.getConfig();
|
|
548
|
+
if (cfg.autoLocalize) {
|
|
549
|
+
void this.localizeFrame();
|
|
550
|
+
}
|
|
183
551
|
});
|
|
184
552
|
renderer.xr.addEventListener("sessionend", () => {
|
|
185
553
|
var _a2, _b2;
|
|
186
554
|
this.isSessionActive = false;
|
|
187
555
|
(_b2 = (_a2 = this.options).onSessionEnd) == null ? void 0 : _b2.call(_a2);
|
|
188
556
|
});
|
|
189
|
-
const
|
|
190
|
-
45,
|
|
191
|
-
window.innerWidth / window.innerHeight,
|
|
192
|
-
0.2,
|
|
193
|
-
1e4
|
|
194
|
-
);
|
|
195
|
-
const scene = new THREE.Scene();
|
|
196
|
-
const animationLoop = () => {
|
|
557
|
+
const animationLoop = (_time, frame) => {
|
|
197
558
|
renderer.render(scene, camera);
|
|
559
|
+
const cfg = this.options.client.getConfig();
|
|
560
|
+
if (cfg.relocalization && frame && !this.isLocalizing) {
|
|
561
|
+
const refSpace = renderer.xr.getReferenceSpace();
|
|
562
|
+
if (refSpace) {
|
|
563
|
+
const viewerPose = frame.getViewerPose(refSpace);
|
|
564
|
+
if (!viewerPose) {
|
|
565
|
+
this.trackingLossFrames += 1;
|
|
566
|
+
if (this.trackingLossFrames >= TRACKING_LOSS_FRAME_THRESHOLD) {
|
|
567
|
+
this.trackingLossFrames = 0;
|
|
568
|
+
void this.localizeFrame();
|
|
569
|
+
}
|
|
570
|
+
} else {
|
|
571
|
+
this.trackingLossFrames = 0;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
198
575
|
};
|
|
199
576
|
renderer.setAnimationLoop(animationLoop);
|
|
200
577
|
const resizeHandler = () => {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
578
|
+
var _a2;
|
|
579
|
+
(_a2 = this.experience) == null ? void 0 : _a2.resize();
|
|
204
580
|
};
|
|
205
581
|
window.addEventListener("resize", resizeHandler);
|
|
206
582
|
const overlayRoot = (_b = this.options.overlayRoot) != null ? _b : document.body;
|
|
@@ -212,51 +588,142 @@ var WebxrController = class {
|
|
|
212
588
|
if (!buttonParent.contains(arButton)) {
|
|
213
589
|
buttonParent.appendChild(arButton);
|
|
214
590
|
}
|
|
215
|
-
this.renderer = renderer;
|
|
216
|
-
this.camera = camera;
|
|
217
|
-
this.scene = scene;
|
|
218
|
-
this.animationLoop = animationLoop;
|
|
219
591
|
this.arButton = arButton;
|
|
220
592
|
this.resizeHandler = resizeHandler;
|
|
221
593
|
(_d = (_c = this.options).onARButtonCreated) == null ? void 0 : _d.call(_c, arButton);
|
|
222
594
|
return arButton;
|
|
223
595
|
}
|
|
224
596
|
getScene() {
|
|
225
|
-
if (!this.
|
|
597
|
+
if (!this.experience) {
|
|
226
598
|
throw new Error("Scene: WebXR controller has not been initialized.");
|
|
227
599
|
}
|
|
228
|
-
return this.
|
|
600
|
+
return this.experience.getScene();
|
|
229
601
|
}
|
|
230
602
|
getCamera() {
|
|
231
|
-
if (!this.
|
|
603
|
+
if (!this.experience) {
|
|
232
604
|
throw new Error("Camera: WebXR controller has not been initialized.");
|
|
233
605
|
}
|
|
234
|
-
return this.
|
|
606
|
+
return this.experience.getCamera();
|
|
235
607
|
}
|
|
236
608
|
getRenderer() {
|
|
237
|
-
if (!this.
|
|
609
|
+
if (!this.experience) {
|
|
238
610
|
throw new Error("Renderer: WebXR controller has not been initialized.");
|
|
239
611
|
}
|
|
240
|
-
return this.
|
|
612
|
+
return this.experience.getRenderer();
|
|
241
613
|
}
|
|
242
614
|
hasActiveSession() {
|
|
243
615
|
var _a;
|
|
244
|
-
return this.isSessionActive && ((_a = this.
|
|
616
|
+
return this.isSessionActive && ((_a = this.experience) == null ? void 0 : _a.getRenderer().xr.isPresenting) === true;
|
|
245
617
|
}
|
|
618
|
+
/**
|
|
619
|
+
* Runs a single-frame localization cycle: multiple attempts (per requestAttempts),
|
|
620
|
+
* picks best result by confidence, optionally validates against confidenceThreshold,
|
|
621
|
+
* and fires onLocalizationInit / onLocalizationSuccess / onLocalizationFailure.
|
|
622
|
+
* Aligns with Unity SingleFrameLocalizationManager.LocalizeFrame().
|
|
623
|
+
*/
|
|
624
|
+
async localizeFrame() {
|
|
625
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l;
|
|
626
|
+
if (!this.experience) {
|
|
627
|
+
throw new Error("WebXR: WebXR controller has not been initialized.");
|
|
628
|
+
}
|
|
629
|
+
const renderer = this.experience.getRenderer();
|
|
630
|
+
const session = (_b = (_a = renderer.xr).getSession) == null ? void 0 : _b.call(_a);
|
|
631
|
+
if (!session) {
|
|
632
|
+
throw new Error(
|
|
633
|
+
"WebXR Session: No active WebXR session. Start AR before calling localizeFrame()."
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
const cfg = this.options.client.getConfig();
|
|
637
|
+
const requestAttempts = clamp(
|
|
638
|
+
(_c = cfg.requestAttempts) != null ? _c : 1,
|
|
639
|
+
REQUEST_ATTEMPTS_MIN,
|
|
640
|
+
REQUEST_ATTEMPTS_MAX
|
|
641
|
+
);
|
|
642
|
+
const localizationInterval = clamp(
|
|
643
|
+
(_d = cfg.localizationInterval) != null ? _d : 1,
|
|
644
|
+
LOCALIZATION_INTERVAL_MIN,
|
|
645
|
+
LOCALIZATION_INTERVAL_MAX
|
|
646
|
+
);
|
|
647
|
+
const confidenceCheck = (_e = cfg.confidenceCheck) != null ? _e : false;
|
|
648
|
+
const confidenceThreshold = clamp(
|
|
649
|
+
(_f = cfg.confidenceThreshold) != null ? _f : 0.5,
|
|
650
|
+
CONFIDENCE_THRESHOLD_MIN,
|
|
651
|
+
CONFIDENCE_THRESHOLD_MAX
|
|
652
|
+
);
|
|
653
|
+
(_g = cfg.onLocalizationInit) == null ? void 0 : _g.call(cfg);
|
|
654
|
+
this.isLocalizing = true;
|
|
655
|
+
const results = [];
|
|
656
|
+
const MAX_FRAME_ATTEMPTS = requestAttempts * 3;
|
|
657
|
+
try {
|
|
658
|
+
let apiCallsDone = 0;
|
|
659
|
+
let frameAttempts = 0;
|
|
660
|
+
while (apiCallsDone < requestAttempts && frameAttempts < MAX_FRAME_ATTEMPTS) {
|
|
661
|
+
frameAttempts++;
|
|
662
|
+
try {
|
|
663
|
+
const { result, apiCalled } = await this.captureFrame();
|
|
664
|
+
if (apiCalled) {
|
|
665
|
+
apiCallsDone++;
|
|
666
|
+
if ((_h = result == null ? void 0 : result.localizeData) == null ? void 0 : _h.poseFound) {
|
|
667
|
+
results.push(result);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
} catch {
|
|
671
|
+
apiCallsDone++;
|
|
672
|
+
}
|
|
673
|
+
if (apiCallsDone < requestAttempts && frameAttempts < MAX_FRAME_ATTEMPTS) {
|
|
674
|
+
await new Promise(
|
|
675
|
+
(r) => setTimeout(r, localizationInterval * 1e3)
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
} finally {
|
|
680
|
+
this.isLocalizing = false;
|
|
681
|
+
}
|
|
682
|
+
const best = results.length ? results.sort(
|
|
683
|
+
(a, b) => {
|
|
684
|
+
var _a2, _b2;
|
|
685
|
+
return ((_a2 = b.localizeData.confidence) != null ? _a2 : 0) - ((_b2 = a.localizeData.confidence) != null ? _b2 : 0);
|
|
686
|
+
}
|
|
687
|
+
)[0] : null;
|
|
688
|
+
const accepted = best && (!confidenceCheck || ((_i = best.localizeData.confidence) != null ? _i : 0) >= confidenceThreshold);
|
|
689
|
+
if (accepted && best) {
|
|
690
|
+
(_j = cfg.onLocalizationSuccess) == null ? void 0 : _j.call(cfg, best);
|
|
691
|
+
if (cfg.showMesh && best.mapDetails && this.world && this.trackerSpace) {
|
|
692
|
+
try {
|
|
693
|
+
await this.world.ensureMeshLoaded(best.mapDetails);
|
|
694
|
+
this.world.applyMeshTransform(best, this.trackerSpace);
|
|
695
|
+
} catch {
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
return best;
|
|
699
|
+
}
|
|
700
|
+
const reason = !best ? "All attempts failed to produce a pose." : confidenceCheck ? `Best confidence ${(_k = best.localizeData.confidence) != null ? _k : 0} below threshold ${confidenceThreshold}.` : void 0;
|
|
701
|
+
(_l = cfg.onLocalizationFailure) == null ? void 0 : _l.call(cfg, reason);
|
|
702
|
+
return null;
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* Internal: captures one frame and calls the localization API. Returns the
|
|
706
|
+
* result and whether the API was actually invoked — used by localizeFrame()
|
|
707
|
+
* to count only real API calls toward requestAttempts (frames with no camera
|
|
708
|
+
* image are skipped).
|
|
709
|
+
*/
|
|
246
710
|
async captureFrame() {
|
|
247
711
|
var _a, _b;
|
|
248
|
-
|
|
249
|
-
const camera = this.camera;
|
|
250
|
-
if (!renderer || !camera) {
|
|
712
|
+
if (!this.experience) {
|
|
251
713
|
throw new Error("WebXR: WebXR controller has not been initialized.");
|
|
252
714
|
}
|
|
715
|
+
const renderer = this.experience.getRenderer();
|
|
253
716
|
const session = (_b = (_a = renderer.xr).getSession) == null ? void 0 : _b.call(_a);
|
|
254
717
|
if (!session) {
|
|
255
|
-
throw new Error(
|
|
718
|
+
throw new Error(
|
|
719
|
+
"WebXR Session: No active WebXR session. Start AR before capturing."
|
|
720
|
+
);
|
|
256
721
|
}
|
|
257
722
|
const referenceSpace = renderer.xr.getReferenceSpace();
|
|
258
723
|
if (!referenceSpace) {
|
|
259
|
-
throw new Error(
|
|
724
|
+
throw new Error(
|
|
725
|
+
"WebXR Reference Space: Unable to acquire XR reference space."
|
|
726
|
+
);
|
|
260
727
|
}
|
|
261
728
|
const gl = renderer.getContext();
|
|
262
729
|
return new Promise((resolve, reject) => {
|
|
@@ -265,7 +732,7 @@ var WebxrController = class {
|
|
|
265
732
|
try {
|
|
266
733
|
const viewerPose = xrFrame.getViewerPose(referenceSpace);
|
|
267
734
|
if (!viewerPose) {
|
|
268
|
-
resolve(null);
|
|
735
|
+
resolve({ result: null, apiCalled: false });
|
|
269
736
|
return;
|
|
270
737
|
}
|
|
271
738
|
for (const view of viewerPose.views) {
|
|
@@ -292,15 +759,16 @@ var WebxrController = class {
|
|
|
292
759
|
y: 0
|
|
293
760
|
});
|
|
294
761
|
if (frameData && intrinsics) {
|
|
762
|
+
this.trackerSpace = new THREE3.Matrix4().fromArray(view.transform.matrix);
|
|
295
763
|
const result = await this.options.client.localizeWithFrame(
|
|
296
764
|
frameData,
|
|
297
765
|
intrinsics
|
|
298
766
|
);
|
|
299
|
-
resolve(result);
|
|
767
|
+
resolve({ result, apiCalled: true });
|
|
300
768
|
return;
|
|
301
769
|
}
|
|
302
770
|
}
|
|
303
|
-
resolve(null);
|
|
771
|
+
resolve({ result: null, apiCalled: false });
|
|
304
772
|
} catch (error) {
|
|
305
773
|
reject(error);
|
|
306
774
|
} finally {
|
|
@@ -313,85 +781,22 @@ var WebxrController = class {
|
|
|
313
781
|
});
|
|
314
782
|
}
|
|
315
783
|
dispose() {
|
|
316
|
-
var _a;
|
|
784
|
+
var _a, _b, _c, _d;
|
|
317
785
|
if (this.resizeHandler) {
|
|
318
786
|
window.removeEventListener("resize", this.resizeHandler);
|
|
319
787
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
this.
|
|
325
|
-
this.
|
|
326
|
-
this.
|
|
327
|
-
if ((_a = this.arButton) == null ? void 0 : _a.parentElement) {
|
|
788
|
+
(_a = this.experience) == null ? void 0 : _a.getRenderer().setAnimationLoop(null);
|
|
789
|
+
(_b = this.experience) == null ? void 0 : _b.dispose();
|
|
790
|
+
this.experience = null;
|
|
791
|
+
(_c = this.world) == null ? void 0 : _c.dispose();
|
|
792
|
+
this.world = null;
|
|
793
|
+
this.trackingLossFrames = 0;
|
|
794
|
+
if ((_d = this.arButton) == null ? void 0 : _d.parentElement) {
|
|
328
795
|
this.arButton.parentElement.removeChild(this.arButton);
|
|
329
796
|
}
|
|
330
797
|
this.arButton = null;
|
|
331
798
|
}
|
|
332
799
|
};
|
|
333
|
-
function getCameraIntrinsics(projectionMatrix, viewport) {
|
|
334
|
-
const p = projectionMatrix;
|
|
335
|
-
const u0 = (1 - p[8]) * viewport.width / 2 + viewport.x;
|
|
336
|
-
const v0 = (1 - p[9]) * viewport.height / 2 + viewport.y;
|
|
337
|
-
const ax = viewport.width / 2 * p[0];
|
|
338
|
-
const ay = viewport.height / 2 * p[5];
|
|
339
|
-
return {
|
|
340
|
-
fx: ax,
|
|
341
|
-
fy: ay,
|
|
342
|
-
px: u0,
|
|
343
|
-
py: v0,
|
|
344
|
-
width: viewport.width,
|
|
345
|
-
height: viewport.height
|
|
346
|
-
};
|
|
347
|
-
}
|
|
348
|
-
async function compressToJpeg(buffer, width, height, quality = 0.8) {
|
|
349
|
-
const canvas = document.createElement("canvas");
|
|
350
|
-
const ctx = canvas.getContext("2d");
|
|
351
|
-
canvas.width = width;
|
|
352
|
-
canvas.height = height;
|
|
353
|
-
const imageData = new ImageData(new Uint8ClampedArray(buffer), width, height);
|
|
354
|
-
ctx == null ? void 0 : ctx.putImageData(imageData, 0, 0);
|
|
355
|
-
return new Promise((resolve) => {
|
|
356
|
-
canvas.toBlob((blob) => resolve(blob != null ? blob : new Blob()), "image/jpeg", quality);
|
|
357
|
-
});
|
|
358
|
-
}
|
|
359
|
-
async function getCameraTextureAsImage(renderer, webGLTexture, width, height) {
|
|
360
|
-
const gl = renderer.getContext();
|
|
361
|
-
if (!gl) return null;
|
|
362
|
-
const framebuffer = gl.createFramebuffer();
|
|
363
|
-
if (!framebuffer) return null;
|
|
364
|
-
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
|
|
365
|
-
gl.framebufferTexture2D(
|
|
366
|
-
gl.FRAMEBUFFER,
|
|
367
|
-
gl.COLOR_ATTACHMENT0,
|
|
368
|
-
gl.TEXTURE_2D,
|
|
369
|
-
webGLTexture,
|
|
370
|
-
0
|
|
371
|
-
);
|
|
372
|
-
const pixelBuffer = new Uint8Array(width * height * 4);
|
|
373
|
-
gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixelBuffer);
|
|
374
|
-
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
375
|
-
gl.deleteFramebuffer(framebuffer);
|
|
376
|
-
const flippedData = new Uint8ClampedArray(pixelBuffer.length);
|
|
377
|
-
for (let row = 0; row < height; row += 1) {
|
|
378
|
-
const sourceStart = row * width * 4;
|
|
379
|
-
const destStart = (height - row - 1) * width * 4;
|
|
380
|
-
flippedData.set(
|
|
381
|
-
pixelBuffer.subarray(sourceStart, sourceStart + width * 4),
|
|
382
|
-
destStart
|
|
383
|
-
);
|
|
384
|
-
}
|
|
385
|
-
const blob = await compressToJpeg(flippedData.buffer, width, height, 0.7);
|
|
386
|
-
if (!blob.size) {
|
|
387
|
-
return null;
|
|
388
|
-
}
|
|
389
|
-
return {
|
|
390
|
-
blob,
|
|
391
|
-
width,
|
|
392
|
-
height
|
|
393
|
-
};
|
|
394
|
-
}
|
|
395
800
|
|
|
396
801
|
export { DEFAULT_ENDPOINTS, MultisetClient, WebxrController };
|
|
397
802
|
//# sourceMappingURL=index.js.map
|