@needle-tools/engine 4.8.7-next.e134730 → 4.8.8-next.5537a55

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 (89) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +31 -23
  3. package/components.needle.json +1 -1
  4. package/dist/{needle-engine.bundle-CvRpjtJj.js → needle-engine.bundle-BoZqXGLO.js} +6957 -6906
  5. package/dist/{needle-engine.bundle-DrGsgE4t.min.js → needle-engine.bundle-CpsmWsJo.min.js} +144 -144
  6. package/dist/{needle-engine.bundle-X9nxhICu.umd.cjs → needle-engine.bundle-D6vHQoPC.umd.cjs} +147 -147
  7. package/dist/needle-engine.js +2 -2
  8. package/dist/needle-engine.min.js +1 -1
  9. package/dist/needle-engine.umd.cjs +1 -1
  10. package/lib/engine/engine_addressables.d.ts +12 -12
  11. package/lib/engine/engine_addressables.js +30 -23
  12. package/lib/engine/engine_addressables.js.map +1 -1
  13. package/lib/engine/engine_animation.d.ts +1 -3
  14. package/lib/engine/engine_animation.js +15 -10
  15. package/lib/engine/engine_animation.js.map +1 -1
  16. package/lib/engine/engine_assetdatabase.js +6 -6
  17. package/lib/engine/engine_assetdatabase.js.map +1 -1
  18. package/lib/engine/engine_camera.d.ts +8 -1
  19. package/lib/engine/engine_camera.js +25 -0
  20. package/lib/engine/engine_camera.js.map +1 -1
  21. package/lib/engine/engine_context.d.ts +9 -0
  22. package/lib/engine/engine_context.js +15 -0
  23. package/lib/engine/engine_context.js.map +1 -1
  24. package/lib/engine/engine_gameobject.js +2 -2
  25. package/lib/engine/engine_gameobject.js.map +1 -1
  26. package/lib/engine/engine_loaders.callbacks.d.ts +1 -0
  27. package/lib/engine/engine_loaders.callbacks.js +1 -0
  28. package/lib/engine/engine_loaders.callbacks.js.map +1 -1
  29. package/lib/engine/engine_loaders.js +15 -11
  30. package/lib/engine/engine_loaders.js.map +1 -1
  31. package/lib/engine/webcomponents/needle-engine.attributes.d.ts +1 -0
  32. package/lib/engine/webcomponents/needle-engine.d.ts +2 -2
  33. package/lib/engine/webcomponents/needle-engine.js +19 -21
  34. package/lib/engine/webcomponents/needle-engine.js.map +1 -1
  35. package/lib/engine-components/Animation.js +2 -1
  36. package/lib/engine-components/Animation.js.map +1 -1
  37. package/lib/engine-components/AnimationUtilsAutoplay.js +1 -6
  38. package/lib/engine-components/AnimationUtilsAutoplay.js.map +1 -1
  39. package/lib/engine-components/Camera.d.ts +2 -0
  40. package/lib/engine-components/Camera.js +5 -1
  41. package/lib/engine-components/Camera.js.map +1 -1
  42. package/lib/engine-components/DropListener.d.ts +21 -15
  43. package/lib/engine-components/DropListener.js +38 -34
  44. package/lib/engine-components/DropListener.js.map +1 -1
  45. package/lib/engine-components/LookAtConstraint.d.ts +5 -1
  46. package/lib/engine-components/LookAtConstraint.js +8 -0
  47. package/lib/engine-components/LookAtConstraint.js.map +1 -1
  48. package/lib/engine-components/OrbitControls.d.ts +30 -9
  49. package/lib/engine-components/OrbitControls.js +53 -14
  50. package/lib/engine-components/OrbitControls.js.map +1 -1
  51. package/lib/engine-components/Skybox.js +8 -9
  52. package/lib/engine-components/Skybox.js.map +1 -1
  53. package/lib/engine-components/api.d.ts +1 -0
  54. package/lib/engine-components/api.js.map +1 -1
  55. package/lib/engine-components/export/usdz/Extension.d.ts +1 -1
  56. package/lib/engine-components/export/usdz/ThreeUSDZExporter.js +1 -1
  57. package/lib/engine-components/export/usdz/ThreeUSDZExporter.js.map +1 -1
  58. package/lib/engine-components/export/usdz/USDZExporter.d.ts +7 -0
  59. package/lib/engine-components/export/usdz/USDZExporter.js +8 -1
  60. package/lib/engine-components/export/usdz/USDZExporter.js.map +1 -1
  61. package/lib/engine-components/webxr/WebXRImageTracking.d.ts +4 -2
  62. package/lib/engine-components/webxr/WebXRImageTracking.js +117 -81
  63. package/lib/engine-components/webxr/WebXRImageTracking.js.map +1 -1
  64. package/package.json +1 -1
  65. package/plugins/common/files.js +6 -3
  66. package/plugins/vite/alias.js +26 -17
  67. package/plugins/vite/editor-connection.js +4 -4
  68. package/src/engine/engine_addressables.ts +44 -33
  69. package/src/engine/engine_animation.ts +17 -10
  70. package/src/engine/engine_assetdatabase.ts +7 -7
  71. package/src/engine/engine_camera.ts +40 -1
  72. package/src/engine/engine_context.ts +21 -1
  73. package/src/engine/engine_gameobject.ts +2 -2
  74. package/src/engine/engine_loaders.callbacks.ts +1 -0
  75. package/src/engine/engine_loaders.ts +18 -13
  76. package/src/engine/webcomponents/needle-engine.attributes.ts +2 -0
  77. package/src/engine/webcomponents/needle-engine.ts +21 -21
  78. package/src/engine-components/Animation.ts +1 -1
  79. package/src/engine-components/AnimationUtilsAutoplay.ts +1 -6
  80. package/src/engine-components/Camera.ts +7 -1
  81. package/src/engine-components/DropListener.ts +44 -34
  82. package/src/engine-components/LookAtConstraint.ts +9 -1
  83. package/src/engine-components/OrbitControls.ts +78 -22
  84. package/src/engine-components/Skybox.ts +9 -10
  85. package/src/engine-components/api.ts +2 -1
  86. package/src/engine-components/export/usdz/Extension.ts +1 -1
  87. package/src/engine-components/export/usdz/ThreeUSDZExporter.ts +1 -1
  88. package/src/engine-components/export/usdz/USDZExporter.ts +21 -12
  89. package/src/engine-components/webxr/WebXRImageTracking.ts +138 -90
@@ -2,6 +2,7 @@ import { AnimationAction, AnimationClip, AnimationMixer, Object3D, PropertyBindi
2
2
 
3
3
  import type { Context } from "./engine_context.js";
4
4
  import { GLTF, IAnimationComponent, Model } from "./engine_types.js";
5
+ import { TypeStore } from "./engine_typestore.js";
5
6
 
6
7
  /**
7
8
  * Registry for animation related data. Use {@link registerAnimationMixer} to register an animation mixer instance.
@@ -88,12 +89,15 @@ export class AnimationUtils {
88
89
  * This method will look for objects in the scene that have animations and assign them to the correct objects.
89
90
  * @param file The GLTF file to assign the animations from
90
91
  */
91
- static assignAnimationsFromFile(file: Pick<Model, "animations" | "scene">, opts?: { createAnimationComponent(obj: Object3D, animation: AnimationClip): IAnimationComponent }) {
92
+ static autoplayAnimations(file: Object3D | Pick<Model, "animations" | "scene">): Array<IAnimationComponent> | null {
92
93
  if (!file || !file.animations) {
93
94
  console.debug("No animations found in file");
94
- return;
95
+ return null;
95
96
  }
96
97
 
98
+ const scene = "scene" in file ? file.scene : file as Object3D;
99
+ const animationComponents = new Array<IAnimationComponent>();
100
+
97
101
  for (let i = 0; i < file.animations.length; i++) {
98
102
  const animation = file.animations[i];
99
103
  if (!animation.tracks || animation.tracks.length <= 0) {
@@ -103,12 +107,12 @@ export class AnimationUtils {
103
107
  for (const t in animation.tracks) {
104
108
  const track = animation.tracks[t];
105
109
  const parsedPath = PropertyBinding.parseTrackName(track.name);
106
- let obj = PropertyBinding.findNode(file.scene, parsedPath.nodeName);
110
+ let obj = PropertyBinding.findNode(scene, parsedPath.nodeName);
107
111
  if (!obj) {
108
112
  const objectName = track["__objectName"] ?? track.name.substring(0, track.name.indexOf("."));
109
113
  // let obj = gltf.scene.getObjectByName(objectName);
110
114
  // this finds unnamed objects that still have tracks targeting them
111
- obj = file.scene.getObjectByProperty('uuid', objectName);
115
+ obj = scene.getObjectByProperty('uuid', objectName);
112
116
 
113
117
  if (!obj) {
114
118
  // console.warn("could not find " + objectName, animation, gltf.scene);
@@ -116,20 +120,23 @@ export class AnimationUtils {
116
120
  }
117
121
  }
118
122
 
119
- let animationComponent = findAnimationGameObjectInParent(obj);
123
+ let animationComponent = findAnimationGameObjectInParent(obj) || findAnimationGameObjectInParent(scene);
120
124
  if (!animationComponent) {
121
- if (!opts?.createAnimationComponent) {
122
- console.warn("No AnimationComponent found in parent hierarchy of object and no 'createAnimationComponent' callback was provided in options.")
123
- continue;
125
+ const anim = TypeStore.get("Animation");
126
+ animationComponent = scene.addComponent(anim);
127
+ if (!animationComponent) {
128
+ console.error("Failed creating Animation component: No 'Animation' component found in TypeStore");
129
+ break;
124
130
  }
125
- animationComponent = opts.createAnimationComponent(file.scene, animation);
126
- obj.addComponent(animationComponent);
127
131
  }
132
+ animationComponents.push(animationComponent);
128
133
  if (animationComponent.addClip) {
129
134
  animationComponent.addClip(animation);
130
135
  }
131
136
  }
132
137
  }
138
+ return animationComponents;
139
+
133
140
  function findAnimationGameObjectInParent(obj): IAnimationComponent | null {
134
141
  if (!obj) return null;
135
142
  const components = obj.userData?.components;
@@ -61,7 +61,7 @@ export function disposeObjectResources(obj: object | null | undefined) {
61
61
  return;
62
62
  }
63
63
 
64
- if(typeof obj === "object") {
64
+ if (typeof obj === "object") {
65
65
  obj[$disposed] = true;
66
66
  }
67
67
 
@@ -79,8 +79,8 @@ export function disposeObjectResources(obj: object | null | undefined) {
79
79
  disposeObjectResources(obj.bindMatrixInverse);
80
80
  disposeObjectResources(obj.customDepthMaterial);
81
81
  disposeObjectResources(obj.customDistanceMaterial);
82
- obj.geometry = null;
83
- obj.material = null;
82
+ obj.geometry = {};
83
+ obj.material = {};
84
84
  obj.visible = false;
85
85
  }
86
86
  else if (obj instanceof Mesh) {
@@ -88,8 +88,8 @@ export function disposeObjectResources(obj: object | null | undefined) {
88
88
  disposeObjectResources(obj.material);
89
89
  disposeObjectResources(obj.customDepthMaterial);
90
90
  disposeObjectResources(obj.customDistanceMaterial);
91
- obj.geometry = null;
92
- obj.material = null;
91
+ obj.geometry = {};
92
+ obj.material = {};
93
93
  obj.visible = false;
94
94
  }
95
95
  else if (obj instanceof BufferGeometry) {
@@ -175,8 +175,8 @@ function free(obj: any) {
175
175
 
176
176
  export function __internalNotifyObjectDestroyed(obj: Object3D) {
177
177
  if (obj instanceof Mesh || obj instanceof SkinnedMesh) {
178
- obj.material = null;
179
- obj.geometry = null;
178
+ obj.material = {};
179
+ obj.geometry = {};
180
180
  }
181
181
  }
182
182
 
@@ -1,5 +1,6 @@
1
- import { Camera, Object3D } from "three";
1
+ import { Camera, HemisphereLightHelper, Object3D, PerspectiveCamera, Vector2, WebGLRenderer } from "three";
2
2
 
3
+ import { Mathf } from "./engine_math.js";
3
4
  import type { ICameraController } from "./engine_types.js";
4
5
 
5
6
 
@@ -37,4 +38,42 @@ export function useForAutoFit(obj: Object3D): boolean {
37
38
  */
38
39
  export function setAutoFitEnabled(obj: Object3D, enabled: boolean): void {
39
40
  obj[autofit] = enabled;
41
+ }
42
+
43
+
44
+
45
+
46
+ export type FocusRect = DOMRect | Element | { x: number, y: number, width: number, height: number };
47
+
48
+ let rendererRect: DOMRect | undefined = undefined;
49
+ const overlapRect = { x: 0, y: 0, width: 0, height: 0 };
50
+
51
+ export function updateCameraFocusRect(focusRect: FocusRect, dt: number, camera: PerspectiveCamera, renderer: WebGLRenderer) {
52
+
53
+ if (focusRect instanceof Element) {
54
+ focusRect = focusRect.getBoundingClientRect();
55
+ }
56
+ rendererRect = renderer.domElement.getBoundingClientRect();
57
+
58
+ const rect = overlapRect;
59
+ rect.x = focusRect.x;
60
+ rect.y = focusRect.y;
61
+ rect.width = focusRect.width;
62
+ rect.height = focusRect.height;
63
+
64
+ rect.x -= rendererRect.x;
65
+ rect.y -= rendererRect.y;
66
+
67
+ const targetX = rect.width / -2 - (rect.x - (rendererRect.width / 2));
68
+ const targetY = rect.height / -2 - (rect.y - (rendererRect.height / 2));
69
+
70
+ const view = camera.view;
71
+
72
+ let offsetX = view?.offsetX || 0;
73
+ let offsetY = view?.offsetY || 0;
74
+ offsetX = Mathf.lerp(offsetX, targetX, dt);
75
+ offsetY = Mathf.lerp(offsetY, targetY, dt);
76
+
77
+ camera.setViewOffset(rendererRect.width, rendererRect.height, offsetX, offsetY, rendererRect.width, rendererRect.height);
78
+ camera.updateProjectionMatrix();
40
79
  }
@@ -18,6 +18,7 @@ import { Addressables } from './engine_addressables.js';
18
18
  import { AnimationsRegistry } from './engine_animation.js';
19
19
  import { Application } from './engine_application.js';
20
20
  import { AssetDatabase } from './engine_assetdatabase.js';
21
+ import { FocusRect, updateCameraFocusRect } from './engine_camera.js';
21
22
  import { VERSION } from './engine_constants.js';
22
23
  import { ContextEvent, ContextRegistry } from './engine_context_registry.js';
23
24
  import { WaitForPromise } from './engine_coroutine.js';
@@ -1371,6 +1372,20 @@ export class Context implements IContext {
1371
1372
  this.internalUpdatePhysics(steps);
1372
1373
  }
1373
1374
 
1375
+
1376
+
1377
+ /**
1378
+ * Set a rect or dom element. The camera center will be moved to the center of the rect.
1379
+ * This is useful if you have Needle Engine embedded in a HTML layout and while you want the webgl background to fill e.g. the whole screen you want to move the camera center to free space.
1380
+ * For that you can simply pass in the rect or HMTL div that you want the camera to center on.
1381
+ */
1382
+ public setCameraFocusRect(rect: FocusRect | null) {
1383
+ this._focusRect = rect;
1384
+ }
1385
+ get focusRect() { return this._focusRect; }
1386
+ private _focusRect: FocusRect | null = null;
1387
+
1388
+
1374
1389
  private _lastTimestamp = 0;
1375
1390
  private _accumulatedTime = 0;
1376
1391
  private _dispatchReadyAfterFrame = false;
@@ -1421,7 +1436,7 @@ export class Context implements IContext {
1421
1436
 
1422
1437
  Context.Current = this;
1423
1438
  this.time.update();
1424
-
1439
+
1425
1440
  if (debugframerate) console.log("FPS", (this.time.smoothedFps).toFixed(0));
1426
1441
 
1427
1442
 
@@ -1498,6 +1513,11 @@ export class Context implements IContext {
1498
1513
 
1499
1514
  if (this.isVisibleToUser || this.runInBackground) {
1500
1515
 
1516
+ if (this._focusRect) {
1517
+ if (this.mainCamera instanceof PerspectiveCamera)
1518
+ updateCameraFocusRect(this._focusRect, this.time.deltaTime / .05, this.mainCamera, this.renderer);
1519
+ }
1520
+
1501
1521
  this._currentFrameEvent = FrameEvent.OnBeforeRender;
1502
1522
 
1503
1523
  // should we move these callbacks in the regular three onBeforeRender events?
@@ -166,9 +166,9 @@ export function destroy(instance: Object3D | Component, recursive: boolean = tru
166
166
  setDestroyed(obj, true);
167
167
  if (dispose) {
168
168
  disposeObjectResources(obj);
169
+ // This needs to be called after disposing because it removes the references to resources
170
+ __internalRemoveReferences(obj);
169
171
  }
170
- // This needs to be called after disposing because it removes the references to resources
171
- __internalRemoveReferences(obj);
172
172
  }
173
173
  destroyed_objects.length = 0;
174
174
  destroyed_components.length = 0;
@@ -98,6 +98,7 @@ export namespace NeedleEngineModelLoader {
98
98
  * }
99
99
  * return null;
100
100
  * });
101
+ * ```
101
102
  */
102
103
  export function onCreateCustomModelLoader(callback: CustomLoaderCallback, opts?: CustomLoaderOptions) {
103
104
  const entry = { name: opts?.name, priority: opts?.priority ?? 0, callback };
@@ -61,7 +61,7 @@ export async function onCreateLoader(url: string, context: Context): Promise<Cus
61
61
  switch (type) {
62
62
  case "unsupported":
63
63
  return null;
64
-
64
+
65
65
  default:
66
66
  case "unknown":
67
67
  {
@@ -287,15 +287,6 @@ async function onAfterLoaded(loader: Loader | CustomLoader, context: Context, gl
287
287
  gltfId = gltfId.split("?")[0];
288
288
  }
289
289
 
290
- // assign animations of loaded glTF to all scenes
291
- if ("scenes" in model) {
292
- for (const scene of model.scenes) {
293
- if (scene && !scene.animations?.length) {
294
- scene.animations = [...model.animations];
295
- }
296
- }
297
- }
298
-
299
290
  // E.g. fbx material cleanup
300
291
  postprocessLoadedFile(loader, model);
301
292
 
@@ -357,13 +348,27 @@ function checkIfUserAttemptedToLoadALocalFile(url: string) {
357
348
  /**
358
349
  * Postprocess the loaded file. This is used to apply any custom postprocessing to the loaded file.
359
350
  */
360
- function postprocessLoadedFile(loader: object, result: Model) {
351
+ function postprocessLoadedFile(loader: object, model: Model) {
352
+
353
+
354
+ // assign animations of loaded glTF to all scenes
355
+ if ("scenes" in model) {
356
+ for (const scene of model.scenes) {
357
+ if (scene && !scene.animations?.length) {
358
+ for (const anim of model.animations) {
359
+ if (!scene.animations.includes(anim)) {
360
+ scene.animations.push(anim);
361
+ }
362
+ }
363
+ }
364
+ }
365
+ }
361
366
 
362
367
  if (loader instanceof FBXLoader || loader instanceof OBJLoader) {
363
368
 
364
- let obj: Object3D | Model = result;
369
+ let obj: Object3D | Model = model;
365
370
  if (!(obj instanceof Object3D)) {
366
- obj = (result as GLTF).scene;
371
+ obj = (model as GLTF).scene || model.scenes.find(s => s);
367
372
  }
368
373
 
369
374
  obj.traverse((child) => {
@@ -60,6 +60,8 @@ type SkyboxAttributes = {
60
60
  "background-color"?: string,
61
61
  /** URL to .exr, .hdr, .png, .jpg to be used for lighting */
62
62
  "environment-image"?: string,
63
+
64
+ "environment-intensity"?: number,
63
65
  }
64
66
 
65
67
  export type TonemappingAttributeOptions = "none" | "linear" | "neutral" | "agx";
@@ -45,6 +45,7 @@ const observedAttributes = [
45
45
  "tone-mapping-exposure",
46
46
  "background-blurriness",
47
47
  "background-color",
48
+ "environment-intensity",
48
49
  ]
49
50
 
50
51
  // https://developers.google.com/web/fundamentals/web-components/customelements
@@ -54,8 +55,8 @@ const observedAttributes = [
54
55
  * The needle engine web component creates and manages a needle engine context which is responsible for rendering a 3D scene using threejs.
55
56
  * The needle engine context is created when the src attribute is set and disposed when the needle engine is removed from the document (you can prevent this by setting the keep-alive attribute to true).
56
57
  * The needle engine context is accessible via the context property on the needle engine element (e.g. document.querySelector("needle-engine").context).
57
- * @link https://engine.needle.tools/docs/reference/needle-engine-attributes
58
- *
58
+ * See {@link https://engine.needle.tools/docs/reference/needle-engine-attributes}
59
+ *
59
60
  * @example
60
61
  * <needle-engine src="https://example.com/scene.glb"></needle-engine>
61
62
  * @example
@@ -64,7 +65,7 @@ const observedAttributes = [
64
65
  export class NeedleEngineWebComponent extends HTMLElement implements INeedleEngineComponent {
65
66
 
66
67
  static get observedAttributes() {
67
- return observedAttributes
68
+ return observedAttributes;
68
69
  }
69
70
 
70
71
  public get loadingProgress01(): number { return this._loadingProgress01; }
@@ -94,7 +95,7 @@ export class NeedleEngineWebComponent extends HTMLElement implements INeedleEngi
94
95
  /**
95
96
  * Get the current context for this web component instance. The context is created when the src attribute is set and the loading has finished.
96
97
  * The context is disposed when the needle engine is removed from the document (you can prevent this by setting the keep-alive attribute to true).
97
- * @returns {Promise<Context>} a promise that resolves to the context when the loading has finished
98
+ * @returns a promise that resolves to the context when the loading has finished
98
99
  */
99
100
  public getContext(): Promise<Context> {
100
101
  return new Promise((res, _rej) => {
@@ -325,22 +326,14 @@ export class NeedleEngineWebComponent extends HTMLElement implements INeedleEngi
325
326
  if (debug) console.log("ktx2DecoderPath", newValue);
326
327
  setKtx2TranscoderPath(newValue);
327
328
  break;
328
- case "tone-mapping": {
329
- this.applyAttributes();
330
- break;
331
- }
332
- case "tone-mapping-exposure": {
333
- this.applyAttributes();
334
- break;
335
- }
336
- case "background-blurriness": {
337
- const value = parseFloat(newValue);
338
- if (value != undefined && this._context) {
339
- this._context.scene.backgroundBlurriness = value;
340
- }
341
- break;
342
- }
343
- case "background-color": {
329
+
330
+ case "tonemapping":
331
+ case "tone-mapping":
332
+ case "tone-mapping-exposure":
333
+ case "background-blurriness":
334
+ case "background-color":
335
+ case "environment-intensity":
336
+ {
344
337
  this.applyAttributes();
345
338
  break;
346
339
  }
@@ -552,11 +545,18 @@ export class NeedleEngineWebComponent extends HTMLElement implements INeedleEngi
552
545
  const backgroundBlurriness = this.getAttribute("background-blurriness");
553
546
  if (backgroundBlurriness !== null && backgroundBlurriness !== undefined) {
554
547
  const value = parseFloat(backgroundBlurriness);
555
- if (value !== undefined && this._context) {
548
+ if (!isNaN(value) && this._context) {
556
549
  this._context.scene.backgroundBlurriness = value;
557
550
  }
558
551
  }
559
552
 
553
+ const environmentIntensity = this.getAttribute("environment-intensity");
554
+ if (environmentIntensity != undefined && this._context) {
555
+ const value = parseFloat(environmentIntensity);
556
+ if (!isNaN(value) && this._context)
557
+ this._context.scene.environmentIntensity = value;
558
+ }
559
+
560
560
  const backgroundColor = this.getAttribute("background-color");
561
561
  if (this._context?.renderer) {
562
562
  if (typeof backgroundColor === "string" && backgroundColor.length > 0) {
@@ -67,7 +67,7 @@ export class Animation extends Behaviour implements IAnimationComponent {
67
67
  get isAnimationComponent(): boolean { return true; }
68
68
  addClip(clip: AnimationClip) {
69
69
  if (!this.animations) this.animations = [];
70
- this.animations.push(clip);
70
+ if (!this.animations.includes(clip)) this.animations.push(clip);
71
71
  }
72
72
 
73
73
  /**
@@ -1,7 +1,6 @@
1
1
  import type { GLTF } from "three/examples/jsm/loaders/GLTFLoader.js";
2
2
 
3
3
  import { AnimationUtils } from "../engine/engine_animation.js";
4
- import { addComponent } from "../engine/engine_components.js";
5
4
  import { ContextEvent, ContextRegistry } from "../engine/engine_context_registry.js";
6
5
  import { Animation } from "./Animation.js";
7
6
  import { Animator } from "./Animator.js";
@@ -29,11 +28,7 @@ ContextRegistry.registerCallback(ContextEvent.ContextCreated, args => {
29
28
  return undefined;
30
29
  }, true);
31
30
  if (hasAnimation !== true) {
32
- AnimationUtils.assignAnimationsFromFile(file.file as GLTF, {
33
- createAnimationComponent: (obj, _clip) => {
34
- return addComponent(obj, Animation);
35
- },
36
- });
31
+ AnimationUtils.autoplayAnimations(file.file as GLTF);
37
32
  }
38
33
  }
39
34
  }
@@ -10,6 +10,7 @@ import { getTempColor, getWorldPosition } from "../engine/engine_three_utils.js"
10
10
  import type { ICamera } from "../engine/engine_types.js"
11
11
  import { getParam } from "../engine/engine_utils.js";
12
12
  import { RGBAColor } from "../engine/js-extensions/index.js";
13
+ import { NeedleXREventArgs } from "../engine/engine_xr.js";
13
14
  import { Behaviour, GameObject } from "./Component.js";
14
15
  import { OrbitControls } from "./OrbitControls.js";
15
16
 
@@ -446,6 +447,11 @@ export class Camera extends Behaviour implements ICamera {
446
447
  this.context.removeCamera(this);
447
448
  }
448
449
 
450
+ onLeaveXR(_args: NeedleXREventArgs): void {
451
+ // Restore previous FOV
452
+ this.fieldOfView = this._fov;
453
+ }
454
+
449
455
 
450
456
  /** @internal */
451
457
  onBeforeRender() {
@@ -553,7 +559,7 @@ export class Camera extends Behaviour implements ICamera {
553
559
  }
554
560
 
555
561
  // restore previous fov (e.g. when user was in VR or AR and the camera's fov has changed)
556
- this.fieldOfView = this._fov;
562
+ this.fieldOfView = this.fieldOfView;
557
563
 
558
564
  if (debug) {
559
565
  const msg = `[Camera] Apply ClearFlags: ${ClearFlags[this._clearFlags]} - \"${this.name}\"`;
@@ -143,13 +143,6 @@ const blobKeyName = "blob";
143
143
  */
144
144
  export class DropListener extends Behaviour {
145
145
 
146
- /**
147
- * When enabled, the DropListener will automatically synchronize dropped files to other connected clients.
148
- * When a file is dropped locally, it will be uploaded to blob storage and the URL will be shared with other clients.
149
- */
150
- @serializable()
151
- useNetworking: boolean = true;
152
-
153
146
  /**
154
147
  * When assigned, the DropListener will only accept files that are dropped on this specific object.
155
148
  * This allows creating designated drop zones in your scene.
@@ -159,7 +152,10 @@ export class DropListener extends Behaviour {
159
152
 
160
153
  /**
161
154
  * When enabled, dropped objects will be automatically scaled to fit within the volume defined by fitVolumeSize.
162
- * Useful for ensuring dropped models appear at an appropriate scale.
155
+ * Useful for ensuring dropped models appear at an appropriate scale.
156
+ *
157
+ * **Tip**: Use the handy `fitObjectIntoVolume` function (`import { fitObjectIntoVolume } from "@needle-tools/engine"`) for custom fitting needs.
158
+ *
163
159
  * @default false
164
160
  */
165
161
  @serializable()
@@ -180,6 +176,14 @@ export class DropListener extends Behaviour {
180
176
  @serializable()
181
177
  placeAtHitPosition: boolean = true;
182
178
 
179
+ /**
180
+ * When enabled, the DropListener will automatically synchronize dropped files to other connected clients.
181
+ * When a file is dropped locally, it will be uploaded to blob storage and the URL will be shared with other clients.
182
+ * @default false
183
+ */
184
+ @serializable()
185
+ useNetworking: boolean = false;
186
+
183
187
  /**
184
188
  * Event list that gets invoked after a file has been successfully added to the scene.
185
189
  * Receives {@link DropListenerOnDropArguments} containing the added object and related information.
@@ -193,6 +197,27 @@ export class DropListener extends Behaviour {
193
197
  @serializable(EventList)
194
198
  onDropped: EventList<DropListenerOnDropArguments> = new EventList();
195
199
 
200
+ /**
201
+ * Loads a file from the given URL and adds it to the scene.
202
+ * @returns A promise that resolves to the loaded object or null if loading failed.
203
+ */
204
+ loadFromURL(url: string, data?: { point?: Vec3, size?: Vec3 }): Promise<Object3D | null> {
205
+ return this.addFromUrl(url, { screenposition: new Vector2(), point: data?.point, size: data?.size, }, false);
206
+ }
207
+
208
+ /**
209
+ * Forgets all previously added objects.
210
+ * The droplistener will then not be able to remove previously added objects.
211
+ */
212
+ forgetObjects() {
213
+ this.removePreviouslyAddedObjects(false);
214
+ }
215
+
216
+
217
+
218
+
219
+ // #region internals
220
+
196
221
  /** @internal */
197
222
  onEnable(): void {
198
223
  this.context.renderer.domElement.addEventListener("dragover", this.onDrag);
@@ -208,21 +233,6 @@ export class DropListener extends Behaviour {
208
233
  this.context.connection.stopListen("droplistener", this.onNetworkEvent);
209
234
  }
210
235
 
211
- /**
212
- * Loads a file from the given URL and adds it to the scene.
213
- */
214
- loadFromURL(url: string, data?: { point?: Vec3, size?: Vec3 }) {
215
- this.addFromUrl(url, { screenposition: new Vector2(), point: data?.point, size: data?.size, }, true);
216
- }
217
-
218
- /**
219
- * Forgets all previously added objects.
220
- * The droplistener will then not be able to remove previously added objects.
221
- */
222
- forgetObjects() {
223
- this.removePreviouslyAddedObjects(false);
224
- }
225
-
226
236
  /**
227
237
  * Handles network events received from other clients containing information about dropped objects
228
238
  * @param evt Network event data containing object information, position, and content URL
@@ -326,7 +336,7 @@ export class DropListener extends Behaviour {
326
336
  }
327
337
  }
328
338
  if (files.length > 0) {
329
- await this.addDroppedFiles(files, ctx);
339
+ await this.addFromFiles(files, ctx);
330
340
  }
331
341
  }
332
342
 
@@ -359,6 +369,7 @@ export class DropListener extends Behaviour {
359
369
  // Ignore dropped images
360
370
  const lowercaseUrl = url.toLowerCase();
361
371
  if (lowercaseUrl.endsWith(".hdr") || lowercaseUrl.endsWith(".hdri") || lowercaseUrl.endsWith(".exr") || lowercaseUrl.endsWith(".png") || lowercaseUrl.endsWith(".jpg") || lowercaseUrl.endsWith(".jpeg")) {
372
+ console.warn(`Fileformat is not supported: ${lowercaseUrl}`);
362
373
  return null;
363
374
  }
364
375
 
@@ -374,7 +385,7 @@ export class DropListener extends Behaviour {
374
385
  });
375
386
  if (res && this._addedObjects.length <= 0) {
376
387
  ctx.url = url;
377
- const obj = this.addObject(res, ctx, isRemote);
388
+ const obj = this.onObjectLoaded(res, ctx, isRemote);
378
389
  return obj;
379
390
  }
380
391
  }
@@ -388,12 +399,13 @@ export class DropListener extends Behaviour {
388
399
  private _abort: AbortController | null = null;
389
400
 
390
401
  /**
391
- * Processes dropped files, loads them as 3D models, and handles networking if enabled
392
- * Creates an abort controller to cancel previous uploads if new files are dropped
402
+ * Processes dropped files and loads them as 3D models.
403
+ * When enabled, it also handles network drops (sending files between clients).
404
+ * Automatically handles cancelling previous uploads if new files are dropped.
393
405
  * @param fileList Array of dropped files
394
- * @param ctx Context information about where the drop occurred
406
+ * @param ctx Context information about where on the screen or in 3D space the drop occurred
395
407
  */
396
- private async addDroppedFiles(fileList: Array<File>, ctx: DropContext) {
408
+ private async addFromFiles(fileList: Array<File>, ctx: DropContext) {
397
409
  if (debug) console.log("Add files", fileList)
398
410
  if (!Array.isArray(fileList)) return;
399
411
  if (!fileList.length) return;
@@ -427,7 +439,7 @@ export class DropListener extends Behaviour {
427
439
  if (res) {
428
440
  this.dispatchEvent(new CustomEvent(DropListenerEvents.FileDropped, { detail: file }));
429
441
  ctx.file = file;
430
- const obj = this.addObject(res, ctx, false);
442
+ const obj = this.onObjectLoaded(res, ctx, false);
431
443
 
432
444
  // handle uploading the dropped object and networking the event
433
445
  if (obj && this.context.connection.isConnected && this.useNetworking) {
@@ -478,7 +490,7 @@ export class DropListener extends Behaviour {
478
490
  * @param isRemote Whether this object was shared from a remote client
479
491
  * @returns The added object or null if adding failed
480
492
  */
481
- private addObject(data: { model: Model, contentMD5: string }, ctx: DropContext, isRemote: boolean): Object3D | null {
493
+ private onObjectLoaded(data: { model: Model, contentMD5: string }, ctx: DropContext, isRemote: boolean): Object3D | null {
482
494
 
483
495
  const { model, contentMD5 } = data;
484
496
 
@@ -522,9 +534,7 @@ export class DropListener extends Behaviour {
522
534
  }
523
535
  }
524
536
 
525
- AnimationUtils.assignAnimationsFromFile(model, {
526
- createAnimationComponent: obj => addComponent(obj, Animation)
527
- });
537
+ AnimationUtils.autoplayAnimations(model);
528
538
 
529
539
  const evt = new DropListenerAddedEvent({
530
540
  sender: this,
@@ -1,4 +1,4 @@
1
- import { Object3D } from "three";
1
+ import { Object3D, Vector3 } from "three";
2
2
 
3
3
  import { serializable } from "../engine/engine_serialization_decorator.js";
4
4
  import { Behaviour } from "./Component.js";
@@ -23,4 +23,12 @@ export class LookAtConstraint extends Behaviour {
23
23
  */
24
24
  @serializable(Object3D)
25
25
  sources: Object3D[] = [];
26
+
27
+ /**
28
+ * Set the position of the constraint.
29
+ */
30
+ setConstraintPosition(worldPosition: Vector3) {
31
+ const source = this.sources[0];
32
+ if (source) source.worldPosition = worldPosition;
33
+ }
26
34
  }