@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/webxr/index.js
CHANGED
|
@@ -1,60 +1,376 @@
|
|
|
1
|
-
import * as
|
|
1
|
+
import * as THREE3 from 'three';
|
|
2
2
|
import { ARButton } from 'three/examples/jsm/webxr/ARButton.js';
|
|
3
|
+
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js';
|
|
4
|
+
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
|
|
3
5
|
|
|
4
|
-
// src/lib/webxr/
|
|
6
|
+
// src/lib/webxr/controller.ts
|
|
7
|
+
|
|
8
|
+
// src/lib/webxr/internal/cameraIntrinsics.ts
|
|
9
|
+
function getCameraIntrinsics(projectionMatrix, viewport) {
|
|
10
|
+
const p = projectionMatrix;
|
|
11
|
+
const u0 = (1 - p[8]) * viewport.width / 2 + viewport.x;
|
|
12
|
+
const v0 = (1 - p[9]) * viewport.height / 2 + viewport.y;
|
|
13
|
+
const ax = viewport.width / 2 * p[0];
|
|
14
|
+
const ay = viewport.height / 2 * p[5];
|
|
15
|
+
return {
|
|
16
|
+
fx: ax,
|
|
17
|
+
fy: ay,
|
|
18
|
+
px: u0,
|
|
19
|
+
py: v0,
|
|
20
|
+
width: viewport.width,
|
|
21
|
+
height: viewport.height
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// src/lib/webxr/internal/frameCapture.ts
|
|
26
|
+
async function compressToJpeg(buffer, width, height, quality = 0.8) {
|
|
27
|
+
const canvas = document.createElement("canvas");
|
|
28
|
+
canvas.width = width;
|
|
29
|
+
canvas.height = height;
|
|
30
|
+
const ctx = canvas.getContext("2d");
|
|
31
|
+
if (!ctx) return new Blob();
|
|
32
|
+
const imageData = new ImageData(new Uint8ClampedArray(buffer), width, height);
|
|
33
|
+
ctx.putImageData(imageData, 0, 0);
|
|
34
|
+
return new Promise((resolve) => {
|
|
35
|
+
canvas.toBlob((blob) => resolve(blob != null ? blob : new Blob()), "image/jpeg", quality);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
async function getCameraTextureAsImage(renderer, webGLTexture, width, height) {
|
|
39
|
+
const gl = renderer.getContext();
|
|
40
|
+
if (!gl) return null;
|
|
41
|
+
const framebuffer = gl.createFramebuffer();
|
|
42
|
+
if (!framebuffer) return null;
|
|
43
|
+
let pixelBuffer;
|
|
44
|
+
try {
|
|
45
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
|
|
46
|
+
gl.framebufferTexture2D(
|
|
47
|
+
gl.FRAMEBUFFER,
|
|
48
|
+
gl.COLOR_ATTACHMENT0,
|
|
49
|
+
gl.TEXTURE_2D,
|
|
50
|
+
webGLTexture,
|
|
51
|
+
0
|
|
52
|
+
);
|
|
53
|
+
pixelBuffer = new Uint8Array(width * height * 4);
|
|
54
|
+
gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixelBuffer);
|
|
55
|
+
} finally {
|
|
56
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
57
|
+
gl.deleteFramebuffer(framebuffer);
|
|
58
|
+
}
|
|
59
|
+
const flippedData = new Uint8ClampedArray(pixelBuffer.length);
|
|
60
|
+
for (let row = 0; row < height; row += 1) {
|
|
61
|
+
const sourceStart = row * width * 4;
|
|
62
|
+
const destStart = (height - row - 1) * width * 4;
|
|
63
|
+
flippedData.set(
|
|
64
|
+
pixelBuffer.subarray(sourceStart, sourceStart + width * 4),
|
|
65
|
+
destStart
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
const blob = await compressToJpeg(flippedData.buffer, width, height, 0.7);
|
|
69
|
+
if (!blob.size) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
blob,
|
|
74
|
+
width,
|
|
75
|
+
height
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
var Experience = class {
|
|
79
|
+
constructor(canvas) {
|
|
80
|
+
this.renderer = new THREE3.WebGLRenderer({
|
|
81
|
+
canvas,
|
|
82
|
+
antialias: true,
|
|
83
|
+
alpha: true
|
|
84
|
+
});
|
|
85
|
+
this.renderer.setSize(window.innerWidth, window.innerHeight);
|
|
86
|
+
this.renderer.setPixelRatio(window.devicePixelRatio);
|
|
87
|
+
this.renderer.xr.enabled = true;
|
|
88
|
+
this.camera = new THREE3.PerspectiveCamera(
|
|
89
|
+
45,
|
|
90
|
+
window.innerWidth / window.innerHeight,
|
|
91
|
+
0.2,
|
|
92
|
+
1e4
|
|
93
|
+
);
|
|
94
|
+
this.scene = new THREE3.Scene();
|
|
95
|
+
this.setupLights();
|
|
96
|
+
}
|
|
97
|
+
setupLights() {
|
|
98
|
+
const light = new THREE3.HemisphereLight(16777215, 16314623, 1);
|
|
99
|
+
light.position.set(0.5, 2, 0.25);
|
|
100
|
+
this.scene.add(light);
|
|
101
|
+
const diLight = new THREE3.DirectionalLight("#7B2CBF");
|
|
102
|
+
diLight.position.set(0, 2, 0);
|
|
103
|
+
this.scene.add(diLight);
|
|
104
|
+
}
|
|
105
|
+
getScene() {
|
|
106
|
+
return this.scene;
|
|
107
|
+
}
|
|
108
|
+
getCamera() {
|
|
109
|
+
return this.camera;
|
|
110
|
+
}
|
|
111
|
+
getRenderer() {
|
|
112
|
+
return this.renderer;
|
|
113
|
+
}
|
|
114
|
+
resize() {
|
|
115
|
+
this.camera.aspect = window.innerWidth / window.innerHeight;
|
|
116
|
+
this.camera.updateProjectionMatrix();
|
|
117
|
+
this.renderer.setSize(window.innerWidth, window.innerHeight);
|
|
118
|
+
}
|
|
119
|
+
dispose() {
|
|
120
|
+
this.renderer.dispose();
|
|
121
|
+
this.scene = null;
|
|
122
|
+
this.camera = null;
|
|
123
|
+
this.renderer = null;
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
var VERTEX = `
|
|
127
|
+
varying vec3 vWorldPosition;
|
|
128
|
+
|
|
129
|
+
void main() {
|
|
130
|
+
vec4 worldPos = modelMatrix * vec4(position, 1.0);
|
|
131
|
+
vWorldPosition = worldPos.xyz;
|
|
132
|
+
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
|
133
|
+
}
|
|
134
|
+
`;
|
|
135
|
+
var FRAGMENT = `
|
|
136
|
+
uniform vec3 uColor;
|
|
137
|
+
uniform float uOpacity;
|
|
138
|
+
uniform vec3 uGridColor;
|
|
139
|
+
uniform float uGridScale;
|
|
140
|
+
|
|
141
|
+
varying vec3 vWorldPosition;
|
|
142
|
+
|
|
143
|
+
void main() {
|
|
144
|
+
vec2 coord = vWorldPosition.xz * uGridScale;
|
|
145
|
+
vec2 grid = abs(fract(coord - 0.5) - 0.5) / fwidth(coord);
|
|
146
|
+
float line = min(grid.x, grid.y) / 2.0;
|
|
147
|
+
float gridLine = 1.0 - min(line, 1.0);
|
|
148
|
+
|
|
149
|
+
vec3 base = uColor;
|
|
150
|
+
vec3 gridCol = uGridColor;
|
|
151
|
+
float alpha = mix(uOpacity, 1.0, gridLine);
|
|
152
|
+
vec3 color = mix(base, gridCol, gridLine);
|
|
153
|
+
gl_FragColor = vec4(color, alpha);
|
|
154
|
+
}
|
|
155
|
+
`;
|
|
156
|
+
function createGridMaterial(options = {}) {
|
|
157
|
+
var _a, _b, _c, _d;
|
|
158
|
+
const color = (_a = options.color) != null ? _a : "#7B2CBF";
|
|
159
|
+
const opacity = (_b = options.opacity) != null ? _b : 0.6;
|
|
160
|
+
const gridColor = (_c = options.gridColor) != null ? _c : "#ffeb3b";
|
|
161
|
+
const gridScale = (_d = options.gridScale) != null ? _d : 2;
|
|
162
|
+
return new THREE3.ShaderMaterial({
|
|
163
|
+
vertexShader: VERTEX,
|
|
164
|
+
fragmentShader: FRAGMENT,
|
|
165
|
+
transparent: true,
|
|
166
|
+
side: THREE3.DoubleSide,
|
|
167
|
+
uniforms: {
|
|
168
|
+
uColor: { value: typeof color === "string" ? new THREE3.Color(color) : color },
|
|
169
|
+
uOpacity: { value: opacity },
|
|
170
|
+
uGridColor: { value: typeof gridColor === "string" ? new THREE3.Color(gridColor) : gridColor },
|
|
171
|
+
uGridScale: { value: gridScale }
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// src/lib/webxr/mapMeshVisualizer.ts
|
|
177
|
+
var LARGE_MAP_THRESHOLD = 50;
|
|
178
|
+
var MapMeshVisualizer = class {
|
|
179
|
+
constructor(scene, client) {
|
|
180
|
+
this.scene = scene;
|
|
181
|
+
this.client = client;
|
|
182
|
+
this.meshGroup = new THREE3.Group();
|
|
183
|
+
this.meshGroup.visible = false;
|
|
184
|
+
this.scene.add(this.meshGroup);
|
|
185
|
+
this.dracoLoader = new DRACOLoader();
|
|
186
|
+
this.dracoLoader.setDecoderPath("/draco/");
|
|
187
|
+
this.gltfLoader = new GLTFLoader();
|
|
188
|
+
this.gltfLoader.setDRACOLoader(this.dracoLoader);
|
|
189
|
+
}
|
|
190
|
+
getMeshGroup() {
|
|
191
|
+
return this.meshGroup;
|
|
192
|
+
}
|
|
193
|
+
async ensureMeshLoaded(mapDetails) {
|
|
194
|
+
var _a, _b;
|
|
195
|
+
if (this.scene.getObjectByName(mapDetails._id)) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const meshKey = (_b = (_a = mapDetails.mapMesh) == null ? void 0 : _a.rawMesh) == null ? void 0 : _b.meshLink;
|
|
199
|
+
if (!meshKey) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const url = await this.client.downloadFile(meshKey);
|
|
203
|
+
if (!url) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const meshMaterial = createGridMaterial({
|
|
207
|
+
color: "#7B2CBF",
|
|
208
|
+
opacity: 0.6,
|
|
209
|
+
gridColor: "#ffeb3b",
|
|
210
|
+
gridScale: 2
|
|
211
|
+
});
|
|
212
|
+
await new Promise((resolve, reject) => {
|
|
213
|
+
this.gltfLoader.load(
|
|
214
|
+
url,
|
|
215
|
+
(gltf) => {
|
|
216
|
+
gltf.scene.traverse((child) => {
|
|
217
|
+
if (child.isMesh) {
|
|
218
|
+
child.material = meshMaterial;
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
const box = new THREE3.Box3().setFromObject(gltf.scene);
|
|
222
|
+
let size = box.getSize(new THREE3.Vector3()).length();
|
|
223
|
+
box.getCenter(new THREE3.Vector3());
|
|
224
|
+
if (size > LARGE_MAP_THRESHOLD) {
|
|
225
|
+
size = LARGE_MAP_THRESHOLD;
|
|
226
|
+
new THREE3.Vector3();
|
|
227
|
+
}
|
|
228
|
+
gltf.scene.name = mapDetails._id;
|
|
229
|
+
this.meshGroup.add(gltf.scene);
|
|
230
|
+
resolve();
|
|
231
|
+
},
|
|
232
|
+
void 0,
|
|
233
|
+
(error) => {
|
|
234
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
235
|
+
}
|
|
236
|
+
);
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
applyMeshTransform(best, trackerSpace) {
|
|
240
|
+
const pose = best.localizeData;
|
|
241
|
+
const resPosition = new THREE3.Vector3(
|
|
242
|
+
pose.position.x,
|
|
243
|
+
pose.position.y,
|
|
244
|
+
pose.position.z
|
|
245
|
+
);
|
|
246
|
+
const resRotation = new THREE3.Quaternion(
|
|
247
|
+
pose.rotation.x,
|
|
248
|
+
pose.rotation.y,
|
|
249
|
+
pose.rotation.z,
|
|
250
|
+
pose.rotation.w
|
|
251
|
+
);
|
|
252
|
+
const responseMatrix = new THREE3.Matrix4();
|
|
253
|
+
responseMatrix.compose(resPosition, resRotation, new THREE3.Vector3(1, 1, 1));
|
|
254
|
+
const inverseResponseMatrix = responseMatrix.clone().invert();
|
|
255
|
+
const resultantMatrix = new THREE3.Matrix4();
|
|
256
|
+
resultantMatrix.multiplyMatrices(trackerSpace, inverseResponseMatrix);
|
|
257
|
+
const position = new THREE3.Vector3();
|
|
258
|
+
const rotation = new THREE3.Quaternion();
|
|
259
|
+
const scale = new THREE3.Vector3();
|
|
260
|
+
resultantMatrix.decompose(position, rotation, scale);
|
|
261
|
+
this.meshGroup.position.copy(position);
|
|
262
|
+
this.meshGroup.quaternion.copy(rotation);
|
|
263
|
+
this.meshGroup.scale.set(1, 1, 1);
|
|
264
|
+
this.meshGroup.visible = true;
|
|
265
|
+
this.meshGroup.updateMatrix();
|
|
266
|
+
}
|
|
267
|
+
dispose() {
|
|
268
|
+
this.meshGroup.traverse((child) => {
|
|
269
|
+
const mesh = child;
|
|
270
|
+
if (mesh.isMesh) {
|
|
271
|
+
mesh.geometry.dispose();
|
|
272
|
+
if (Array.isArray(mesh.material)) {
|
|
273
|
+
mesh.material.forEach((m) => m.dispose());
|
|
274
|
+
} else {
|
|
275
|
+
mesh.material.dispose();
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
this.scene.remove(this.meshGroup);
|
|
280
|
+
this.dracoLoader.dispose();
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
// src/lib/webxr/world/World.ts
|
|
285
|
+
var World = class {
|
|
286
|
+
constructor(scene, client) {
|
|
287
|
+
this.meshVisualizer = new MapMeshVisualizer(scene, client);
|
|
288
|
+
}
|
|
289
|
+
async ensureMeshLoaded(mapDetails) {
|
|
290
|
+
await this.meshVisualizer.ensureMeshLoaded(mapDetails);
|
|
291
|
+
}
|
|
292
|
+
applyMeshTransform(best, trackerSpace) {
|
|
293
|
+
this.meshVisualizer.applyMeshTransform(best, trackerSpace);
|
|
294
|
+
}
|
|
295
|
+
dispose() {
|
|
296
|
+
this.meshVisualizer.dispose();
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
// src/lib/webxr/controller.ts
|
|
301
|
+
function clamp(value, min, max) {
|
|
302
|
+
return Math.max(min, Math.min(max, value));
|
|
303
|
+
}
|
|
304
|
+
var REQUEST_ATTEMPTS_MIN = 1;
|
|
305
|
+
var REQUEST_ATTEMPTS_MAX = 5;
|
|
306
|
+
var LOCALIZATION_INTERVAL_MIN = 1;
|
|
307
|
+
var LOCALIZATION_INTERVAL_MAX = 5;
|
|
308
|
+
var CONFIDENCE_THRESHOLD_MIN = 0.2;
|
|
309
|
+
var CONFIDENCE_THRESHOLD_MAX = 0.8;
|
|
310
|
+
var TRACKING_LOSS_FRAME_THRESHOLD = 60;
|
|
5
311
|
var WebxrController = class {
|
|
6
312
|
constructor(options) {
|
|
7
313
|
this.options = options;
|
|
8
|
-
this.
|
|
9
|
-
this.
|
|
10
|
-
this.scene = null;
|
|
11
|
-
this.animationLoop = null;
|
|
314
|
+
this.experience = null;
|
|
315
|
+
this.world = null;
|
|
12
316
|
this.arButton = null;
|
|
13
317
|
this.resizeHandler = null;
|
|
14
318
|
this.isSessionActive = false;
|
|
319
|
+
this.trackingLossFrames = 0;
|
|
320
|
+
this.isLocalizing = false;
|
|
321
|
+
this.trackerSpace = null;
|
|
15
322
|
}
|
|
16
323
|
async initialize(buttonContainer) {
|
|
17
324
|
var _a, _b, _c, _d;
|
|
18
|
-
if (this.
|
|
325
|
+
if (this.experience) {
|
|
19
326
|
return this.arButton;
|
|
20
327
|
}
|
|
21
328
|
if (!window.isSecureContext) {
|
|
22
329
|
throw new Error("WebXR requires a secure context (HTTPS).");
|
|
23
330
|
}
|
|
24
331
|
const canvas = (_a = this.options.canvas) != null ? _a : document.createElement("canvas");
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
31
|
-
renderer.setPixelRatio(window.devicePixelRatio);
|
|
32
|
-
renderer.xr.enabled = true;
|
|
332
|
+
this.experience = new Experience(canvas);
|
|
333
|
+
this.world = new World(this.experience.getScene(), this.options.client);
|
|
334
|
+
const renderer = this.experience.getRenderer();
|
|
335
|
+
const camera = this.experience.getCamera();
|
|
336
|
+
const scene = this.experience.getScene();
|
|
33
337
|
renderer.xr.addEventListener("sessionstart", () => {
|
|
34
338
|
var _a2, _b2;
|
|
35
339
|
this.isSessionActive = true;
|
|
36
340
|
(_b2 = (_a2 = this.options).onSessionStart) == null ? void 0 : _b2.call(_a2);
|
|
341
|
+
const cfg = this.options.client.getConfig();
|
|
342
|
+
if (cfg.autoLocalize) {
|
|
343
|
+
void this.localizeFrame();
|
|
344
|
+
}
|
|
37
345
|
});
|
|
38
346
|
renderer.xr.addEventListener("sessionend", () => {
|
|
39
347
|
var _a2, _b2;
|
|
40
348
|
this.isSessionActive = false;
|
|
41
349
|
(_b2 = (_a2 = this.options).onSessionEnd) == null ? void 0 : _b2.call(_a2);
|
|
42
350
|
});
|
|
43
|
-
const
|
|
44
|
-
45,
|
|
45
|
-
window.innerWidth / window.innerHeight,
|
|
46
|
-
0.2,
|
|
47
|
-
1e4
|
|
48
|
-
);
|
|
49
|
-
const scene = new THREE.Scene();
|
|
50
|
-
const animationLoop = () => {
|
|
351
|
+
const animationLoop = (_time, frame) => {
|
|
51
352
|
renderer.render(scene, camera);
|
|
353
|
+
const cfg = this.options.client.getConfig();
|
|
354
|
+
if (cfg.relocalization && frame && !this.isLocalizing) {
|
|
355
|
+
const refSpace = renderer.xr.getReferenceSpace();
|
|
356
|
+
if (refSpace) {
|
|
357
|
+
const viewerPose = frame.getViewerPose(refSpace);
|
|
358
|
+
if (!viewerPose) {
|
|
359
|
+
this.trackingLossFrames += 1;
|
|
360
|
+
if (this.trackingLossFrames >= TRACKING_LOSS_FRAME_THRESHOLD) {
|
|
361
|
+
this.trackingLossFrames = 0;
|
|
362
|
+
void this.localizeFrame();
|
|
363
|
+
}
|
|
364
|
+
} else {
|
|
365
|
+
this.trackingLossFrames = 0;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
52
369
|
};
|
|
53
370
|
renderer.setAnimationLoop(animationLoop);
|
|
54
371
|
const resizeHandler = () => {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
372
|
+
var _a2;
|
|
373
|
+
(_a2 = this.experience) == null ? void 0 : _a2.resize();
|
|
58
374
|
};
|
|
59
375
|
window.addEventListener("resize", resizeHandler);
|
|
60
376
|
const overlayRoot = (_b = this.options.overlayRoot) != null ? _b : document.body;
|
|
@@ -66,51 +382,142 @@ var WebxrController = class {
|
|
|
66
382
|
if (!buttonParent.contains(arButton)) {
|
|
67
383
|
buttonParent.appendChild(arButton);
|
|
68
384
|
}
|
|
69
|
-
this.renderer = renderer;
|
|
70
|
-
this.camera = camera;
|
|
71
|
-
this.scene = scene;
|
|
72
|
-
this.animationLoop = animationLoop;
|
|
73
385
|
this.arButton = arButton;
|
|
74
386
|
this.resizeHandler = resizeHandler;
|
|
75
387
|
(_d = (_c = this.options).onARButtonCreated) == null ? void 0 : _d.call(_c, arButton);
|
|
76
388
|
return arButton;
|
|
77
389
|
}
|
|
78
390
|
getScene() {
|
|
79
|
-
if (!this.
|
|
391
|
+
if (!this.experience) {
|
|
80
392
|
throw new Error("Scene: WebXR controller has not been initialized.");
|
|
81
393
|
}
|
|
82
|
-
return this.
|
|
394
|
+
return this.experience.getScene();
|
|
83
395
|
}
|
|
84
396
|
getCamera() {
|
|
85
|
-
if (!this.
|
|
397
|
+
if (!this.experience) {
|
|
86
398
|
throw new Error("Camera: WebXR controller has not been initialized.");
|
|
87
399
|
}
|
|
88
|
-
return this.
|
|
400
|
+
return this.experience.getCamera();
|
|
89
401
|
}
|
|
90
402
|
getRenderer() {
|
|
91
|
-
if (!this.
|
|
403
|
+
if (!this.experience) {
|
|
92
404
|
throw new Error("Renderer: WebXR controller has not been initialized.");
|
|
93
405
|
}
|
|
94
|
-
return this.
|
|
406
|
+
return this.experience.getRenderer();
|
|
95
407
|
}
|
|
96
408
|
hasActiveSession() {
|
|
97
409
|
var _a;
|
|
98
|
-
return this.isSessionActive && ((_a = this.
|
|
410
|
+
return this.isSessionActive && ((_a = this.experience) == null ? void 0 : _a.getRenderer().xr.isPresenting) === true;
|
|
99
411
|
}
|
|
412
|
+
/**
|
|
413
|
+
* Runs a single-frame localization cycle: multiple attempts (per requestAttempts),
|
|
414
|
+
* picks best result by confidence, optionally validates against confidenceThreshold,
|
|
415
|
+
* and fires onLocalizationInit / onLocalizationSuccess / onLocalizationFailure.
|
|
416
|
+
* Aligns with Unity SingleFrameLocalizationManager.LocalizeFrame().
|
|
417
|
+
*/
|
|
418
|
+
async localizeFrame() {
|
|
419
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l;
|
|
420
|
+
if (!this.experience) {
|
|
421
|
+
throw new Error("WebXR: WebXR controller has not been initialized.");
|
|
422
|
+
}
|
|
423
|
+
const renderer = this.experience.getRenderer();
|
|
424
|
+
const session = (_b = (_a = renderer.xr).getSession) == null ? void 0 : _b.call(_a);
|
|
425
|
+
if (!session) {
|
|
426
|
+
throw new Error(
|
|
427
|
+
"WebXR Session: No active WebXR session. Start AR before calling localizeFrame()."
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
const cfg = this.options.client.getConfig();
|
|
431
|
+
const requestAttempts = clamp(
|
|
432
|
+
(_c = cfg.requestAttempts) != null ? _c : 1,
|
|
433
|
+
REQUEST_ATTEMPTS_MIN,
|
|
434
|
+
REQUEST_ATTEMPTS_MAX
|
|
435
|
+
);
|
|
436
|
+
const localizationInterval = clamp(
|
|
437
|
+
(_d = cfg.localizationInterval) != null ? _d : 1,
|
|
438
|
+
LOCALIZATION_INTERVAL_MIN,
|
|
439
|
+
LOCALIZATION_INTERVAL_MAX
|
|
440
|
+
);
|
|
441
|
+
const confidenceCheck = (_e = cfg.confidenceCheck) != null ? _e : false;
|
|
442
|
+
const confidenceThreshold = clamp(
|
|
443
|
+
(_f = cfg.confidenceThreshold) != null ? _f : 0.5,
|
|
444
|
+
CONFIDENCE_THRESHOLD_MIN,
|
|
445
|
+
CONFIDENCE_THRESHOLD_MAX
|
|
446
|
+
);
|
|
447
|
+
(_g = cfg.onLocalizationInit) == null ? void 0 : _g.call(cfg);
|
|
448
|
+
this.isLocalizing = true;
|
|
449
|
+
const results = [];
|
|
450
|
+
const MAX_FRAME_ATTEMPTS = requestAttempts * 3;
|
|
451
|
+
try {
|
|
452
|
+
let apiCallsDone = 0;
|
|
453
|
+
let frameAttempts = 0;
|
|
454
|
+
while (apiCallsDone < requestAttempts && frameAttempts < MAX_FRAME_ATTEMPTS) {
|
|
455
|
+
frameAttempts++;
|
|
456
|
+
try {
|
|
457
|
+
const { result, apiCalled } = await this.captureFrame();
|
|
458
|
+
if (apiCalled) {
|
|
459
|
+
apiCallsDone++;
|
|
460
|
+
if ((_h = result == null ? void 0 : result.localizeData) == null ? void 0 : _h.poseFound) {
|
|
461
|
+
results.push(result);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
} catch {
|
|
465
|
+
apiCallsDone++;
|
|
466
|
+
}
|
|
467
|
+
if (apiCallsDone < requestAttempts && frameAttempts < MAX_FRAME_ATTEMPTS) {
|
|
468
|
+
await new Promise(
|
|
469
|
+
(r) => setTimeout(r, localizationInterval * 1e3)
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
} finally {
|
|
474
|
+
this.isLocalizing = false;
|
|
475
|
+
}
|
|
476
|
+
const best = results.length ? results.sort(
|
|
477
|
+
(a, b) => {
|
|
478
|
+
var _a2, _b2;
|
|
479
|
+
return ((_a2 = b.localizeData.confidence) != null ? _a2 : 0) - ((_b2 = a.localizeData.confidence) != null ? _b2 : 0);
|
|
480
|
+
}
|
|
481
|
+
)[0] : null;
|
|
482
|
+
const accepted = best && (!confidenceCheck || ((_i = best.localizeData.confidence) != null ? _i : 0) >= confidenceThreshold);
|
|
483
|
+
if (accepted && best) {
|
|
484
|
+
(_j = cfg.onLocalizationSuccess) == null ? void 0 : _j.call(cfg, best);
|
|
485
|
+
if (cfg.showMesh && best.mapDetails && this.world && this.trackerSpace) {
|
|
486
|
+
try {
|
|
487
|
+
await this.world.ensureMeshLoaded(best.mapDetails);
|
|
488
|
+
this.world.applyMeshTransform(best, this.trackerSpace);
|
|
489
|
+
} catch {
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
return best;
|
|
493
|
+
}
|
|
494
|
+
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;
|
|
495
|
+
(_l = cfg.onLocalizationFailure) == null ? void 0 : _l.call(cfg, reason);
|
|
496
|
+
return null;
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Internal: captures one frame and calls the localization API. Returns the
|
|
500
|
+
* result and whether the API was actually invoked — used by localizeFrame()
|
|
501
|
+
* to count only real API calls toward requestAttempts (frames with no camera
|
|
502
|
+
* image are skipped).
|
|
503
|
+
*/
|
|
100
504
|
async captureFrame() {
|
|
101
505
|
var _a, _b;
|
|
102
|
-
|
|
103
|
-
const camera = this.camera;
|
|
104
|
-
if (!renderer || !camera) {
|
|
506
|
+
if (!this.experience) {
|
|
105
507
|
throw new Error("WebXR: WebXR controller has not been initialized.");
|
|
106
508
|
}
|
|
509
|
+
const renderer = this.experience.getRenderer();
|
|
107
510
|
const session = (_b = (_a = renderer.xr).getSession) == null ? void 0 : _b.call(_a);
|
|
108
511
|
if (!session) {
|
|
109
|
-
throw new Error(
|
|
512
|
+
throw new Error(
|
|
513
|
+
"WebXR Session: No active WebXR session. Start AR before capturing."
|
|
514
|
+
);
|
|
110
515
|
}
|
|
111
516
|
const referenceSpace = renderer.xr.getReferenceSpace();
|
|
112
517
|
if (!referenceSpace) {
|
|
113
|
-
throw new Error(
|
|
518
|
+
throw new Error(
|
|
519
|
+
"WebXR Reference Space: Unable to acquire XR reference space."
|
|
520
|
+
);
|
|
114
521
|
}
|
|
115
522
|
const gl = renderer.getContext();
|
|
116
523
|
return new Promise((resolve, reject) => {
|
|
@@ -119,7 +526,7 @@ var WebxrController = class {
|
|
|
119
526
|
try {
|
|
120
527
|
const viewerPose = xrFrame.getViewerPose(referenceSpace);
|
|
121
528
|
if (!viewerPose) {
|
|
122
|
-
resolve(null);
|
|
529
|
+
resolve({ result: null, apiCalled: false });
|
|
123
530
|
return;
|
|
124
531
|
}
|
|
125
532
|
for (const view of viewerPose.views) {
|
|
@@ -146,15 +553,16 @@ var WebxrController = class {
|
|
|
146
553
|
y: 0
|
|
147
554
|
});
|
|
148
555
|
if (frameData && intrinsics) {
|
|
556
|
+
this.trackerSpace = new THREE3.Matrix4().fromArray(view.transform.matrix);
|
|
149
557
|
const result = await this.options.client.localizeWithFrame(
|
|
150
558
|
frameData,
|
|
151
559
|
intrinsics
|
|
152
560
|
);
|
|
153
|
-
resolve(result);
|
|
561
|
+
resolve({ result, apiCalled: true });
|
|
154
562
|
return;
|
|
155
563
|
}
|
|
156
564
|
}
|
|
157
|
-
resolve(null);
|
|
565
|
+
resolve({ result: null, apiCalled: false });
|
|
158
566
|
} catch (error) {
|
|
159
567
|
reject(error);
|
|
160
568
|
} finally {
|
|
@@ -167,85 +575,22 @@ var WebxrController = class {
|
|
|
167
575
|
});
|
|
168
576
|
}
|
|
169
577
|
dispose() {
|
|
170
|
-
var _a;
|
|
578
|
+
var _a, _b, _c, _d;
|
|
171
579
|
if (this.resizeHandler) {
|
|
172
580
|
window.removeEventListener("resize", this.resizeHandler);
|
|
173
581
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
this.
|
|
179
|
-
this.
|
|
180
|
-
this.
|
|
181
|
-
if ((_a = this.arButton) == null ? void 0 : _a.parentElement) {
|
|
582
|
+
(_a = this.experience) == null ? void 0 : _a.getRenderer().setAnimationLoop(null);
|
|
583
|
+
(_b = this.experience) == null ? void 0 : _b.dispose();
|
|
584
|
+
this.experience = null;
|
|
585
|
+
(_c = this.world) == null ? void 0 : _c.dispose();
|
|
586
|
+
this.world = null;
|
|
587
|
+
this.trackingLossFrames = 0;
|
|
588
|
+
if ((_d = this.arButton) == null ? void 0 : _d.parentElement) {
|
|
182
589
|
this.arButton.parentElement.removeChild(this.arButton);
|
|
183
590
|
}
|
|
184
591
|
this.arButton = null;
|
|
185
592
|
}
|
|
186
593
|
};
|
|
187
|
-
function getCameraIntrinsics(projectionMatrix, viewport) {
|
|
188
|
-
const p = projectionMatrix;
|
|
189
|
-
const u0 = (1 - p[8]) * viewport.width / 2 + viewport.x;
|
|
190
|
-
const v0 = (1 - p[9]) * viewport.height / 2 + viewport.y;
|
|
191
|
-
const ax = viewport.width / 2 * p[0];
|
|
192
|
-
const ay = viewport.height / 2 * p[5];
|
|
193
|
-
return {
|
|
194
|
-
fx: ax,
|
|
195
|
-
fy: ay,
|
|
196
|
-
px: u0,
|
|
197
|
-
py: v0,
|
|
198
|
-
width: viewport.width,
|
|
199
|
-
height: viewport.height
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
async function compressToJpeg(buffer, width, height, quality = 0.8) {
|
|
203
|
-
const canvas = document.createElement("canvas");
|
|
204
|
-
const ctx = canvas.getContext("2d");
|
|
205
|
-
canvas.width = width;
|
|
206
|
-
canvas.height = height;
|
|
207
|
-
const imageData = new ImageData(new Uint8ClampedArray(buffer), width, height);
|
|
208
|
-
ctx == null ? void 0 : ctx.putImageData(imageData, 0, 0);
|
|
209
|
-
return new Promise((resolve) => {
|
|
210
|
-
canvas.toBlob((blob) => resolve(blob != null ? blob : new Blob()), "image/jpeg", quality);
|
|
211
|
-
});
|
|
212
|
-
}
|
|
213
|
-
async function getCameraTextureAsImage(renderer, webGLTexture, width, height) {
|
|
214
|
-
const gl = renderer.getContext();
|
|
215
|
-
if (!gl) return null;
|
|
216
|
-
const framebuffer = gl.createFramebuffer();
|
|
217
|
-
if (!framebuffer) return null;
|
|
218
|
-
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
|
|
219
|
-
gl.framebufferTexture2D(
|
|
220
|
-
gl.FRAMEBUFFER,
|
|
221
|
-
gl.COLOR_ATTACHMENT0,
|
|
222
|
-
gl.TEXTURE_2D,
|
|
223
|
-
webGLTexture,
|
|
224
|
-
0
|
|
225
|
-
);
|
|
226
|
-
const pixelBuffer = new Uint8Array(width * height * 4);
|
|
227
|
-
gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixelBuffer);
|
|
228
|
-
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
229
|
-
gl.deleteFramebuffer(framebuffer);
|
|
230
|
-
const flippedData = new Uint8ClampedArray(pixelBuffer.length);
|
|
231
|
-
for (let row = 0; row < height; row += 1) {
|
|
232
|
-
const sourceStart = row * width * 4;
|
|
233
|
-
const destStart = (height - row - 1) * width * 4;
|
|
234
|
-
flippedData.set(
|
|
235
|
-
pixelBuffer.subarray(sourceStart, sourceStart + width * 4),
|
|
236
|
-
destStart
|
|
237
|
-
);
|
|
238
|
-
}
|
|
239
|
-
const blob = await compressToJpeg(flippedData.buffer, width, height, 0.7);
|
|
240
|
-
if (!blob.size) {
|
|
241
|
-
return null;
|
|
242
|
-
}
|
|
243
|
-
return {
|
|
244
|
-
blob,
|
|
245
|
-
width,
|
|
246
|
-
height
|
|
247
|
-
};
|
|
248
|
-
}
|
|
249
594
|
|
|
250
595
|
export { WebxrController };
|
|
251
596
|
//# sourceMappingURL=index.js.map
|