@needle-tools/engine 5.1.0-beta.1 → 5.1.0-beta.3

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/components.needle.json +1 -1
  2. package/dist/{materialx-B85WjP7E.min.js → materialx-B5gKeM65.min.js} +1 -1
  3. package/dist/{materialx-B47Bz-xs.umd.cjs → materialx-CFRcCYWy.umd.cjs} +1 -1
  4. package/dist/{materialx-jkHmVPez.js → materialx-DqGmteNO.js} +1 -1
  5. package/dist/{needle-engine.bundle-CWiO9cA8.umd.cjs → needle-engine.bundle--5M1FCR0.umd.cjs} +154 -142
  6. package/dist/{needle-engine.bundle-CFo3xWPu.js → needle-engine.bundle-B6JqW8fC.js} +6020 -5970
  7. package/dist/{needle-engine.bundle-DxJaiP7e.min.js → needle-engine.bundle-D5eqAuZu.min.js} +158 -146
  8. package/dist/needle-engine.d.ts +22 -11
  9. package/dist/needle-engine.js +211 -211
  10. package/dist/needle-engine.min.js +1 -1
  11. package/dist/needle-engine.umd.cjs +1 -1
  12. package/dist/{vendor-BQ2Vuntm.min.js → vendor-2--bD1c9.min.js} +19 -19
  13. package/dist/{vendor-BslSKZPo.js → vendor-BNtcIZkv.js} +1141 -1138
  14. package/dist/{vendor-1UvpPPSB.umd.cjs → vendor-vbe9sv1t.umd.cjs} +20 -20
  15. package/lib/engine/api.d.ts +1 -1
  16. package/lib/engine/api.js +1 -1
  17. package/lib/engine/api.js.map +1 -1
  18. package/lib/engine/engine_init.js +2 -2
  19. package/lib/engine/engine_license.d.ts +7 -7
  20. package/lib/engine/engine_license.js +72 -72
  21. package/lib/engine/engine_license.js.map +1 -1
  22. package/lib/engine/engine_networking_blob.js +3 -3
  23. package/lib/engine/engine_networking_blob.js.map +1 -1
  24. package/lib/engine/engine_utils_qrcode.js +2 -2
  25. package/lib/engine/engine_utils_qrcode.js.map +1 -1
  26. package/lib/engine/webcomponents/needle menu/needle-menu-spatial.js +2 -2
  27. package/lib/engine/webcomponents/needle menu/needle-menu-spatial.js.map +1 -1
  28. package/lib/engine/webcomponents/needle menu/needle-menu.d.ts +2 -2
  29. package/lib/engine/webcomponents/needle menu/needle-menu.js +9 -9
  30. package/lib/engine/webcomponents/needle menu/needle-menu.js.map +1 -1
  31. package/lib/engine/webcomponents/needle-engine.js +2 -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/TempXRContext.js +2 -2
  36. package/lib/engine/xr/TempXRContext.js.map +1 -1
  37. package/lib/engine-components/Component.js +12 -0
  38. package/lib/engine-components/Component.js.map +1 -1
  39. package/lib/engine-components/ContactShadows.d.ts +7 -2
  40. package/lib/engine-components/ContactShadows.js +65 -45
  41. package/lib/engine-components/ContactShadows.js.map +1 -1
  42. package/lib/engine-components/GroundProjection.d.ts +6 -0
  43. package/lib/engine-components/GroundProjection.js +69 -7
  44. package/lib/engine-components/GroundProjection.js.map +1 -1
  45. package/lib/engine-components/NeedleMenu.d.ts +1 -1
  46. package/lib/engine-components/NeedleMenu.js +5 -2
  47. package/lib/engine-components/NeedleMenu.js.map +1 -1
  48. package/lib/engine-components/Skybox.js +9 -0
  49. package/lib/engine-components/Skybox.js.map +1 -1
  50. package/lib/engine-components/export/usdz/USDZExporter.js +4 -4
  51. package/lib/engine-components/export/usdz/USDZExporter.js.map +1 -1
  52. package/lib/engine-components/export/usdz/extensions/behavior/BehaviourComponents.js +9 -5
  53. package/lib/engine-components/export/usdz/extensions/behavior/BehaviourComponents.js.map +1 -1
  54. package/lib/engine-components/particlesystem/ParticleSystem.js +4 -0
  55. package/lib/engine-components/particlesystem/ParticleSystem.js.map +1 -1
  56. package/lib/engine-components/webxr/WebXRImageTracking.js +3 -0
  57. package/lib/engine-components/webxr/WebXRImageTracking.js.map +1 -1
  58. package/package.json +2 -2
  59. package/plugins/common/license.js +4 -4
  60. package/plugins/vite/build-pipeline.js +33 -2
  61. package/plugins/vite/license.js +4 -4
  62. package/src/engine/api.ts +1 -1
  63. package/src/engine/engine_init.ts +2 -2
  64. package/src/engine/engine_license.ts +69 -69
  65. package/src/engine/engine_networking_blob.ts +3 -3
  66. package/src/engine/engine_utils_qrcode.ts +2 -2
  67. package/src/engine/webcomponents/needle menu/needle-menu-spatial.ts +2 -2
  68. package/src/engine/webcomponents/needle menu/needle-menu.ts +10 -10
  69. package/src/engine/webcomponents/needle-engine.loading.ts +6 -6
  70. package/src/engine/webcomponents/needle-engine.ts +2 -2
  71. package/src/engine/xr/TempXRContext.ts +2 -2
  72. package/src/engine-components/Component.ts +11 -0
  73. package/src/engine-components/ContactShadows.ts +75 -49
  74. package/src/engine-components/GroundProjection.ts +71 -7
  75. package/src/engine-components/NeedleMenu.ts +6 -3
  76. package/src/engine-components/Skybox.ts +8 -0
  77. package/src/engine-components/export/usdz/USDZExporter.ts +4 -4
  78. package/src/engine-components/export/usdz/extensions/behavior/BehaviourComponents.ts +12 -5
  79. package/src/engine-components/particlesystem/ParticleSystem.ts +4 -0
  80. package/src/engine-components/webxr/WebXRImageTracking.ts +1 -0
@@ -83,7 +83,7 @@ type FitParameters = {
83
83
  */
84
84
  export class ContactShadows extends Behaviour {
85
85
 
86
- private static readonly _instances: Map<Context, ContactShadows> = new Map();
86
+ private static readonly _instances: WeakMap<Context, ContactShadows> = new WeakMap();
87
87
  /**
88
88
  * Create contact shadows for the scene. Automatically fits the shadows to the scene.
89
89
  * The instance of contact shadows will be created only once.
@@ -179,8 +179,9 @@ export class ContactShadows extends Behaviour {
179
179
  * in which case ContactShadows scale would affect the projection
180
180
  **/
181
181
  private readonly shadowsRoot: IGameObject = new Object3D() as IGameObject;
182
- private shadowCamera?: OrthographicCamera;
183
182
  private readonly shadowGroup: Group = new Group();
183
+
184
+ private shadowCamera?: OrthographicCamera;
184
185
 
185
186
  private renderTarget?: WebGLRenderTarget;
186
187
  private renderTargetBlur?: WebGLRenderTarget;
@@ -240,19 +241,86 @@ export class ContactShadows extends Behaviour {
240
241
  if (debug) console.log("Fitted shadows to scene", this.shadowsRoot.scale.clone());
241
242
  }
242
243
 
244
+ /**
245
+ * Call to update the shadows immediately. Only needed if `manualUpdate` is enabled, usually the shadows will update automatically when the scene changes or when the component is enabled.
246
+ */
247
+ renderShadowsNow() {
248
+ this._renderShadows();
249
+ }
243
250
 
244
251
  /** @internal */
245
252
  awake() {
246
253
  ContactShadows._instances.set(this.context, this);
247
- this.shadowsRoot.hideFlags = HideFlags.DontExport;
254
+ }
248
255
 
256
+ /** @internal */
257
+ onEnable(): void {
258
+ this.shadowsRoot.hideFlags = HideFlags.DontExport;
249
259
  // ignore self for autofitting
250
260
  setAutoFitEnabled(this.shadowsRoot, false);
261
+
262
+ this._needsUpdate = true;
263
+ this.autoCleanup(this.context.events.on("scene-content-changed", () => {
264
+ if (!this.autoFit) return;
265
+ this._needsFit = true;
266
+ this._needsUpdate = true;
267
+ }));
268
+
269
+ if (this.autoFit) this.fitShadows();
270
+ else this.applyMinSize();
251
271
  }
252
272
 
273
+ /** @internal */
274
+ onDestroy(): void {
275
+ if (debug) console.warn("Destroying ContactShadows on " + this.gameObject.name);
276
+
277
+ const instance = ContactShadows._instances.get(this.context);
278
+ if (instance === this) {
279
+ ContactShadows._instances.delete(this.context);
280
+ }
281
+
282
+ // dispose the render targets
283
+ this.renderTarget?.dispose();
284
+ this.renderTargetBlur?.dispose();
285
+
286
+ // dispose the materials
287
+ this.depthMaterial?.dispose();
288
+ this.horizontalBlurMaterial?.dispose();
289
+ this.verticalBlurMaterial?.dispose();
290
+
291
+ // dispose the geometries
292
+ this.blurPlane?.geometry.dispose();
293
+ this.plane?.geometry.dispose();
294
+ this.occluderMesh?.geometry.dispose();
295
+ }
253
296
 
254
297
  /** @internal */
255
- start(): void {
298
+ onBeforeRender(_frame: XRFrame | null): void {
299
+
300
+ if (this._needsFit && this.autoFit) {
301
+ this._needsFit = false;
302
+ this.fitShadows();
303
+ }
304
+
305
+ if (this.manualUpdate) {
306
+ if (!this._needsUpdate) return;
307
+ }
308
+ this._needsUpdate = false;
309
+ this._renderShadows();
310
+ }
311
+
312
+ private _ensureSetup() {
313
+
314
+ // Make sure the shadows root is parented to the game object. This is needed for the shadows to be in the correct position and scale relative to the game object. We need to do this before checking if the materials are set up because some properties (e.g. darkness) can be changed in the editor before the component is enabled, so we want to avoid re-creating everything in that case.
315
+ if (this.shadowsRoot.parent !== this.gameObject) {
316
+ this.gameObject.add(this.shadowsRoot);
317
+ }
318
+
319
+ // Check if everything is already set up. We need to do this because some properties (e.g. darkness) can be changed in the editor before the component is enabled, so we want to avoid re-creating everything in that case.
320
+ if (this.depthMaterial && this.horizontalBlurMaterial && this.verticalBlurMaterial && this.planeMaterial) {
321
+ return;
322
+ }
323
+
256
324
  if (debug) console.log("Create ContactShadows on " + this.gameObject.name, this)
257
325
 
258
326
  this.gameObject.add(this.shadowsRoot);
@@ -363,54 +431,11 @@ export class ContactShadows extends Behaviour {
363
431
  this.verticalBlurMaterial.depthTest = false;
364
432
 
365
433
  this.shadowGroup.visible = false;
366
-
367
- if (this.autoFit) this.fitShadows();
368
- else this.applyMinSize();
369
434
  }
370
435
 
371
- onEnable(): void {
372
- this._needsUpdate = true;
373
- this.autoCleanup(this.context.events.on("scene-content-changed", () => {
374
- if (!this.autoFit) return;
375
- this._needsFit = true;
376
- this._needsUpdate = true;
377
- }));
378
- }
436
+ private _renderShadows() {
379
437
 
380
- /** @internal */
381
- onDestroy(): void {
382
- const instance = ContactShadows._instances.get(this.context);
383
- if (instance === this) {
384
- ContactShadows._instances.delete(this.context);
385
- }
386
-
387
- // dispose the render targets
388
- this.renderTarget?.dispose();
389
- this.renderTargetBlur?.dispose();
390
-
391
- // dispose the materials
392
- this.depthMaterial?.dispose();
393
- this.horizontalBlurMaterial?.dispose();
394
- this.verticalBlurMaterial?.dispose();
395
-
396
- // dispose the geometries
397
- this.blurPlane?.geometry.dispose();
398
- this.plane?.geometry.dispose();
399
- this.occluderMesh?.geometry.dispose();
400
- }
401
-
402
- /** @internal */
403
- onBeforeRender(_frame: XRFrame | null): void {
404
-
405
- if (this._needsFit && this.autoFit) {
406
- this._needsFit = false;
407
- this.fitShadows();
408
- }
409
-
410
- if (this.manualUpdate) {
411
- if (!this._needsUpdate) return;
412
- }
413
- this._needsUpdate = false;
438
+ this._ensureSetup();
414
439
 
415
440
  if (!this.renderTarget || !this.renderTargetBlur ||
416
441
  !this.depthMaterial || !this.shadowCamera ||
@@ -536,6 +561,7 @@ export class ContactShadows extends Behaviour {
536
561
  scene.background = initialBackground;
537
562
  renderer.xr.enabled = prevXRState;
538
563
  this.context.scene.matrixWorldAutoUpdate = prevSceneMatrixAutoUpdate;
564
+
539
565
  }
540
566
 
541
567
  // renderTarget --> blurPlane (horizontalBlur) --> renderTargetBlur --> blurPlane (verticalBlur) --> renderTarget
@@ -1,4 +1,4 @@
1
- import { CubeUVReflectionMapping, ShaderMaterial, Texture } from "three";
1
+ import { CubeUVReflectionMapping, Euler, Matrix3, Matrix4, ShaderMaterial, Texture } from "three";
2
2
  import { GroundedSkybox as GroundProjection } from 'three/examples/jsm/objects/GroundedSkybox.js';
3
3
 
4
4
  import { Gizmos } from "../engine/engine_gizmos.js";
@@ -21,8 +21,13 @@ type GroundProjectionShaderUniforms = {
21
21
  needleGroundProjectionBlending: { value: number };
22
22
  needleGroundProjectionAlphaFactor: { value: number };
23
23
  needleGroundProjectionBackgroundIntensity: { value: number };
24
+ needleGroundProjectionBackgroundRotation: { value: Matrix3 };
24
25
  };
25
26
 
27
+ // Reused per-frame to build the background-rotation matrix without allocating in onBeforeRender.
28
+ const _backgroundRotationEuler = new Euler();
29
+ const _backgroundRotationMatrix = new Matrix4();
30
+
26
31
  const needleCubeUvMapVarying = /* glsl */`
27
32
  #ifdef NEEDLE_USE_CUBE_UV_MAP
28
33
  varying vec3 vNeedleGroundProjectionWorldDirection;
@@ -79,7 +84,10 @@ const needleCubeUvMapFragment = /* glsl */`
79
84
 
80
85
  #endif
81
86
 
82
- sampledDiffuseColor.rgb *= mix(1.0, needleGroundProjectionBackgroundIntensity, needleGroundProjectionBlurFactorValue);
87
+ // Apply backgroundIntensity uniformly across the whole projection (center included), matching how the
88
+ // skybox applies it (backgroundCube.glsl: texColor.rgb *= backgroundIntensity). Only the blurriness
89
+ // ramps toward the horizon - the intensity must not.
90
+ sampledDiffuseColor.rgb *= needleGroundProjectionBackgroundIntensity;
83
91
  diffuseColor *= sampledDiffuseColor;
84
92
 
85
93
  #endif
@@ -122,7 +130,8 @@ function getGroundProjectionShaderUniforms(material: GroundProjectionMaterial):
122
130
  needleGroundProjectionBlurriness: { value: 0 },
123
131
  needleGroundProjectionBlending: { value: 0 },
124
132
  needleGroundProjectionAlphaFactor: { value: 1 },
125
- needleGroundProjectionBackgroundIntensity: { value: 1 }
133
+ needleGroundProjectionBackgroundIntensity: { value: 1 },
134
+ needleGroundProjectionBackgroundRotation: { value: new Matrix3() }
126
135
  };
127
136
  }
128
137
 
@@ -133,15 +142,25 @@ function configureGroundProjectionMaterial(material: GroundProjectionMaterial, t
133
142
  shader.uniforms.needleGroundProjectionBlending = projectionUniforms.needleGroundProjectionBlending;
134
143
  shader.uniforms.needleGroundProjectionAlphaFactor = projectionUniforms.needleGroundProjectionAlphaFactor;
135
144
  shader.uniforms.needleGroundProjectionBackgroundIntensity = projectionUniforms.needleGroundProjectionBackgroundIntensity;
145
+ shader.uniforms.needleGroundProjectionBackgroundRotation = projectionUniforms.needleGroundProjectionBackgroundRotation;
136
146
 
137
147
  shader.vertexShader = shader.vertexShader
138
- .replace("#include <uv_pars_vertex>", `#include <uv_pars_vertex>\n${needleCubeUvMapVarying}`)
148
+ .replace("#include <uv_pars_vertex>", `#include <uv_pars_vertex>\n${needleCubeUvMapVarying}
149
+ #ifdef NEEDLE_USE_CUBE_UV_MAP
150
+ uniform mat3 needleGroundProjectionBackgroundRotation;
151
+ #endif`)
139
152
  .replace(
140
153
  "#include <worldpos_vertex>",
141
154
  `#include <worldpos_vertex>
142
155
  #ifdef NEEDLE_USE_CUBE_UV_MAP
143
- // GroundedSkybox mirrors geometry on Z, so undo that before deriving the sampling direction.
144
- vNeedleGroundProjectionWorldDirection = transformDirection( vec3( position.x, position.y, -position.z ), modelMatrix );
156
+ // Sample the environment in the fragment's actual world direction. GroundedSkybox mirrors the geometry
157
+ // on Z (geometry.scale(1,1,-1)) and that mirrored vertex IS the on-screen direction; the skybox and
158
+ // environment lighting are likewise sampled in true world directions. So we must NOT undo the mirror
159
+ // here - doing so reflects the sampling azimuth and flips the panorama left-right vs. the skybox.
160
+ // backgroundRotation is applied to the sampling direction (exactly like the skybox does in
161
+ // backgroundCube.glsl), NOT by rotating the mesh - that keeps the floor flat and matches the skybox at
162
+ // the horizon for any background rotation.
163
+ vNeedleGroundProjectionWorldDirection = needleGroundProjectionBackgroundRotation * transformDirection( position, modelMatrix );
145
164
  #endif`
146
165
  );
147
166
 
@@ -271,6 +290,25 @@ export class GroundProjectedEnv extends Behaviour {
271
290
  private _projection?: GroundProjection;
272
291
  private _watcher?: Watch;
273
292
 
293
+ /** The scene's real backgroundBlurriness, stored while we temporarily override it for the render. */
294
+ private _savedBackgroundBlur: number | null = null;
295
+
296
+ /** pre_render_callback: force the skybox blur just before the render (after all component onBeforeRender). */
297
+ private readonly _forceBackgroundBlur = (_frame?: XRFrame | null) => {
298
+ if (this._savedBackgroundBlur !== null) return; // already overriding (safety)
299
+ if (!(this.scene.background instanceof Texture)) return; // no skybox to blur
300
+ const current = this.scene.backgroundBlurriness ?? 0;
301
+ this._savedBackgroundBlur = current;
302
+ this.scene.backgroundBlurriness = 1;
303
+ };
304
+
305
+ /** post_render_callback: restore the scene's real blur right after the render. */
306
+ private readonly _restoreBackgroundBlur = () => {
307
+ if (this._savedBackgroundBlur === null) return;
308
+ this.scene.backgroundBlurriness = this._savedBackgroundBlur;
309
+ this._savedBackgroundBlur = null;
310
+ };
311
+
274
312
 
275
313
  /** @internal */
276
314
  awake() {
@@ -291,11 +329,23 @@ export class GroundProjectedEnv extends Behaviour {
291
329
  this._needsTextureUpdate = true;
292
330
  });
293
331
  }
332
+ // Force the skybox blur via the render-bracketing callbacks (these run after every component's
333
+ // onBeforeRender, so the override can't be clobbered by e.g. Camera.applyClearFlags).
334
+ if (this.context.pre_render_callbacks.indexOf(this._forceBackgroundBlur) < 0)
335
+ this.context.pre_render_callbacks.push(this._forceBackgroundBlur);
336
+ if (this.context.post_render_callbacks.indexOf(this._restoreBackgroundBlur) < 0)
337
+ this.context.post_render_callbacks.push(this._restoreBackgroundBlur);
294
338
  }
295
339
  /** @internal */
296
340
  onDisable() {
297
341
  this._watcher?.revoke();
298
342
  this._projection?.removeFromParent();
343
+ const pre = this.context.pre_render_callbacks.indexOf(this._forceBackgroundBlur);
344
+ if (pre >= 0) this.context.pre_render_callbacks.splice(pre, 1);
345
+ const post = this.context.post_render_callbacks.indexOf(this._restoreBackgroundBlur);
346
+ if (post >= 0) this.context.post_render_callbacks.splice(post, 1);
347
+ // If we were disabled mid-frame (after force, before restore), put the real value back.
348
+ this._restoreBackgroundBlur();
299
349
  }
300
350
  /** @internal */
301
351
  onEnterXR(): void {
@@ -312,7 +362,21 @@ export class GroundProjectedEnv extends Behaviour {
312
362
  /** @internal */
313
363
  onBeforeRender(): void {
314
364
  if (this._projection && this.scene.backgroundRotation) {
315
- this._projection.rotation.copy(this.scene.backgroundRotation);
365
+ // Mirror three's WebGLBackground: build the same mat3 it applies to the skybox sampling
366
+ // direction and feed it to our shader. This rotates the sampled environment exactly like the
367
+ // skybox (backgroundCube.glsl) instead of rotating the mesh - keeping the floor flat and the
368
+ // horizon aligned with the skybox for any background rotation.
369
+ _backgroundRotationEuler.copy(this.scene.backgroundRotation);
370
+ // accommodate left-handed frame (same as WebGLBackground.js)
371
+ _backgroundRotationEuler.x *= -1;
372
+ _backgroundRotationEuler.y *= -1;
373
+ _backgroundRotationEuler.z *= -1;
374
+ // Note: the extra cube-texture flip in WebGLBackground only applies to raw CubeTextures, not the
375
+ // PMREM render-target env this shader path samples - so it is intentionally omitted here.
376
+ const uniforms = getGroundProjectionShaderUniforms(this._projection.material);
377
+ uniforms.needleGroundProjectionBackgroundRotation.value.setFromMatrix4(
378
+ _backgroundRotationMatrix.makeRotationFromEuler(_backgroundRotationEuler)
379
+ );
316
380
  }
317
381
 
318
382
  if (this._projection && this.context.scene.background instanceof Texture) {
@@ -49,8 +49,8 @@ export class NeedleMenu extends Behaviour {
49
49
  @serializable()
50
50
  position: "top" | "bottom" = "bottom";
51
51
 
52
- /**
53
- * Controls the visibility of the Needle logo in the menu (requires PRO license)
52
+ /**
53
+ * Controls the visibility of the Needle logo in the menu (requires a commercial license)
54
54
  */
55
55
  @serializable()
56
56
  showNeedleLogo: boolean = false;
@@ -102,7 +102,10 @@ export class NeedleMenu extends Behaviour {
102
102
  this.context.menu.showFullscreenOption(true);
103
103
  if (this.createMuteButton === true)
104
104
  this.context.menu.showAudioPlaybackOption(true);
105
- if (this.showSpatialMenu === true)
105
+ // Apply the value in BOTH directions: the spatial menu is enabled by
106
+ // default, so unchecking the option must explicitly disable it. Only skip
107
+ // when the field is unset (undefined) to keep the engine default.
108
+ if (this.showSpatialMenu !== undefined)
106
109
  this.context.menu.showSpatialMenu(this.showSpatialMenu);
107
110
  if (this.createQRCodeButton === true) {
108
111
  if (!DeviceUtilities.isMobileDevice()) {
@@ -56,6 +56,14 @@ function applyRotationAttribute(attr: string, target: { set(x: number, y: number
56
56
  }
57
57
 
58
58
  function applyTexture(context: IContext, texture: Texture, isBackground: boolean, isEnvironment: boolean) {
59
+ // Textures load asynchronously. The context can be disposed/torn down while a
60
+ // load is in flight (element removed, context disposed, scene switched) — at
61
+ // which point context.scene is set to null on dispose. Bail instead of
62
+ // dereferencing it (the rest of this function also touches context.scene).
63
+ if (!context.scene) {
64
+ if (debug) console.warn("Skybox: skipping applyTexture — context was torn down before the texture finished loading");
65
+ return;
66
+ }
59
67
  if (!(texture instanceof CubeTexture || texture instanceof CompressedCubeTexture)
60
68
  && texture.mapping !== CubeUVReflectionMapping) {
61
69
  texture.mapping = EquirectangularRefractionMapping;
@@ -3,7 +3,7 @@ import { Euler, Material, Matrix4, Mesh, Object3D, Quaternion, Vector3 } from "t
3
3
 
4
4
  import { isDevEnvironment, showBalloonMessage, showBalloonWarning } from "../../../engine/debug/index.js";
5
5
  import { findObjectOfType } from "../../../engine/engine_components.js";
6
- import { $KhjWEFh } from "../../../engine/engine_license.js";
6
+ import { _$gQv } from "../../../engine/engine_license.js";
7
7
  import { serializable } from "../../../engine/engine_serialization.js";
8
8
  import { getFormattedDate, Progress } from "../../../engine/engine_time_utils.js";
9
9
  import { DeviceUtilities, getParam } from "../../../engine/engine_utils.js";
@@ -277,7 +277,7 @@ export class USDZExporter extends Behaviour {
277
277
  let name = this.exportFileName ?? this.objectToExport?.name ?? this.name;
278
278
  name += "-" + getFormattedDate(); // seems iOS caches the file in some cases, this ensures we always have a fresh file
279
279
 
280
- if (!$KhjWEFh()) {
280
+ if (!_$gQv()) {
281
281
  if (name !== "") name += "-";
282
282
  name += "MadeWithNeedle";
283
283
  }
@@ -682,7 +682,7 @@ export class USDZExporter extends Behaviour {
682
682
  if (debug)
683
683
  showBalloonMessage("Quicklook url: " + callToActionURL);
684
684
  if (callToActionURL) {
685
- if (!$KhjWEFh()) {
685
+ if (!_$gQv()) {
686
686
  console.warn("Quicklook closed: custom redirects require a Needle Engine Pro license: https://needle.tools/pricing", callToActionURL)
687
687
  }
688
688
  else {
@@ -697,7 +697,7 @@ export class USDZExporter extends Behaviour {
697
697
  private buildQuicklookOverlay(): CustomBranding {
698
698
  const obj: CustomBranding = {};
699
699
  if (this.customBranding) Object.assign(obj, this.customBranding);
700
- if (!$KhjWEFh()) {
700
+ if (!_$gQv()) {
701
701
  console.log("Custom Quicklook banner text requires pro license: https://needle.tools/pricing");
702
702
  obj.callToAction = "Close";
703
703
  obj.checkoutTitle = "🌵 Made with Needle";
@@ -11,6 +11,7 @@ import { Animation } from "../../../../Animation.js";
11
11
  import { Animator } from "../../../../Animator.js";
12
12
  import { AudioSource } from "../../../../AudioSource.js";
13
13
  import { Behaviour, GameObject } from "../../../../Component.js";
14
+ import type { ComponentInit } from "../../../../../engine/engine_types.js";
14
15
  import { Rigidbody } from "../../../../RigidBody.js";
15
16
  import type { IPointerClickHandler, PointerEventData } from "../../../../ui/PointerEvents.js";
16
17
  import { makeNameSafeForUSD, USDDocument, USDObject, USDZExporterContext } from "../../ThreeUSDZExporter.js";
@@ -811,13 +812,16 @@ export class PlayAudioOnClick extends Behaviour implements IPointerClickHandler,
811
812
 
812
813
  ensureAudioSource() {
813
814
  if (!this.target) {
814
- const newAudioSource = this.gameObject.addComponent(AudioSource);
815
+ const initializeAudioSource: ComponentInit<AudioSource> = {
816
+ spatialBlend: 1,
817
+ volume: 1,
818
+ loop: false,
819
+ clip: this.clip,
820
+ preload: true,
821
+ };
822
+ const newAudioSource = this.gameObject.addComponent(AudioSource, initializeAudioSource);
815
823
  if (newAudioSource) {
816
824
  this.target = newAudioSource;
817
- newAudioSource.spatialBlend = 1;
818
- newAudioSource.volume = 1;
819
- newAudioSource.loop = false;
820
- newAudioSource.preload = true;
821
825
  }
822
826
  }
823
827
  }
@@ -828,6 +832,9 @@ export class PlayAudioOnClick extends Behaviour implements IPointerClickHandler,
828
832
  label: "Play audio: " + (this.clip || this.target?.clip || "unknown clip"),
829
833
  hidden: false,
830
834
  })
835
+
836
+ this.ensureAudioSource();
837
+
831
838
  }
832
839
  onDisable(): void {
833
840
  this.context.accessibility.updateElement(this, {
@@ -593,6 +593,10 @@ class ColorBehaviour extends ParticleSystemBaseBehaviour {
593
593
  col.copy(this.system.main.startColor.evaluate(Math.random()));
594
594
  if (materialColor?.color) {
595
595
  tempColor.copy(materialColor.color);
596
+ // THREE.Color has no alpha — the material's color alpha is stored in
597
+ // material.opacity. Without this, a particle material's alpha/opacity
598
+ // was ignored (reported externally). multiply() propagates alpha.
599
+ tempColor.alpha = materialColor.opacity ?? 1;
596
600
  col.multiply(tempColor)
597
601
  }
598
602
  col.convertLinearToSRGB();
@@ -567,6 +567,7 @@ export class WebXRImageTracking extends Behaviour {
567
567
  * });
568
568
  * ```
569
569
  */
570
+ @serializable(EventList)
570
571
  imageTracked: EventList<WebXRImageTrackingEvent> = new EventList();
571
572
 
572
573
  /** @inheritdoc */