@needle-tools/engine 5.1.6 → 5.1.7
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/CHANGELOG.md +7 -0
- package/components.needle.json +1 -1
- package/dist/{needle-engine.bundle-FRzWLdVf.umd.cjs → needle-engine.bundle-1QN--joB.umd.cjs} +140 -140
- package/dist/{needle-engine.bundle-BcyvM7a6.js → needle-engine.bundle-DT6F1AXh.js} +4839 -4791
- package/dist/{needle-engine.bundle-CadxfFMM.min.js → needle-engine.bundle-DlX4Rcl_.min.js} +140 -140
- package/dist/needle-engine.d.ts +33 -15
- package/dist/needle-engine.js +28 -28
- package/dist/needle-engine.min.js +1 -1
- package/dist/needle-engine.umd.cjs +1 -1
- package/lib/engine/api.d.ts +1 -1
- package/lib/engine/api.js +1 -1
- package/lib/engine/api.js.map +1 -1
- package/lib/engine/engine_init.js +2 -2
- package/lib/engine/engine_license.d.ts +7 -7
- package/lib/engine/engine_license.js +71 -71
- package/lib/engine/engine_license.js.map +1 -1
- package/lib/engine/engine_networking_blob.js +3 -3
- package/lib/engine/engine_networking_blob.js.map +1 -1
- package/lib/engine/engine_utils.js +6 -0
- package/lib/engine/engine_utils.js.map +1 -1
- package/lib/engine/engine_utils_qrcode.js +2 -2
- package/lib/engine/engine_utils_qrcode.js.map +1 -1
- package/lib/engine/webcomponents/needle menu/needle-menu-spatial.js +2 -2
- package/lib/engine/webcomponents/needle menu/needle-menu-spatial.js.map +1 -1
- package/lib/engine/webcomponents/needle menu/needle-menu.js +5 -5
- package/lib/engine/webcomponents/needle menu/needle-menu.js.map +1 -1
- package/lib/engine/webcomponents/needle-engine.js +2 -2
- package/lib/engine/webcomponents/needle-engine.js.map +1 -1
- package/lib/engine/webcomponents/needle-engine.loading.js +2 -2
- package/lib/engine/webcomponents/needle-engine.loading.js.map +1 -1
- package/lib/engine/xr/NeedleXRSession.js +20 -2
- package/lib/engine/xr/NeedleXRSession.js.map +1 -1
- package/lib/engine/xr/TempXRContext.js +2 -2
- package/lib/engine/xr/TempXRContext.js.map +1 -1
- package/lib/engine-components/export/usdz/USDZExporter.js +4 -4
- package/lib/engine-components/export/usdz/USDZExporter.js.map +1 -1
- package/lib/engine-components/webxr/WebARSessionRoot.d.ts +24 -6
- package/lib/engine-components/webxr/WebARSessionRoot.js +130 -39
- package/lib/engine-components/webxr/WebARSessionRoot.js.map +1 -1
- package/lib/engine-components/webxr/WebXR.d.ts +3 -3
- package/lib/engine-components/webxr/WebXR.js +4 -4
- package/lib/engine-components/webxr/WebXR.js.map +1 -1
- package/package.json +1 -1
- package/plugins/common/license.js +23 -5
- package/plugins/vite/dependencies.js +34 -0
- package/plugins/vite/license.js +4 -4
- package/plugins/vite/needle-app.js +13 -0
- package/src/engine/api.ts +1 -1
- package/src/engine/engine_init.ts +2 -2
- package/src/engine/engine_license.ts +68 -68
- package/src/engine/engine_networking_blob.ts +3 -3
- package/src/engine/engine_utils.ts +4 -0
- package/src/engine/engine_utils_qrcode.ts +2 -2
- package/src/engine/webcomponents/needle menu/needle-menu-spatial.ts +2 -2
- package/src/engine/webcomponents/needle menu/needle-menu.ts +5 -5
- package/src/engine/webcomponents/needle-engine.loading.ts +6 -6
- package/src/engine/webcomponents/needle-engine.ts +2 -2
- package/src/engine/xr/NeedleXRSession.ts +19 -2
- package/src/engine/xr/TempXRContext.ts +2 -2
- package/src/engine-components/export/usdz/USDZExporter.ts +4 -4
- package/src/engine-components/webxr/WebARSessionRoot.ts +124 -38
- package/src/engine-components/webxr/WebXR.ts +4 -4
|
@@ -18,6 +18,22 @@ const debug = getParam("debugwebxr");
|
|
|
18
18
|
|
|
19
19
|
const invertForwardMatrix = new Matrix4().makeRotationY(Math.PI);
|
|
20
20
|
|
|
21
|
+
/** a XRRigidTransform pose stored as plain values — XR hit test results and frames are only
|
|
22
|
+
* valid within the animation frame that produced them, so poses that need to survive until a
|
|
23
|
+
* later frame (or an input event) must be snapshotted */
|
|
24
|
+
type XRAnchorRefPose = { x: number, y: number, z: number, qx: number, qy: number, qz: number, qw: number };
|
|
25
|
+
|
|
26
|
+
const unitScale = new Vector3(1, 1, 1);
|
|
27
|
+
const tempAnchorPose = new Matrix4();
|
|
28
|
+
const tempAnchorDelta = new Matrix4();
|
|
29
|
+
|
|
30
|
+
function matrixApproximatelyEquals(a: Matrix4, b: Matrix4, epsilon: number = 1e-6): boolean {
|
|
31
|
+
for (let i = 0; i < 16; i++) {
|
|
32
|
+
if (Math.abs(a.elements[i] - b.elements[i]) > epsilon) return false;
|
|
33
|
+
}
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
|
|
21
37
|
|
|
22
38
|
/**
|
|
23
39
|
* The WebARSessionRoot is the root object for a WebAR session and used to place the scene in AR.
|
|
@@ -101,11 +117,12 @@ export class WebARSessionRoot extends Behaviour {
|
|
|
101
117
|
/** When enabled the scene center will be automatically calculated from the content in the scene */
|
|
102
118
|
autoCenter: boolean = false;
|
|
103
119
|
|
|
104
|
-
/**
|
|
105
|
-
*
|
|
106
|
-
*
|
|
120
|
+
/** When enabled a XR anchor is created at the scene placement position and the scene
|
|
121
|
+
* stays at that anchored point while tracking refines during the XR session.
|
|
122
|
+
* On platforms without WebXR anchor support this gracefully falls back to plain placement.
|
|
123
|
+
* @default true
|
|
107
124
|
**/
|
|
108
|
-
useXRAnchor: boolean =
|
|
125
|
+
useXRAnchor: boolean = true;
|
|
109
126
|
|
|
110
127
|
/** true if we're currently placing the scene */
|
|
111
128
|
private _isPlacing = true;
|
|
@@ -125,13 +142,18 @@ export class WebARSessionRoot extends Behaviour {
|
|
|
125
142
|
|
|
126
143
|
/** the reticles used for placement */
|
|
127
144
|
private readonly _reticle: IGameObject[] = [];
|
|
128
|
-
/**
|
|
129
|
-
|
|
145
|
+
/** reference-space hit poses, in sync with the reticles — captured while placing when {@link useXRAnchor}
|
|
146
|
+
* is enabled, because hit test results can not be used outside the animation frame that produced them */
|
|
147
|
+
private readonly _hitRefPoses: (XRAnchorRefPose | undefined)[] = [];
|
|
130
148
|
|
|
131
149
|
private _placementStartTime: number = -1;
|
|
132
150
|
private _rigPlacementMatrix?: Matrix4;
|
|
133
151
|
/** if useAnchor is enabled this is the anchor we have created on placing the scene using the placement hit */
|
|
134
152
|
private _anchor: XRAnchor | null = null;
|
|
153
|
+
/** the placement pose (reference space) waiting for anchor creation on the next animation frame */
|
|
154
|
+
private _pendingAnchorPose: XRAnchorRefPose | null = null;
|
|
155
|
+
/** the last anchor pose (in rig space) that has been applied to the rig */
|
|
156
|
+
private readonly _anchorLastLocalPose = new Matrix4();
|
|
135
157
|
/** user input is used for ar touch transform */
|
|
136
158
|
private userInput?: WebXRSessionRootUserInput;
|
|
137
159
|
|
|
@@ -147,6 +169,8 @@ export class WebARSessionRoot extends Behaviour {
|
|
|
147
169
|
if (debug) console.log("ENTER WEBXR: SessionRoot start...");
|
|
148
170
|
|
|
149
171
|
this._anchor = null;
|
|
172
|
+
this._pendingAnchorPose = null;
|
|
173
|
+
this._hitRefPoses.length = 0;
|
|
150
174
|
WebARSessionRoot._hasPlaced = false;
|
|
151
175
|
|
|
152
176
|
// if (_args.xr.session.enabledFeatures?.includes("image-tracking")) {
|
|
@@ -205,8 +229,9 @@ export class WebARSessionRoot extends Behaviour {
|
|
|
205
229
|
// TODO: WebARSessionRoot doesnt work when we enter passthrough and leave XR without having placed the session!!!
|
|
206
230
|
this.context.input.removeEventListener("pointerup", this.onPlaceScene, { queue: InputEventQueue.Early });
|
|
207
231
|
this.onRevertSceneChanges();
|
|
208
|
-
//
|
|
232
|
+
// no explicit anchor.delete() — anchors are released by the platform when the session ends
|
|
209
233
|
this._anchor = null;
|
|
234
|
+
this._pendingAnchorPose = null;
|
|
210
235
|
WebARSessionRoot._hasPlaced = false;
|
|
211
236
|
this._rigPlacementMatrix = undefined;
|
|
212
237
|
}
|
|
@@ -252,20 +277,8 @@ export class WebARSessionRoot extends Behaviour {
|
|
|
252
277
|
|
|
253
278
|
}
|
|
254
279
|
else {
|
|
255
|
-
//
|
|
256
|
-
if (this.
|
|
257
|
-
const pose = args.xr.frame.getPose(this._anchor.anchorSpace, args.xr.referenceSpace);
|
|
258
|
-
if (pose && this.context.time.frame % 20 === 0) {
|
|
259
|
-
// apply the anchor pose to one of the reticles
|
|
260
|
-
const converted = args.xr.convertSpace(pose.transform);
|
|
261
|
-
const reticle = this._reticle[0];
|
|
262
|
-
if (reticle) {
|
|
263
|
-
reticle.position.copy(converted.position);
|
|
264
|
-
reticle.quaternion.copy(converted.quaternion);
|
|
265
|
-
this.onApplyPose(reticle);
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
}
|
|
280
|
+
// create the anchor (deferred from placement) and keep the scene glued to it
|
|
281
|
+
if (this.useXRAnchor) this.updateXRAnchor(args.xr);
|
|
269
282
|
|
|
270
283
|
// Scene has been placed
|
|
271
284
|
if (this.arTouchTransform) {
|
|
@@ -286,9 +299,20 @@ export class WebARSessionRoot extends Behaviour {
|
|
|
286
299
|
}
|
|
287
300
|
}
|
|
288
301
|
|
|
289
|
-
private updateReticleAndHits(
|
|
290
|
-
//
|
|
291
|
-
|
|
302
|
+
private updateReticleAndHits(xr: NeedleXRSession, i: number, hit: NeedleXRHitTestResult, scale: number) {
|
|
303
|
+
// capture the reference-space hit pose while the XRFrame that produced the hit is still
|
|
304
|
+
// active — it can not be queried anymore during placement (which runs from an input event)
|
|
305
|
+
if (this.useXRAnchor) {
|
|
306
|
+
const referenceSpace = xr.referenceSpace;
|
|
307
|
+
const rawPose = referenceSpace ? hit.hit.getPose(referenceSpace) : undefined;
|
|
308
|
+
if (rawPose) {
|
|
309
|
+
let pose = this._hitRefPoses[i];
|
|
310
|
+
if (!pose) pose = this._hitRefPoses[i] = { x: 0, y: 0, z: 0, qx: 0, qy: 0, qz: 0, qw: 1 };
|
|
311
|
+
const { position, orientation } = rawPose.transform;
|
|
312
|
+
pose.x = position.x; pose.y = position.y; pose.z = position.z;
|
|
313
|
+
pose.qx = orientation.x; pose.qy = orientation.y; pose.qz = orientation.z; pose.qw = orientation.w;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
292
316
|
let reticle = this._reticle[i];
|
|
293
317
|
if (!reticle) {
|
|
294
318
|
if (this.customReticle) {
|
|
@@ -392,14 +416,14 @@ export class WebARSessionRoot extends Behaviour {
|
|
|
392
416
|
return;
|
|
393
417
|
}
|
|
394
418
|
|
|
395
|
-
let
|
|
419
|
+
let hitIndex = 0;
|
|
396
420
|
|
|
397
421
|
if (evt && evt.origin instanceof NeedleXRController) {
|
|
398
422
|
// until we can use hit testing for both controllers and have multple reticles we only allow placement with the first controller
|
|
399
423
|
const controllerReticle = this._reticle[evt.origin.index];
|
|
400
424
|
if (controllerReticle) {
|
|
401
425
|
reticle = controllerReticle;
|
|
402
|
-
|
|
426
|
+
hitIndex = evt.origin.index;
|
|
403
427
|
}
|
|
404
428
|
}
|
|
405
429
|
|
|
@@ -423,7 +447,15 @@ export class WebARSessionRoot extends Behaviour {
|
|
|
423
447
|
WebARSessionRoot._hasPlaced = true;
|
|
424
448
|
|
|
425
449
|
if (this.useXRAnchor) {
|
|
426
|
-
|
|
450
|
+
// anchor creation is deferred to the next animation frame — see updateXRAnchor
|
|
451
|
+
const refPose = this._hitRefPoses[hitIndex];
|
|
452
|
+
if (refPose) {
|
|
453
|
+
this._pendingAnchorPose = { ...refPose };
|
|
454
|
+
}
|
|
455
|
+
else {
|
|
456
|
+
console.warn("[WebARSessionRoot] can not create a XR anchor: no hit pose was captured for the placement");
|
|
457
|
+
if (isDevEnvironment()) showBalloonWarning("Can not create XR anchor: no hit pose available");
|
|
458
|
+
}
|
|
427
459
|
}
|
|
428
460
|
|
|
429
461
|
if (this.context.xr) {
|
|
@@ -464,19 +496,73 @@ export class WebARSessionRoot extends Behaviour {
|
|
|
464
496
|
}
|
|
465
497
|
}
|
|
466
498
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
499
|
+
/**
|
|
500
|
+
* Deferred anchor creation and per-frame anchor tracking. Called from {@link onUpdateXR} after the scene has been placed.
|
|
501
|
+
*
|
|
502
|
+
* Creation can not happen during placement: `XRHitTestResult.createAnchor` only works while the animation
|
|
503
|
+
* frame that produced the hit is active, and placement runs from an input event where that frame is already
|
|
504
|
+
* over (`XRFrame.createAnchor` equally requires an active animation frame). So {@link onPlaceScene} only
|
|
505
|
+
* records the reference-space pose and the anchor is created here, on the next animation frame.
|
|
506
|
+
*
|
|
507
|
+
* Once the anchor exists, its pose *changes* (delta in rig space) are applied to the XR rig: the world pose
|
|
508
|
+
* of the anchored point stays constant while tracking refines, without discarding other rig modifications
|
|
509
|
+
* ({@link arTouchTransform} user adjustments, {@link arScale}).
|
|
510
|
+
*/
|
|
511
|
+
private updateXRAnchor(xr: NeedleXRSession) {
|
|
512
|
+
const referenceSpace = xr.referenceSpace;
|
|
513
|
+
if (!referenceSpace) return;
|
|
514
|
+
const frame = xr.frame;
|
|
515
|
+
|
|
516
|
+
if (this._pendingAnchorPose) {
|
|
517
|
+
const pending = this._pendingAnchorPose;
|
|
518
|
+
this._pendingAnchorPose = null;
|
|
519
|
+
if (typeof frame.createAnchor !== "function" || xr.session.enabledFeatures?.includes("anchors") === false) {
|
|
520
|
+
// dev-only: useXRAnchor is enabled by default, so platforms without anchor
|
|
521
|
+
// support (e.g. iOS) would otherwise warn in every production session
|
|
522
|
+
if (debug || isDevEnvironment()) {
|
|
523
|
+
console.warn("[WebARSessionRoot] useXRAnchor is enabled but WebXR anchors are not supported by this session");
|
|
524
|
+
showBalloonWarning("WebXR anchors are not supported by this session");
|
|
525
|
+
}
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
const transform = new XRRigidTransform({ x: pending.x, y: pending.y, z: pending.z }, { x: pending.qx, y: pending.qy, z: pending.qz, w: pending.qw });
|
|
529
|
+
// the anchor is created exactly at this pose — remember it (in rig space) as the baseline for deltas
|
|
530
|
+
const initialPose = xr.convertSpace(transform);
|
|
531
|
+
this._anchorLastLocalPose.compose(initialPose.position, initialPose.quaternion, unitScale);
|
|
532
|
+
frame.createAnchor(transform, referenceSpace).then(anchor => {
|
|
533
|
+
if (xr.running && !this._isPlacing) {
|
|
534
|
+
this._anchor = anchor;
|
|
535
|
+
if (debug) console.log("[WebARSessionRoot] created XR anchor", anchor);
|
|
536
|
+
}
|
|
537
|
+
else anchor.delete();
|
|
538
|
+
}).catch((err: unknown) => {
|
|
539
|
+
console.warn("[WebARSessionRoot] failed to create XR anchor", err);
|
|
540
|
+
if (isDevEnvironment()) showBalloonWarning("Failed to create XR anchor");
|
|
541
|
+
});
|
|
471
542
|
return;
|
|
472
543
|
}
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
544
|
+
|
|
545
|
+
if (!this._anchor) return;
|
|
546
|
+
const rig = xr.rig?.gameObject;
|
|
547
|
+
if (!rig) return;
|
|
548
|
+
const pose = frame.getPose(this._anchor.anchorSpace, referenceSpace);
|
|
549
|
+
// the anchor may temporarily not be tracked — keep the last applied correction then
|
|
550
|
+
if (!pose) return;
|
|
551
|
+
|
|
552
|
+
const current = xr.convertSpace(pose.transform);
|
|
553
|
+
tempAnchorPose.compose(current.position, current.quaternion, unitScale);
|
|
554
|
+
if (matrixApproximatelyEquals(tempAnchorPose, this._anchorLastLocalPose)) return;
|
|
555
|
+
|
|
556
|
+
// rig_new = rig_old · lastPose · currentPose⁻¹ — keeps the world pose of the anchored
|
|
557
|
+
// point (rigWorld · anchorRigSpacePose) constant across tracking refinements
|
|
558
|
+
tempAnchorDelta.copy(tempAnchorPose).invert().premultiply(this._anchorLastLocalPose);
|
|
559
|
+
this._anchorLastLocalPose.copy(tempAnchorPose);
|
|
560
|
+
rig.updateMatrix();
|
|
561
|
+
rig.matrix.multiply(tempAnchorDelta);
|
|
562
|
+
rig.matrix.decompose(rig.position, rig.quaternion, rig.scale);
|
|
563
|
+
if (debug) {
|
|
564
|
+
const dx = tempAnchorDelta.elements[12], dy = tempAnchorDelta.elements[13], dz = tempAnchorDelta.elements[14];
|
|
565
|
+
console.log(`[WebARSessionRoot] applied XR anchor correction: ${(Math.hypot(dx, dy, dz) * 1000).toFixed(2)}mm`);
|
|
480
566
|
}
|
|
481
567
|
}
|
|
482
568
|
|
|
@@ -152,12 +152,12 @@ export class WebXR extends Behaviour {
|
|
|
152
152
|
arScale: number = 1;
|
|
153
153
|
|
|
154
154
|
/**
|
|
155
|
-
* When enabled, an XRAnchor
|
|
156
|
-
*
|
|
157
|
-
* @
|
|
155
|
+
* When enabled, an XRAnchor is created at the AR placement position and the scene stays glued to it while tracking refines.
|
|
156
|
+
* On platforms without WebXR anchor support this gracefully falls back to plain placement.
|
|
157
|
+
* @default true
|
|
158
158
|
*/
|
|
159
159
|
@serializable()
|
|
160
|
-
useXRAnchor: boolean =
|
|
160
|
+
useXRAnchor: boolean = true;
|
|
161
161
|
|
|
162
162
|
/**
|
|
163
163
|
* When enabled, the scene will be automatically placed as soon as a suitable surface is detected in AR,
|