@needle-tools/engine 5.0.4 → 5.0.6

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 (91) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/components.needle.json +1 -1
  3. package/dist/{needle-engine.bundle-CX-SJZzp.min.js → needle-engine.bundle-BNqZHrmH.min.js} +142 -141
  4. package/dist/{needle-engine.bundle-CYrPktak.umd.cjs → needle-engine.bundle-BY1mZ1X_.umd.cjs} +140 -139
  5. package/dist/{needle-engine.bundle-B3Km2VZ4.js → needle-engine.bundle-BsFWwS-j.js} +8183 -7957
  6. package/dist/needle-engine.d.ts +139 -14
  7. package/dist/needle-engine.js +560 -557
  8. package/dist/needle-engine.min.js +1 -1
  9. package/dist/needle-engine.umd.cjs +1 -1
  10. package/dist/{vendor-vHLk8sXu.js → vendor-CAcsI0eU.js} +116 -115
  11. package/dist/{vendor-CntUvmJu.umd.cjs → vendor-CEM38hLE.umd.cjs} +2 -2
  12. package/dist/{vendor-DPbfJJ4d.min.js → vendor-HRlxIBga.min.js} +2 -2
  13. package/lib/engine/api.d.ts +2 -0
  14. package/lib/engine/api.js +2 -0
  15. package/lib/engine/api.js.map +1 -1
  16. package/lib/engine/debug/debug_spatial_console.d.ts +2 -0
  17. package/lib/engine/debug/debug_spatial_console.js +10 -7
  18. package/lib/engine/debug/debug_spatial_console.js.map +1 -1
  19. package/lib/engine/engine_addressables.d.ts +2 -0
  20. package/lib/engine/engine_addressables.js +6 -3
  21. package/lib/engine/engine_addressables.js.map +1 -1
  22. package/lib/engine/engine_audio.d.ts +68 -0
  23. package/lib/engine/engine_audio.js +172 -0
  24. package/lib/engine/engine_audio.js.map +1 -1
  25. package/lib/engine/engine_gameobject.js +2 -2
  26. package/lib/engine/engine_gameobject.js.map +1 -1
  27. package/lib/engine/engine_init.js +8 -0
  28. package/lib/engine/engine_init.js.map +1 -1
  29. package/lib/engine/engine_mainloop_utils.js +5 -2
  30. package/lib/engine/engine_mainloop_utils.js.map +1 -1
  31. package/lib/engine/engine_serialization_builtin_serializer.js +27 -0
  32. package/lib/engine/engine_serialization_builtin_serializer.js.map +1 -1
  33. package/lib/engine/webcomponents/needle-engine.d.ts +9 -3
  34. package/lib/engine/webcomponents/needle-engine.js.map +1 -1
  35. package/lib/engine/xr/NeedleXRSession.d.ts +3 -2
  36. package/lib/engine/xr/NeedleXRSession.js +50 -14
  37. package/lib/engine/xr/NeedleXRSession.js.map +1 -1
  38. package/lib/engine-components/Animation.js +17 -16
  39. package/lib/engine-components/Animation.js.map +1 -1
  40. package/lib/engine-components/AnimatorController.d.ts +2 -0
  41. package/lib/engine-components/AnimatorController.js +4 -1
  42. package/lib/engine-components/AnimatorController.js.map +1 -1
  43. package/lib/engine-components/AudioSource.d.ts +19 -3
  44. package/lib/engine-components/AudioSource.js +121 -68
  45. package/lib/engine-components/AudioSource.js.map +1 -1
  46. package/lib/engine-components/DragControls.d.ts +7 -0
  47. package/lib/engine-components/DragControls.js +19 -0
  48. package/lib/engine-components/DragControls.js.map +1 -1
  49. package/lib/engine-components/NestedGltf.d.ts +19 -3
  50. package/lib/engine-components/NestedGltf.js +19 -3
  51. package/lib/engine-components/NestedGltf.js.map +1 -1
  52. package/lib/engine-components/Networking.d.ts +1 -1
  53. package/lib/engine-components/Networking.js +1 -1
  54. package/lib/engine-components/OrbitControls.js +16 -11
  55. package/lib/engine-components/OrbitControls.js.map +1 -1
  56. package/lib/engine-components/postprocessing/VolumeParameter.d.ts +2 -0
  57. package/lib/engine-components/postprocessing/VolumeParameter.js +4 -1
  58. package/lib/engine-components/postprocessing/VolumeParameter.js.map +1 -1
  59. package/lib/engine-components/ui/Canvas.d.ts +1 -1
  60. package/lib/engine-components/ui/Canvas.js +2 -8
  61. package/lib/engine-components/ui/Canvas.js.map +1 -1
  62. package/lib/engine-components/ui/Text.d.ts +8 -1
  63. package/lib/engine-components/ui/Text.js +29 -14
  64. package/lib/engine-components/ui/Text.js.map +1 -1
  65. package/lib/engine-components/web/CursorFollow.js +21 -12
  66. package/lib/engine-components/web/CursorFollow.js.map +1 -1
  67. package/lib/engine-components/webxr/WebXRImageTracking.js +4 -0
  68. package/lib/engine-components/webxr/WebXRImageTracking.js.map +1 -1
  69. package/package.json +1 -1
  70. package/src/engine/api.ts +3 -0
  71. package/src/engine/debug/debug_spatial_console.ts +10 -7
  72. package/src/engine/engine_addressables.ts +6 -3
  73. package/src/engine/engine_audio.ts +184 -0
  74. package/src/engine/engine_gameobject.ts +2 -2
  75. package/src/engine/engine_init.ts +8 -0
  76. package/src/engine/engine_mainloop_utils.ts +5 -2
  77. package/src/engine/engine_serialization_builtin_serializer.ts +31 -3
  78. package/src/engine/webcomponents/needle-engine.ts +9 -3
  79. package/src/engine/xr/NeedleXRSession.ts +48 -13
  80. package/src/engine-components/Animation.ts +19 -16
  81. package/src/engine-components/AnimatorController.ts +4 -1
  82. package/src/engine-components/AudioSource.ts +130 -79
  83. package/src/engine-components/DragControls.ts +18 -2
  84. package/src/engine-components/NestedGltf.ts +20 -4
  85. package/src/engine-components/Networking.ts +1 -1
  86. package/src/engine-components/OrbitControls.ts +18 -9
  87. package/src/engine-components/postprocessing/VolumeParameter.ts +4 -1
  88. package/src/engine-components/ui/Canvas.ts +2 -8
  89. package/src/engine-components/ui/Text.ts +43 -18
  90. package/src/engine-components/web/CursorFollow.ts +21 -13
  91. package/src/engine-components/webxr/WebXRImageTracking.ts +2 -0
@@ -9,9 +9,9 @@ import { EventList } from "./EventList.js";
9
9
  const debug = getParam("debugnestedgltf");
10
10
 
11
11
  /**
12
- * NestedGltf loads and instantiates a glTF file when the component starts.
13
- * NestedGltf components are created by the Unity exporter when nesting Objects with the GltfObject component (in Unity).
14
- * Use this for lazy-loading content, modular scene composition, or dynamic asset loading.
12
+ * NestedGltf loads and instantiates a glTF file when the component starts.
13
+ * This enables splitting a scene into multiple `.glb` files that are loaded on demand,
14
+ * keeping initial load times small and allowing parts of a scene to be reused across different contexts.
15
15
  *
16
16
  * ![](https://cloud.needle.tools/-/media/lJKrr_2tWlqRFdFc46U4bQ.gif)
17
17
  *
@@ -23,6 +23,23 @@ const debug = getParam("debugnestedgltf");
23
23
  * - Progress callbacks for loading UI
24
24
  * - Preloading support for faster display
25
25
  * - Event callback when loading completes
26
+ *
27
+ * **Limitations:**
28
+ * - Loads only once; there is no public API to unload and reload content
29
+ * - Requires a parent object — does nothing if the GameObject has no parent
30
+ * - Not intended to be added from code. For loading glTF assets at runtime,
31
+ * use {@link AssetReference.loadAsset} or {@link SceneSwitcher} instead
32
+ *
33
+ * **Unity Editor usage:**
34
+ * NestedGltf components are created automatically by the exporter when nesting GltfObject components:
35
+ * - A parent GameObject with a `GltfObject` component is exported as a `.glb` file
36
+ * - Any child GameObject that also has a `GltfObject` component is exported as a separate `.glb` file
37
+ * - The child's `GltfObject` is replaced with a `NestedGltf` reference pointing to that separate `.glb`
38
+ *
39
+ * ```
40
+ * Parent [GltfObject] → exported as parent.glb
41
+ * └─ Child [GltfObject] → exported as child.glb, replaced with NestedGltf in parent.glb
42
+ * ```
26
43
  *
27
44
  * @example Load a glTF when object becomes active
28
45
  * ```ts
@@ -40,7 +57,6 @@ const debug = getParam("debugnestedgltf");
40
57
  * // Later, when object becomes active, it displays instantly
41
58
  * ```
42
59
  *
43
- * @summary Loads and instantiates a nested glTF file
44
60
  * @category Asset Management
45
61
  * @group Components
46
62
  * @see {@link AssetReference} for asset loading utilities
@@ -37,7 +37,7 @@ const debug = getParam("debugnet");
37
37
  * @see {@link RoomEvents} for room lifecycle events
38
38
  * @see {@link isLocalNetwork} for local network detection
39
39
  * @link https://engine.needle.tools/docs/how-to-guides/networking/
40
- * @summary Networking configuration
40
+ * @summary Configures the websocket server URL for multiplayer networking
41
41
  * @category Networking
42
42
  * @group Components
43
43
  */
@@ -758,19 +758,26 @@ export class OrbitControls extends Behaviour implements ICameraController {
758
758
 
759
759
  if (this.targetBounds) {
760
760
  // #region target bounds
761
- const targetVector = this._controls.target;
762
761
  const boundsCenter = this.targetBounds.worldPosition;
763
762
  const boundsHalfSize = getTempVector(this.targetBounds.worldScale).multiplyScalar(0.5);
764
763
  const min = getTempVector(boundsCenter).sub(boundsHalfSize);
765
764
  const max = getTempVector(boundsCenter).add(boundsHalfSize);
766
- const newTarget = getTempVector(this._controls.target).clamp(min, max);
767
- const duration = .1;
768
- if (duration <= 0) targetVector.copy(newTarget);
769
- else targetVector.lerp(newTarget, this.context.time.deltaTime / duration);
765
+
770
766
  if (this._lookTargetLerpActive) {
771
- if (duration <= 0) this._lookTargetEndPosition.copy(newTarget);
772
- else this._lookTargetEndPosition.lerp(newTarget, this.context.time.deltaTime / (duration * 5));
767
+ // During a programmatic transition (fitCamera / setLookTargetPosition with immediate: false),
768
+ // only clamp the destination. The look-target lerp (above) handles moving _controls.target
769
+ // towards the endpoint — we must not fight it by also lerping _controls.target here.
770
+ this._lookTargetEndPosition.clamp(min, max);
771
+ }
772
+ else {
773
+ // Interactive use (pan/orbit): smoothly push the target back into bounds
774
+ const targetVector = this._controls.target;
775
+ const newTarget = getTempVector(targetVector).clamp(min, max);
776
+ const duration = .1;
777
+ if (duration <= 0) targetVector.copy(newTarget);
778
+ else targetVector.lerp(newTarget, Math.min(1, this.context.time.deltaTime / duration));
773
779
  }
780
+
774
781
  if (debug) {
775
782
  Gizmos.DrawWireBox(boundsCenter, boundsHalfSize.multiplyScalar(2), 0xffaa00);
776
783
  }
@@ -847,7 +854,8 @@ export class OrbitControls extends Behaviour implements ICameraController {
847
854
  this._controls.update(this.context.time.deltaTime);
848
855
 
849
856
  if (debug) {
850
- Gizmos.DrawWireSphere(this._controls.target, 0.1, 0x00ff00);
857
+ const distance = this._controls.getDistance();
858
+ Gizmos.DrawWireSphere(this._controls.target, 0.01 * distance, 0x00ff00);
851
859
  }
852
860
  }
853
861
  }
@@ -1022,7 +1030,8 @@ export class OrbitControls extends Behaviour implements ICameraController {
1022
1030
 
1023
1031
  if (debug) {
1024
1032
  console.warn("OrbitControls: setLookTargetPosition", position, immediateOrDuration);
1025
- Gizmos.DrawWireSphere(this._lookTargetEndPosition, .2, 0xff0000, 2);
1033
+ const distance = this._controls.getDistance();
1034
+ Gizmos.DrawWireSphere(this._lookTargetEndPosition, 0.01 * distance, 0xff5500, 2);
1026
1035
  }
1027
1036
 
1028
1037
  if (immediateOrDuration === true) {
@@ -156,4 +156,7 @@ class VolumeParameterSerializer extends TypeSerializer {
156
156
  return parameter;
157
157
  }
158
158
  }
159
- new VolumeParameterSerializer();
159
+ /** @internal */
160
+ export function initVolumeParameterSerializer() {
161
+ new VolumeParameterSerializer();
162
+ }
@@ -4,7 +4,7 @@ import * as ThreeMeshUI from 'three-mesh-ui'
4
4
  import { Mathf } from "../../engine/engine_math.js";
5
5
  import { serializable } from "../../engine/engine_serialization_decorator.js";
6
6
  import { FrameEvent } from "../../engine/engine_setup.js";
7
- import { delayForFrames, getParam } from "../../engine/engine_utils.js";
7
+ import { getParam } from "../../engine/engine_utils.js";
8
8
  import { type NeedleXREventArgs } from "../../engine/xr/api.js";
9
9
  import { Camera } from "../Camera.js";
10
10
  import { GameObject } from "../Component.js";
@@ -236,19 +236,13 @@ export class Canvas extends UIRootComponent implements ICanvas {
236
236
  }
237
237
  }
238
238
 
239
- async onEnterXR(args: NeedleXREventArgs) {
239
+ onEnterXR(args: NeedleXREventArgs) {
240
240
  // workaround for https://linear.app/needle/issue/NE-4114
241
241
  if (this.screenspace) {
242
242
  if (args.xr.isVR || args.xr.isPassThrough) {
243
243
  this.gameObject.visible = false;
244
244
  }
245
245
  }
246
- else {
247
- this.gameObject.visible = false;
248
- await delayForFrames(1).then(()=>{
249
- this.gameObject.visible = true;
250
- });
251
- }
252
246
  }
253
247
  onLeaveXR(args: NeedleXREventArgs): void {
254
248
  if (this.screenspace) {
@@ -81,7 +81,24 @@ export class Text extends Graphic implements IHasAlphaFactor, ICanvasEventReceiv
81
81
  @serializable()
82
82
  supportRichText: boolean = false;
83
83
  @serializable(URL)
84
- font?: string;
84
+ set font(val: string | null) {
85
+ if (val !== this._font) {
86
+ this._font = val;
87
+ // If the font is assigned during deserialization it means the font URL MUST be resolved with the style suffix. If it's assigned at runtime by a user invocation and is absolute then we assume the user is prividing a full path to the font asset json or png
88
+ this._assignedAtRuntime = this.__didAwake;
89
+ }
90
+ }
91
+ get font(): string | null {
92
+ return this._font;
93
+ }
94
+ private _font: string | null = null;
95
+ private _assignedAtRuntime: boolean = true;
96
+
97
+
98
+ /**
99
+ * Whether to support basic rich text tags in the `text` property. Supported tags include `<b>`, `<i>`, and `<color=hex>`. For example: `Hello <b>World</b>` or `Score: <color=#ff0000>100</color>`
100
+ * @default false
101
+ */
85
102
  @serializable()
86
103
  fontStyle: FontStyle = FontStyle.Normal;
87
104
 
@@ -224,8 +241,15 @@ export class Text extends Graphic implements IHasAlphaFactor, ICanvasEventReceiv
224
241
  // fontSize /= this.canvas?.scaleFactor;
225
242
  // }
226
243
 
227
-
228
- const textOpts = {
244
+ const textOpts: {
245
+ color: Color,
246
+ fontOpacity: number,
247
+ fontSize: number,
248
+ fontKerning: string,
249
+ whiteSpace?: string,
250
+ overflow?: string,
251
+ lineHeight?: number
252
+ } = {
229
253
  color: this.color,
230
254
  fontOpacity: this.color.alpha,
231
255
  fontSize: fontSize,
@@ -486,15 +510,16 @@ export class Text extends Graphic implements IHasAlphaFactor, ICanvasEventReceiv
486
510
  * @private
487
511
  */
488
512
  private setFont(opts: ThreeMeshUIEveryOptions, fontStyle: FontStyle) {
513
+
514
+ if (!this.font) {
515
+ if(debug) console.warn("No font set for Text component, skipping font setup");
516
+ return;
517
+ }
489
518
 
490
- // @TODO : THH could be useful to uniformize font family name :
491
- // This would ease possible html/vr matching
492
- // - Arial instead of assets/arial
493
- // - Arial should stay Arial instead of arial
494
- if (!this.font) return;
495
519
  const fontName = this.font;
496
520
  const familyName = this.getFamilyNameWithCorrectSuffix(fontName, fontStyle);
497
- if (debug) console.log("Selected font family:" + familyName);
521
+
522
+ if (debug) console.log("Selected font family:" + familyName, this.font);
498
523
 
499
524
  // ensure a font family is register under this name
500
525
  let fontFamily = ThreeMeshUI.FontLibrary.getFontFamily(familyName as string);
@@ -527,9 +552,7 @@ export class Text extends Graphic implements IHasAlphaFactor, ICanvasEventReceiv
527
552
  opts.fontWeight = 400;
528
553
  }
529
554
 
530
-
531
555
  // Ensure a fontVariant is registered
532
- //@TODO: @swingingtom add type for fontWeight
533
556
  let fontVariant = fontFamily.getVariant(opts.fontWeight as any as string, opts.fontStyle);
534
557
  if (!fontVariant) {
535
558
  let jsonPath = familyName;
@@ -550,13 +573,7 @@ export class Text extends Graphic implements IHasAlphaFactor, ICanvasEventReceiv
550
573
 
551
574
  private getFamilyNameWithCorrectSuffix(familyName: string, style: FontStyle): string {
552
575
 
553
-
554
- // the URL decorator resolves the URL to absolute URLs - we need to remove the domain part since we're only interested in the path
555
- if (familyName.startsWith("https:") || familyName.startsWith("http:")) {
556
- const url = new URL(familyName);
557
- familyName = url.pathname;
558
- }
559
-
576
+ const isAbsolute = familyName.startsWith("https:") || familyName.startsWith("http:");
560
577
 
561
578
  // we can only change the style for the family if the name has a suffix (e.g. Arial-Bold)
562
579
  const styleSeparator = familyName.lastIndexOf('-');
@@ -580,8 +597,16 @@ export class Text extends Graphic implements IHasAlphaFactor, ICanvasEventReceiv
580
597
  }
581
598
  const isUpperCase = fontBaseName[0] === fontBaseName[0].toUpperCase();
582
599
  const fontNameWithoutSuffix = familyName.substring(0, styleSeparator > pathSeparatorIndex ? styleSeparator : familyName.length);
600
+
583
601
  if (debug) console.log("Select font: ", familyName, FontStyle[style], fontBaseName, isUpperCase, fontNameWithoutSuffix);
584
602
 
603
+ /**
604
+ * If a user provides a font with an absolute URL AND the font name does not end with "-msdf.json" or ".png" (e.g. "https://example.com/fonts/Arial-Bold"), we will assume that the user is providing the full path to the font files and we will not try to modify the font name based on the style. This allows users to have more control over the font files they are using, especially if they are hosting their own fonts or using a custom font provider that does not follow the same naming conventions as our default fonts.
605
+ */
606
+ if (isAbsolute && this._assignedAtRuntime && !(familyName.endsWith("-msdf.json") || familyName.endsWith(".png"))) {
607
+ return fontNameWithoutSuffix;
608
+ }
609
+
585
610
  switch (style) {
586
611
  case FontStyle.Normal:
587
612
  if (isUpperCase) return fontNameWithoutSuffix + "-Regular";
@@ -289,20 +289,28 @@ export class CursorFollow extends Behaviour {
289
289
 
290
290
 
291
291
  if (this.snapToSurface) {
292
- ray.origin = _position;
293
- ray.direction = rayDirection.multiplyScalar(-1);
294
- const hits = this.context.physics.raycastFromRay(ray);
295
- if (hits?.length) {
296
- const hit = hits[0];
297
- if (this.damping > 0) {
298
- this.gameObject.worldPosition = _position.lerp(hit.point, this.context.time.deltaTime / this.damping);
299
- }
300
- else {
301
- this.gameObject.worldPosition = hit.point;
292
+ ray.origin = cameraPosition;
293
+ ray.direction = rayDirection;
294
+ const hits = this.context.physics.raycastFromRay(ray, {
295
+ testObject: obj => {
296
+ return obj !== this.gameObject && !this.gameObject.contains(obj) ? true : false
302
297
  }
303
-
304
- if(debug) {
305
- Gizmos.DrawLine(hit.point, hit.normal!.add(hit.point), 0x00FF00);
298
+ });
299
+ if (hits?.length) {
300
+ // Get the first hit that is not a child of *this* object. Because we do not want to lerp to a surface that is part of the object itself, since that would result in *this* object moving closer and closer to the camera as it tries to snap to itself
301
+ const hit = hits[0];//.find(h => !this.gameObject.contains(h.object));
302
+ if (hit) {
303
+ if (this.damping > 0) {
304
+ this.gameObject.worldPosition = _position.lerp(hit.point, this.context.time.deltaTime / this.damping);
305
+ // this._distance = this.gameObject.worldPosition.distanceTo(cameraPosition);
306
+ }
307
+ else {
308
+ this.gameObject.worldPosition = hit.point;
309
+ // this._distance = hit.point.distanceTo(cameraPosition);
310
+ }
311
+ if (debug) {
312
+ Gizmos.DrawLine(hit.point, hit.normal!.add(hit.point), 0x00FF00);
313
+ }
306
314
  }
307
315
  }
308
316
  }
@@ -886,9 +886,11 @@ async function loadImage(url: string) {
886
886
  }
887
887
  const promise = new Promise<boolean>(res => {
888
888
  _imageElements.set(url, null);
889
+ if(isDevEnvironment() || debug) console.debug(`[WebXRImageTracking] Start loading image for tracking: ${url}`);
889
890
  const imageElement = document.createElement("img") as HTMLImageElement;
890
891
  imageElement.src = url;
891
892
  imageElement.addEventListener("load", async () => {
893
+ if(isDevEnvironment() || debug) console.debug(`[WebXRImageTracking] Loaded image for tracking: ${url}`);
892
894
  const img = await createImageBitmap(imageElement);
893
895
  _imageElements.set(url, img);
894
896
  res(true);