@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/dist/index.js CHANGED
@@ -1,8 +1,10 @@
1
1
  import axios from 'axios';
2
- import * as THREE from 'three';
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/index.ts
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.mapIds) == null ? void 0 : _a.length) {
107
- const mapDetails = await this.fetchMapDetails(data.mapIds[0]);
108
- if (mapDetails) {
109
- result.mapDetails = mapDetails;
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(mapId) {
180
+ async fetchMapDetails(mapCode) {
119
181
  try {
120
182
  const response = await axios.get(
121
- `${this.endpoints.mapDetailsUrl}${mapId}`,
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.renderer = null;
155
- this.camera = null;
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.renderer) {
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
- const renderer = new THREE.WebGLRenderer({
172
- canvas,
173
- antialias: true,
174
- alpha: true
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 camera = new THREE.PerspectiveCamera(
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
- camera.aspect = window.innerWidth / window.innerHeight;
202
- camera.updateProjectionMatrix();
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.scene) {
597
+ if (!this.experience) {
226
598
  throw new Error("Scene: WebXR controller has not been initialized.");
227
599
  }
228
- return this.scene;
600
+ return this.experience.getScene();
229
601
  }
230
602
  getCamera() {
231
- if (!this.camera) {
603
+ if (!this.experience) {
232
604
  throw new Error("Camera: WebXR controller has not been initialized.");
233
605
  }
234
- return this.camera;
606
+ return this.experience.getCamera();
235
607
  }
236
608
  getRenderer() {
237
- if (!this.renderer) {
609
+ if (!this.experience) {
238
610
  throw new Error("Renderer: WebXR controller has not been initialized.");
239
611
  }
240
- return this.renderer;
612
+ return this.experience.getRenderer();
241
613
  }
242
614
  hasActiveSession() {
243
615
  var _a;
244
- return this.isSessionActive && ((_a = this.renderer) == null ? void 0 : _a.xr.isPresenting) === true;
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
- const renderer = this.renderer;
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("WebXR Session: No active WebXR session. Start AR before capturing.");
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("WebXR Reference Space: Unable to acquire XR reference space.");
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
- if (this.renderer) {
321
- this.renderer.dispose();
322
- this.renderer = null;
323
- }
324
- this.animationLoop = null;
325
- this.camera = null;
326
- this.scene = null;
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