@needle-tools/engine 5.1.0 → 5.1.2

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.
Files changed (80) hide show
  1. package/CHANGELOG.md +7 -1
  2. package/dist/{needle-engine.bundle-5oIL49YP.js → needle-engine.bundle-BAUfRyBA.js} +7692 -7506
  3. package/dist/{needle-engine.bundle-DvOrfXws.umd.cjs → needle-engine.bundle-BL2wG5Te.umd.cjs} +159 -159
  4. package/dist/{needle-engine.bundle-DAAfj8Cs.min.js → needle-engine.bundle-BpNRkY3s.min.js} +184 -184
  5. package/dist/needle-engine.d.ts +25 -7
  6. package/dist/needle-engine.js +533 -533
  7. package/dist/needle-engine.min.js +1 -1
  8. package/dist/needle-engine.umd.cjs +1 -1
  9. package/lib/engine/api.d.ts +1 -1
  10. package/lib/engine/api.js +1 -1
  11. package/lib/engine/api.js.map +1 -1
  12. package/lib/engine/engine_init.js +2 -2
  13. package/lib/engine/engine_init.js.map +1 -1
  14. package/lib/engine/engine_license.d.ts +45 -7
  15. package/lib/engine/engine_license.js +276 -77
  16. package/lib/engine/engine_license.js.map +1 -1
  17. package/lib/engine/engine_networking.js +6 -0
  18. package/lib/engine/engine_networking.js.map +1 -1
  19. package/lib/engine/engine_networking_blob.js +3 -3
  20. package/lib/engine/engine_networking_blob.js.map +1 -1
  21. package/lib/engine/engine_networking_peer.js +19 -13
  22. package/lib/engine/engine_networking_peer.js.map +1 -1
  23. package/lib/engine/engine_utils_qrcode.js +2 -2
  24. package/lib/engine/engine_utils_qrcode.js.map +1 -1
  25. package/lib/engine/webcomponents/WebXRButtons.js +5 -6
  26. package/lib/engine/webcomponents/WebXRButtons.js.map +1 -1
  27. package/lib/engine/webcomponents/needle menu/needle-menu-spatial.js +2 -2
  28. package/lib/engine/webcomponents/needle menu/needle-menu-spatial.js.map +1 -1
  29. package/lib/engine/webcomponents/needle menu/needle-menu.js +5 -5
  30. package/lib/engine/webcomponents/needle menu/needle-menu.js.map +1 -1
  31. package/lib/engine/webcomponents/needle-engine.js +31 -2
  32. package/lib/engine/webcomponents/needle-engine.js.map +1 -1
  33. package/lib/engine/webcomponents/needle-engine.loading.js +2 -2
  34. package/lib/engine/webcomponents/needle-engine.loading.js.map +1 -1
  35. package/lib/engine/xr/NeedleXRSession.d.ts +7 -1
  36. package/lib/engine/xr/NeedleXRSession.js +70 -11
  37. package/lib/engine/xr/NeedleXRSession.js.map +1 -1
  38. package/lib/engine/xr/NeedleXRSync.js +23 -4
  39. package/lib/engine/xr/NeedleXRSync.js.map +1 -1
  40. package/lib/engine/xr/TempXRContext.js +2 -2
  41. package/lib/engine/xr/TempXRContext.js.map +1 -1
  42. package/lib/engine-components/SyncedTransform.d.ts +5 -1
  43. package/lib/engine-components/SyncedTransform.js +45 -8
  44. package/lib/engine-components/SyncedTransform.js.map +1 -1
  45. package/lib/engine-components/export/usdz/USDZExporter.js +4 -4
  46. package/lib/engine-components/webxr/Avatar.d.ts +8 -0
  47. package/lib/engine-components/webxr/Avatar.js +60 -32
  48. package/lib/engine-components/webxr/Avatar.js.map +1 -1
  49. package/lib/engine-components/webxr/WebXR.js +25 -14
  50. package/lib/engine-components/webxr/WebXR.js.map +1 -1
  51. package/lib/engine-schemes/synced-transform-model.d.ts +2 -0
  52. package/lib/engine-schemes/synced-transform-model.js +8 -1
  53. package/lib/engine-schemes/synced-transform-model.js.map +1 -1
  54. package/package.json +2 -2
  55. package/plugins/common/license.js +4 -4
  56. package/plugins/types/userconfig.d.ts +4 -3
  57. package/plugins/vite/build-pipeline.d.ts +3 -1
  58. package/plugins/vite/build-pipeline.js +19 -9
  59. package/plugins/vite/license.js +4 -4
  60. package/src/engine/api.ts +1 -1
  61. package/src/engine/engine_init.ts +2 -2
  62. package/src/engine/engine_license.ts +263 -73
  63. package/src/engine/engine_networking.ts +5 -0
  64. package/src/engine/engine_networking_blob.ts +3 -3
  65. package/src/engine/engine_networking_peer.ts +12 -13
  66. package/src/engine/engine_utils_qrcode.ts +2 -2
  67. package/src/engine/webcomponents/WebXRButtons.ts +5 -6
  68. package/src/engine/webcomponents/needle menu/needle-menu-spatial.ts +2 -2
  69. package/src/engine/webcomponents/needle menu/needle-menu.ts +5 -5
  70. package/src/engine/webcomponents/needle-engine.loading.ts +6 -6
  71. package/src/engine/webcomponents/needle-engine.ts +31 -2
  72. package/src/engine/xr/NeedleXRSession.ts +72 -14
  73. package/src/engine/xr/NeedleXRSync.ts +24 -4
  74. package/src/engine/xr/TempXRContext.ts +2 -2
  75. package/src/engine-components/SyncedTransform.ts +49 -10
  76. package/src/engine-components/export/usdz/USDZExporter.ts +4 -4
  77. package/src/engine-components/webxr/Avatar.ts +60 -33
  78. package/src/engine-components/webxr/WebXR.ts +23 -15
  79. package/src/engine-schemes/synced-transform-model.ts +10 -1
  80. package/src/engine-schemes/transforms.fbs +1 -0
@@ -42,6 +42,10 @@ export class Avatar extends Behaviour {
42
42
  private _leftHandMeshes?: Mesh[];
43
43
  private _rightHandMeshes?: Mesh[];
44
44
 
45
+ // meshes of remote avatar hands - hidden render-only so the hand root stays active and keeps syncing
46
+ private _remoteLeftHandMeshes?: Mesh[];
47
+ private _remoteRightHandMeshes?: Mesh[];
48
+
45
49
  private _syncTransforms?: SyncedTransform[];
46
50
 
47
51
  async onEnterXR(_args: NeedleXREventArgs) {
@@ -120,46 +124,54 @@ export class Avatar extends Behaviour {
120
124
  }
121
125
 
122
126
  // synchronize hands
127
+ // IMPORTANT: never toggle `.visible` on the hand object itself - it carries the SyncedTransform,
128
+ // and setting it invisible deactivates the component (onDisable -> freeOwnership), which means the
129
+ // owner loses ownership and stops sending (the remote hand then stays frozen). The head syncs fine
130
+ // precisely because its synced root is never deactivated (only a child mesh is hidden). So we keep
131
+ // the hand object active and hide only the rendered meshes (render-only, via setCustomVisibility).
123
132
  const leftCtrl = args.xr.leftController;
124
133
  const leftObj = this.leftHand?.asset as Object3D;
125
- if (leftCtrl && leftObj) {
126
- leftObj.position.copy(leftCtrl.gripPosition);
127
- leftObj.quaternion.copy(leftCtrl.gripQuaternion);
128
- leftObj.quaternion.multiply(flipForwardQuaternion);
129
- leftObj.visible = leftCtrl.isTracking;
134
+ if (leftObj) {
135
+ if (leftCtrl) {
136
+ leftObj.position.copy(leftCtrl.gripPosition);
137
+ leftObj.quaternion.copy(leftCtrl.gripQuaternion);
138
+ leftObj.quaternion.multiply(flipForwardQuaternion);
139
+ }
140
+ leftObj.visible = true;
130
141
  this.updateHandVisibility(leftCtrl, leftObj, this._leftHandMeshes);
131
142
  }
132
- else if (leftObj && leftObj.visible) {
133
- leftObj.visible = false;
134
- }
135
143
 
136
144
  const right = args.xr.rightController;
137
145
  const rightObj = this.rightHand?.asset as Object3D;
138
- if (right && rightObj) {
139
- rightObj.position.copy(right.gripPosition);
140
- rightObj.quaternion.copy(right.gripQuaternion);
141
- rightObj.quaternion.multiply(flipForwardQuaternion);
142
- rightObj.visible = right.isTracking;
146
+ if (rightObj) {
147
+ if (right) {
148
+ rightObj.position.copy(right.gripPosition);
149
+ rightObj.quaternion.copy(right.gripQuaternion);
150
+ rightObj.quaternion.multiply(flipForwardQuaternion);
151
+ }
152
+ rightObj.visible = true;
143
153
  this.updateHandVisibility(right, rightObj, this._rightHandMeshes);
144
154
  }
145
- else if (rightObj && rightObj.visible) {
146
- rightObj.visible = false;
147
- }
148
155
  }
149
156
 
150
157
  onBeforeRender(): void {
151
- if (this.context.xr) {
152
- if (this.context.time.frame % 10 === 0)
153
- this.updateRemoteAvatarVisibility();
154
- }
158
+ // Remote avatar visibility depends on the *owner's* tracking state, not on whether THIS client is
159
+ // in XR: other users' avatars are syncInstantiated onto non-XR clients too (spectators / lobby),
160
+ // and must hide/show their hands correctly there as well. So this runs regardless of the local XR
161
+ // state. updateRemoteAvatarVisibility() early-outs for the local player and when not connected.
162
+ if (this.context.time.frame % 10 === 0)
163
+ this.updateRemoteAvatarVisibility();
155
164
  }
156
165
 
157
- private updateHandVisibility(controller: NeedleXRController, avatarHand: Object3D, meshes: Mesh[] | undefined) {
166
+ private updateHandVisibility(controller: NeedleXRController | null | undefined, avatarHand: Object3D, meshes: Mesh[] | undefined) {
158
167
  if (meshes) {
159
- // Hide the hand meshes for the local user if another model (e.g. the controller model) is being rendered
160
- // We don't set the visible flag here because it would also disable SyncedTransforms networking
161
- const hasOtherRenderingModel = controller.model && controller.model.visible && controller.model !== avatarHand;
162
- meshes.forEach(mesh => { setCustomVisibility(mesh, !hasOtherRenderingModel); });
168
+ // Hide the hand meshes (render-only) when the hand is not tracked, or when another model
169
+ // (e.g. the controller/hand model) is being rendered in its place. We hide the meshes rather
170
+ // than the object's `visible` flag because that would disable SyncedTransform networking.
171
+ const isTracking = controller?.isTracking === true;
172
+ const hasOtherRenderingModel = !!controller?.model && controller.model.visible && controller.model !== avatarHand;
173
+ const render = isTracking && !hasOtherRenderingModel;
174
+ meshes.forEach(mesh => { setCustomVisibility(mesh, render); });
163
175
  }
164
176
  }
165
177
 
@@ -173,14 +185,13 @@ export class Avatar extends Behaviour {
173
185
  if (sync.hasState(state.owner)) {
174
186
  this.tryFindAvatarObjectsIfMissing();
175
187
 
176
- const leftObj = this.leftHand?.asset as Object3D;
177
- if (leftObj) {
178
- leftObj.visible = sync?.isTracking(state.owner, "left") ?? false;
179
- }
180
- const rightObj = this.rightHand?.asset as Object3D;
181
- if (rightObj) {
182
- rightObj.visible = sync?.isTracking(state.owner, "right") ?? false;
183
- }
188
+ // Keep the hand object active (don't toggle `.visible`) so its SyncedTransform keeps
189
+ // applying updates and a non-owner freeOwnership can't strip the owner's ownership.
190
+ // Hide/show the rendered meshes based on the owner's tracking state instead.
191
+ const leftRendered = sync.isTracking(state.owner, "left") ?? false;
192
+ this._remoteLeftHandMeshes = this.setRemoteHandRendered(this.leftHand?.asset as Object3D, this._remoteLeftHandMeshes, leftRendered);
193
+ const rightRendered = sync.isTracking(state.owner, "right") ?? false;
194
+ this._remoteRightHandMeshes = this.setRemoteHandRendered(this.rightHand?.asset as Object3D, this._remoteRightHandMeshes, rightRendered);
184
195
  }
185
196
  }
186
197
 
@@ -198,6 +209,22 @@ export class Avatar extends Behaviour {
198
209
 
199
210
 
200
211
 
212
+ /**
213
+ * Show/hide a remote avatar hand render-only. The hand object stays active so its SyncedTransform keeps
214
+ * applying networked updates (toggling `.visible` would deactivate it and could free the owner's ownership).
215
+ * The mesh list is gathered lazily and cached, and returned so the caller can keep it.
216
+ */
217
+ private setRemoteHandRendered(obj: Object3D | undefined, meshes: Mesh[] | undefined, rendered: boolean): Mesh[] | undefined {
218
+ if (!obj) return meshes;
219
+ obj.visible = true;
220
+ if (!meshes || meshes.length === 0) {
221
+ meshes = [];
222
+ obj.traverse(o => { if ((o as Mesh).isMesh) meshes!.push(o as Mesh); });
223
+ }
224
+ for (const mesh of meshes) setCustomVisibility(mesh, rendered);
225
+ return meshes;
226
+ }
227
+
201
228
  private tryFindAvatarObjectsIfMissing() {
202
229
  // if no avatar objects are set, try to find them
203
230
  if (!this.head || !this.leftHand || !this.rightHand) {
@@ -237,22 +237,30 @@ export class WebXR extends Behaviour {
237
237
  // Showing the QuickLook button depends on whether we're on iOS or visionOS –
238
238
  // on iOS we have AppClip support, so we don't need QuickLook button there,
239
239
  // while on visionOS we use QuickLook for AR experiences for now (unless it happens to have WebXR support by then).
240
- navigator.xr?.isSessionSupported("immersive-ar").catch(() => false).then((arSupported) => {
241
-
242
- const isVisionOSFallback = DeviceUtilities.isVisionOS() && !arSupported;
243
-
244
- if (this.useQuicklookExport || isVisionOSFallback) {
245
- const existingUSDZExporter = GameObject.findObjectOfType(USDZExporter);
246
- if (!existingUSDZExporter) {
247
- // if no USDZ Exporter is found we add one and assign the scene to be exported
248
- if (debug) console.log("WebXR: Adding USDZExporter");
249
- this._usdzExporter = GameObject.addComponent(this.gameObject, USDZExporter);
250
- this._usdzExporter.objectToExport = this.context.scene;
251
- this._usdzExporter.autoExportAnimations = true;
252
- this._usdzExporter.autoExportAudioSources = true;
253
- }
240
+ const addUsdzExporterIfMissing = () => {
241
+ const existingUSDZExporter = GameObject.findObjectOfType(USDZExporter);
242
+ if (!existingUSDZExporter) {
243
+ // if no USDZ Exporter is found we add one and assign the scene to be exported
244
+ if (debug) console.log("WebXR: Adding USDZExporter");
245
+ this._usdzExporter = GameObject.addComponent(this.gameObject, USDZExporter);
246
+ this._usdzExporter.objectToExport = this.context.scene;
247
+ this._usdzExporter.autoExportAnimations = true;
248
+ this._usdzExporter.autoExportAudioSources = true;
254
249
  }
255
- });
250
+ };
251
+ // QuickLook is added when explicitly requested, or as the visionOS fallback when
252
+ // WebXR AR isn't available. Only probe XR support on visionOS (the only case that
253
+ // consumes the result) and go through the cached NeedleXRSession.isARSupported() so
254
+ // we don't eagerly hit navigator.xr on every enable — that probe trips the
255
+ // `xr-spatial-tracking` Permissions-Policy in embeds that weren't granted it.
256
+ if (this.useQuicklookExport) {
257
+ addUsdzExporterIfMissing();
258
+ }
259
+ else if (DeviceUtilities.isVisionOS()) {
260
+ NeedleXRSession.isARSupported().then((arSupported) => {
261
+ if (!arSupported) addUsdzExporterIfMissing();
262
+ });
263
+ }
256
264
 
257
265
  this.handleCreatingHTML();
258
266
  this.handleOfferSession();
@@ -54,8 +54,13 @@ dontSave():boolean {
54
54
  return offset ? !!this.bb!.readInt8(this.bb_pos + offset) : false;
55
55
  }
56
56
 
57
+ version():number {
58
+ const offset = this.bb!.__offset(this.bb_pos, 12);
59
+ return offset ? this.bb!.readInt32(this.bb_pos + offset) : 0;
60
+ }
61
+
57
62
  static startSyncedTransformModel(builder:flatbuffers.Builder) {
58
- builder.startObject(4);
63
+ builder.startObject(5);
59
64
  }
60
65
 
61
66
  static addGuid(builder:flatbuffers.Builder, guidOffset:flatbuffers.Offset) {
@@ -74,6 +79,10 @@ static addDontSave(builder:flatbuffers.Builder, dontSave:boolean) {
74
79
  builder.addFieldInt8(3, +dontSave, +false);
75
80
  }
76
81
 
82
+ static addVersion(builder:flatbuffers.Builder, version:number) {
83
+ builder.addFieldInt32(4, version, 0);
84
+ }
85
+
77
86
  static endSyncedTransformModel(builder:flatbuffers.Builder):flatbuffers.Offset {
78
87
  const offset = builder.endObject();
79
88
  return offset;
@@ -20,6 +20,7 @@ table SyncedTransformModel {
20
20
  transform: Transform;
21
21
  /// if the server should not save this info
22
22
  dont_save:bool;
23
+ version : int;
23
24
  }
24
25
 
25
26
  // file_identifier "STRS";