@needle-tools/engine 4.11.0-next.8bfb2f5 → 4.11.0-next.c7baa24

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 (106) hide show
  1. package/CHANGELOG.md +7 -1
  2. package/README.md +3 -1
  3. package/components.needle.json +1 -1
  4. package/dist/{gltf-progressive-CXVECA3a.js → gltf-progressive-BvlZQAkt.js} +3 -3
  5. package/dist/{gltf-progressive-D4Z_Khp3.min.js → gltf-progressive-CftVUJy3.min.js} +1 -1
  6. package/dist/{gltf-progressive-CHeORqEv.umd.cjs → gltf-progressive-GwdQV1Qx.umd.cjs} +1 -1
  7. package/dist/{needle-engine.bundle-8EmDJd2O.umd.cjs → needle-engine.bundle-DBDKvYj5.umd.cjs} +142 -142
  8. package/dist/{needle-engine.bundle-DtF-fcok.min.js → needle-engine.bundle-qNZSuWhD.min.js} +144 -144
  9. package/dist/{needle-engine.bundle-lul9TfB2.js → needle-engine.bundle-tE15q9uM.js} +6264 -6202
  10. package/dist/needle-engine.d.ts +6 -0
  11. package/dist/needle-engine.js +4 -4
  12. package/dist/needle-engine.min.js +1 -1
  13. package/dist/needle-engine.umd.cjs +1 -1
  14. package/dist/{postprocessing-DQ2pynXW.js → postprocessing-CJC0Npcd.js} +2 -2
  15. package/dist/{postprocessing-BsnRNRRS.umd.cjs → postprocessing-DrM4PWU3.umd.cjs} +1 -1
  16. package/dist/{postprocessing-BHMVuZQ1.min.js → postprocessing-l7zsdO_Q.min.js} +1 -1
  17. package/dist/{three-qw28ZtTy.min.js → three-BDW9I486.min.js} +13 -13
  18. package/dist/{three-CJSAehtG.js → three-MHVqtJYj.js} +1 -0
  19. package/dist/{three-examples-Doq0rvFU.js → three-examples-C5Ht-QFN.js} +1 -1
  20. package/dist/{three-examples-Deqc1bNw.umd.cjs → three-examples-CgwGHSgz.umd.cjs} +1 -1
  21. package/dist/{three-examples-BivkhnvN.min.js → three-examples-fvEPSC8L.min.js} +1 -1
  22. package/dist/{three-B-jwTHao.umd.cjs → three-iFaDq9U3.umd.cjs} +13 -13
  23. package/dist/{three-mesh-ui-CktOi6oI.js → three-mesh-ui-BjWTTk1R.js} +1 -1
  24. package/dist/{three-mesh-ui-CsHwj9cJ.umd.cjs → three-mesh-ui-Bm32sS2a.umd.cjs} +1 -1
  25. package/dist/{three-mesh-ui-DhYXcXZe.min.js → three-mesh-ui-CLdkp21K.min.js} +1 -1
  26. package/dist/{vendor-BcsPRUmt.umd.cjs → vendor-CAWj5cBK.umd.cjs} +2 -2
  27. package/dist/{vendor-CyfN5nor.js → vendor-DJBpoQcM.js} +608 -599
  28. package/dist/{vendor-DyavoogU.min.js → vendor-DWGd3dEf.min.js} +20 -20
  29. package/lib/engine/js-extensions/Object3D.d.ts +6 -0
  30. package/lib/engine/js-extensions/Object3D.js +15 -0
  31. package/lib/engine/js-extensions/Object3D.js.map +1 -1
  32. package/lib/engine-components/Collider.d.ts +26 -0
  33. package/lib/engine-components/Collider.js +26 -0
  34. package/lib/engine-components/Collider.js.map +1 -1
  35. package/lib/engine-components/ContactShadows.d.ts +11 -2
  36. package/lib/engine-components/ContactShadows.js +11 -2
  37. package/lib/engine-components/ContactShadows.js.map +1 -1
  38. package/lib/engine-components/DropListener.d.ts +3 -0
  39. package/lib/engine-components/DropListener.js +44 -21
  40. package/lib/engine-components/DropListener.js.map +1 -1
  41. package/lib/engine-components/Duplicatable.d.ts +2 -2
  42. package/lib/engine-components/Duplicatable.js +2 -2
  43. package/lib/engine-components/EventList.d.ts +18 -1
  44. package/lib/engine-components/EventList.js +18 -1
  45. package/lib/engine-components/EventList.js.map +1 -1
  46. package/lib/engine-components/GroundProjection.d.ts +3 -0
  47. package/lib/engine-components/GroundProjection.js +3 -0
  48. package/lib/engine-components/GroundProjection.js.map +1 -1
  49. package/lib/engine-components/Interactable.d.ts +4 -0
  50. package/lib/engine-components/Interactable.js +4 -0
  51. package/lib/engine-components/Interactable.js.map +1 -1
  52. package/lib/engine-components/OrbitControls.d.ts +4 -2
  53. package/lib/engine-components/OrbitControls.js +31 -3
  54. package/lib/engine-components/OrbitControls.js.map +1 -1
  55. package/lib/engine-components/RigidBody.d.ts +5 -0
  56. package/lib/engine-components/RigidBody.js +5 -0
  57. package/lib/engine-components/RigidBody.js.map +1 -1
  58. package/lib/engine-components/SeeThrough.js +20 -0
  59. package/lib/engine-components/SeeThrough.js.map +1 -1
  60. package/lib/engine-components/export/usdz/ThreeUSDZExporter.d.ts +4 -2
  61. package/lib/engine-components/export/usdz/ThreeUSDZExporter.js +69 -14
  62. package/lib/engine-components/export/usdz/ThreeUSDZExporter.js.map +1 -1
  63. package/lib/engine-components/ui/Text.js +6 -1
  64. package/lib/engine-components/ui/Text.js.map +1 -1
  65. package/lib/engine-components/utils/LookAt.d.ts +3 -0
  66. package/lib/engine-components/utils/LookAt.js +3 -0
  67. package/lib/engine-components/utils/LookAt.js.map +1 -1
  68. package/lib/engine-components/utils/OpenURL.d.ts +2 -1
  69. package/lib/engine-components/utils/OpenURL.js +2 -1
  70. package/lib/engine-components/utils/OpenURL.js.map +1 -1
  71. package/lib/engine-components/web/ScrollFollow.js +1 -2
  72. package/lib/engine-components/web/ScrollFollow.js.map +1 -1
  73. package/lib/engine-components/webxr/WebARCameraBackground.d.ts +2 -0
  74. package/lib/engine-components/webxr/WebARCameraBackground.js +2 -0
  75. package/lib/engine-components/webxr/WebARCameraBackground.js.map +1 -1
  76. package/lib/engine-components/webxr/WebARSessionRoot.d.ts +1 -1
  77. package/lib/engine-components/webxr/WebARSessionRoot.js +1 -1
  78. package/lib/engine-components/webxr/WebXR.d.ts +2 -0
  79. package/lib/engine-components/webxr/WebXR.js +2 -0
  80. package/lib/engine-components/webxr/WebXR.js.map +1 -1
  81. package/lib/engine-components/webxr/WebXRImageTracking.d.ts +29 -0
  82. package/lib/engine-components/webxr/WebXRImageTracking.js +29 -0
  83. package/lib/engine-components/webxr/WebXRImageTracking.js.map +1 -1
  84. package/package.json +2 -2
  85. package/plugins/vite/index.js +4 -1
  86. package/plugins/vite/needle-app.js +103 -57
  87. package/src/engine/js-extensions/Object3D.ts +24 -0
  88. package/src/engine-components/Collider.ts +27 -1
  89. package/src/engine-components/ContactShadows.ts +12 -4
  90. package/src/engine-components/DropListener.ts +45 -24
  91. package/src/engine-components/Duplicatable.ts +2 -2
  92. package/src/engine-components/EventList.ts +18 -1
  93. package/src/engine-components/GroundProjection.ts +4 -1
  94. package/src/engine-components/Interactable.ts +4 -1
  95. package/src/engine-components/OrbitControls.ts +27 -3
  96. package/src/engine-components/RigidBody.ts +6 -1
  97. package/src/engine-components/SeeThrough.ts +42 -2
  98. package/src/engine-components/export/usdz/ThreeUSDZExporter.ts +117 -17
  99. package/src/engine-components/ui/Text.ts +11 -2
  100. package/src/engine-components/utils/LookAt.ts +3 -0
  101. package/src/engine-components/utils/OpenURL.ts +3 -2
  102. package/src/engine-components/web/ScrollFollow.ts +1 -1
  103. package/src/engine-components/webxr/WebARCameraBackground.ts +2 -0
  104. package/src/engine-components/webxr/WebARSessionRoot.ts +1 -1
  105. package/src/engine-components/webxr/WebXR.ts +2 -0
  106. package/src/engine-components/webxr/WebXRImageTracking.ts +30 -3
@@ -45,8 +45,13 @@ export const needleApp = (command, config, userSettings) => {
45
45
  // refs: context.chunk.referencedFiles,
46
46
  // });
47
47
 
48
- const referencedGlbs = tryParseNeedleEngineSrcAttributeFromHtml(html);
49
- const webComponent = generateNeedleEmbedWebComponent(path, referencedGlbs);
48
+ // TODO: here we try to find the main asset (entrypoint) for this web app. It's a bit hacky right now but we have to assign this url directly to make it work with `gen.js` where multiple needle-apps are on different (or the same) pages.
49
+ const attribute_src = tryParseNeedleEngineSrcAttributeFromHtml(html);
50
+ const imported_glbs = Array.from(context.chunk.viteMetadata?.importedAssets?.values() || [])?.filter(f => f.endsWith(".glb") || f.endsWith(".gltf"));
51
+
52
+ const main_asset = attribute_src?.[0] || imported_glbs?.[0];
53
+
54
+ const webComponent = generateNeedleEmbedWebComponent(path, main_asset);
50
55
  await writeFile(`${outputDir}/needle-app.js`, webComponent, (err) => {
51
56
  if (err) {
52
57
  console.error("[needle-app] could not create needle-app.js", err);
@@ -71,78 +76,119 @@ export const needleApp = (command, config, userSettings) => {
71
76
 
72
77
  /**
73
78
  * @param {string} filepath
74
- * @param {string[]} needleEngineSrcPaths
79
+ * @param {string | null} src
75
80
  * @returns {string}
76
81
  */
77
- function generateNeedleEmbedWebComponent(filepath, needleEngineSrcPaths) {
82
+ function generateNeedleEmbedWebComponent(filepath, src) {
78
83
 
79
84
 
80
85
  // filepath is e.g. `assets/index-XXXXXXXX.js`
81
86
  // we want to make sure the path is correct relative to where the component will be used
82
87
  // this script will be emitted in the output directory root (e.g. needle-embed.js)
83
88
 
84
- const src = needleEngineSrcPaths?.length ? `${needleEngineSrcPaths[0]}` : "";
85
-
86
- const componentName = 'needle-app';
87
- const className = 'NeedleApp';
89
+ const componentDefaultName = 'needle-app';
88
90
 
89
91
  return `
90
- class ${className} extends HTMLElement {
91
- constructor() {
92
- super();
93
- this.attachShadow({ mode: 'open' });
94
- const template = document.createElement('template');
95
- template.innerHTML = \`
96
- <style>
97
- :host {
98
- position: relative;
99
- display: block;
100
- width: 100%;
101
- height: 100%;
102
- margin: 0;
103
- padding: 0;
104
- }
105
- needle-engine {
106
- position: absolute;
107
- top: 0;
108
- left: 0;
109
- width: 100%;
110
- height: 100%;
111
- }
112
- </style>
113
- \`;
114
- this.shadowRoot.appendChild(template.content.cloneNode(true));
115
-
116
- const script = document.createElement('script');
117
- script.type = 'module';
118
- const url = new URL('.', import.meta.url);
119
- let basePath = this.getAttribute('base-path') || \`\${url.protocol}//\${url.host}\${url.pathname}\`;
120
- while(basePath.endsWith('/')) {
121
- basePath = basePath.slice(0, -1);
92
+
93
+ // Needle Engine attributes we want to allow to be overriden
94
+ const knownAttributes = [
95
+ "src",
96
+ "background-color",
97
+ "background-image",
98
+ "environment-image",
99
+ "focus-rect",
100
+ ];
101
+
102
+ const scriptUrl = new URL(import.meta.url);
103
+ const componentName = scriptUrl.searchParams.get("component-name") || '${componentDefaultName}';
104
+
105
+
106
+ if (!customElements.get(componentName)) {
107
+ console.debug(\`Defining needle-app as <\${componentName}>\`);
108
+ customElements.define(componentName, class extends HTMLElement {
109
+
110
+ static get observedAttributes() {
111
+ return knownAttributes;
122
112
  }
123
- script.src = this.getAttribute('script-src') || \`\${basePath}/${filepath}\`;
124
- this.shadowRoot.appendChild(script);
125
113
 
126
- const needleEngine = document.createElement('needle-engine');
127
- needleEngine.src = this.getAttribute('src') || ${src?.length ? `\${basePath}/${needleEngineSrcPaths}` : undefined};
128
- this.shadowRoot.appendChild(needleEngine);
114
+ constructor() {
115
+ super();
116
+ this.attachShadow({ mode: 'open' });
117
+ const template = document.createElement('template');
118
+ template.innerHTML = \`
119
+ <style>
120
+ :host {
121
+ position: relative;
122
+ display: block;
123
+ width: max(360px 100%);
124
+ height: max(240px, 100%);
125
+ margin: 0;
126
+ padding: 0;
127
+ }
128
+ needle-engine {
129
+ position: absolute;
130
+ top: 0;
131
+ left: 0;
132
+ width: 100%;
133
+ height: 100%;
134
+ }
135
+ </style>
136
+ \`;
137
+ this.shadowRoot.appendChild(template.content.cloneNode(true));
138
+
139
+ const script = document.createElement('script');
140
+ script.type = 'module';
141
+ const url = new URL('.', import.meta.url);
142
+ this.basePath = this.getAttribute('base-path') || \`\${url.protocol}//\${url.host}\${url.pathname}\`;
143
+ while(this.basePath.endsWith('/')) {
144
+ this.basePath = this.basePath.slice(0, -1);
145
+ }
146
+ script.src = this.getAttribute('script-src') || \`\${this.basePath}/${filepath}\`;
147
+ this.shadowRoot.appendChild(script);
148
+
149
+ this.needleEngine = document.createElement('needle-engine');
150
+ this.updateAttributes();
151
+ this.shadowRoot.appendChild(this.needleEngine);
152
+
153
+ console.debug(this.basePath, script.src, this.needleEngine.getAttribute("src"));
154
+ }
129
155
 
130
- console.debug(basePath, script.src, needleEngine.getAttribute("src"));
131
- }
156
+ onConnectedCallback() {
157
+ console.debug('NeedleEmbed connected to the DOM');
158
+ }
132
159
 
133
- onConnectedCallback() {
134
- console.debug('NeedleEmbed connected to the DOM');
135
- }
160
+ disconnectedCallback() {
161
+ console.debug('NeedleEmbed disconnected from the DOM');
162
+ }
163
+
164
+ attributeChangedCallback(name, oldValue, newValue) {
165
+ console.debug(\`NeedleApp attribute changed: \${name} from \${oldValue} to \${newValue}\`);
166
+ this.updateAttributes();
167
+ }
136
168
 
137
- disconnectedCallback() {
138
- console.debug('NeedleEmbed disconnected from the DOM');
139
- }
140
- }
169
+ updateAttributes() {
170
+ console.debug("NeedleApp updating attributes");
171
+
172
+ const src = this.getAttribute('src') || ${src?.length ? `\`\${this.basePath}/${src}\`` : null};
173
+ if(src) this.needleEngine.setAttribute("src", src);
174
+ else this.needleEngine.removeAttribute("src");
141
175
 
176
+ for(const attr of knownAttributes) {
177
+
178
+ if(attr === "src") continue; // already handled above
142
179
 
143
- if (!customElements.get('${componentName}')) {
144
- console.debug("Defining ${componentName}");
145
- customElements.define('${componentName}', ${className});
180
+ if(this.hasAttribute(attr)) {
181
+ this.needleEngine.setAttribute(attr, this.getAttribute(attr));
182
+ }
183
+ else {
184
+ this.needleEngine.removeAttribute(attr);
185
+ }
186
+ }
187
+ }
188
+ });
189
+ }
190
+ else {
191
+ console.warn(\`needle-app <\${componentName}> already defined.\`);
146
192
  }
147
193
  `
148
194
  }
@@ -157,6 +157,14 @@ declare module 'three' {
157
157
  * Added by Needle Engine.
158
158
  */
159
159
  get worldUp(): Vector3;
160
+
161
+
162
+ /**
163
+ * Check if the given object is contained in the hierarchy of this object or if it's the same object.
164
+ * @param object The object to check.
165
+ * @returns True if the object is contained in the hierarchy, false otherwise.
166
+ */
167
+ contains(object: Object3D | null | undefined): boolean;
160
168
  }
161
169
  }
162
170
 
@@ -356,5 +364,21 @@ if (!Object.getOwnPropertyDescriptor(Object3D.prototype, "worldUp")) {
356
364
 
357
365
 
358
366
 
367
+ if (!Object.getOwnPropertyDescriptor(Object3D.prototype, "contains")) {
368
+ Object.defineProperty(Object3D.prototype, "contains", {
369
+ value: function (object: Object3D | null | undefined): boolean {
370
+ if (!object) return false;
371
+ if (this === object) return true;
372
+ for (const child of this.children) {
373
+ if (child.contains(object)) return true;
374
+ }
375
+ return false;
376
+ }
377
+ });
378
+ }
379
+
380
+
381
+
382
+
359
383
  // do this after adding the component extensions
360
384
  registerPrototypeExtensions(Object3D);
@@ -16,7 +16,13 @@ import { Rigidbody } from "./RigidBody.js";
16
16
  /**
17
17
  * Collider is the base class for all colliders. A collider is a physical shape that is used to detect collisions with other objects in the scene.
18
18
  * Colliders are used in combination with a {@link Rigidbody} to create physical interactions between objects.
19
- * Colliders are registered with the physics engine when they are enabled and removed when they are disabled.
19
+ * Colliders are registered with the physics engine when they are enabled and removed when they are disabled.
20
+ *
21
+ * - Example: https://samples.needle.tools/physics-basic
22
+ * - Example: https://samples.needle.tools/physics-playground
23
+ * - Example: https://samples.needle.tools/physics-&-animation
24
+ *
25
+ *
20
26
  * @category Physics
21
27
  * @group Components
22
28
  */
@@ -112,6 +118,11 @@ export class Collider extends Behaviour implements ICollider {
112
118
  /**
113
119
  * SphereCollider represents a sphere-shaped collision volume.
114
120
  * Useful for objects that are roughly spherical in shape or need a simple collision boundary.
121
+ *
122
+ * - Example: https://samples.needle.tools/physics-basic
123
+ * - Example: https://samples.needle.tools/physics-playground
124
+ * - Example: https://samples.needle.tools/physics-&-animation
125
+ *
115
126
  * @category Physics
116
127
  * @group Components
117
128
  */
@@ -158,6 +169,11 @@ export class SphereCollider extends Collider implements ISphereCollider {
158
169
  /**
159
170
  * BoxCollider represents a box-shaped collision volume.
160
171
  * Ideal for rectangular objects or objects that need a simple cuboid collision boundary.
172
+ *
173
+ * - Example: https://samples.needle.tools/physics-basic
174
+ * - Example: https://samples.needle.tools/physics-playground
175
+ * - Example: https://samples.needle.tools/physics-&-animation
176
+ *
161
177
  * @category Physics
162
178
  * @group Components
163
179
  */
@@ -260,6 +276,11 @@ export class BoxCollider extends Collider implements IBoxCollider {
260
276
  /**
261
277
  * MeshCollider creates a collision shape from a mesh geometry.
262
278
  * Allows for complex collision shapes that match the exact geometry of an object.
279
+ *
280
+ * - Example: https://samples.needle.tools/physics-basic
281
+ * - Example: https://samples.needle.tools/physics-playground
282
+ * - Example: https://samples.needle.tools/physics-&-animation
283
+ *
263
284
  * @category Physics
264
285
  * @group Components
265
286
  */
@@ -343,6 +364,11 @@ export class MeshCollider extends Collider {
343
364
  /**
344
365
  * CapsuleCollider represents a capsule-shaped collision volume (cylinder with hemispherical ends).
345
366
  * Ideal for character controllers and objects that need a rounded collision shape.
367
+ *
368
+ * - Example: https://samples.needle.tools/physics-basic
369
+ * - Example: https://samples.needle.tools/physics-playground
370
+ * - Example: https://samples.needle.tools/physics-&-animation
371
+ *
346
372
  * @category Physics
347
373
  * @group Components
348
374
  */
@@ -44,10 +44,19 @@ type FitParameters = {
44
44
  // - node can simply be scaled in Y to adjust max. ground height
45
45
 
46
46
  /**
47
- * ContactShadows is a component that allows to display contact shadows in the scene.
47
+ * ContactShadows is a component that allows to display contact shadows in the scene. It has options for darkness, opacity and blur of the shadows.
48
+ *
49
+ * - Example: https://samples.needle.tools/contact-shadows
50
+ *
51
+ * ## Usage
52
+ * You can use {@link ContactShadows.auto} to automatically create a ContactShadows instance for the scene or you can add the component to an object in the scene. Note that the scale of the object will be used to define the size of the shadow area.
53
+ *
54
+ * ContactShadows can also be enabled on the `<needle-engine>` web component directly by adding the `contactshadows` attribute. The value of the attribute will be used as opacity and darkness of the shadows: `<needle-engine contactshadows="0.7">`.
55
+ *
56
+ *
48
57
  * @category Rendering
49
58
  * @group Components
50
- */
59
+ */
51
60
  export class ContactShadows extends Behaviour {
52
61
 
53
62
  private static readonly _instances: Map<Context, ContactShadows> = new Map();
@@ -362,8 +371,7 @@ export class ContactShadows extends Behaviour {
362
371
  !this.depthMaterial || !this.shadowCamera ||
363
372
  !this.blurPlane || !this.shadowGroup || !this.plane ||
364
373
  !this.horizontalBlurMaterial || !this.verticalBlurMaterial) {
365
- if (debug)
366
- console.error("ContactShadows: not initialized yet");
374
+ if (debug) console.error("ContactShadows: not initialized yet");
367
375
  return;
368
376
  }
369
377
 
@@ -1,23 +1,22 @@
1
- import { AxesHelper, Box3, Cache, Object3D, Vector2, Vector3 } from "three";
1
+ import { Box3, Object3D, Vector2, Vector3 } from "three";
2
2
 
3
3
  import { isDevEnvironment } from "../engine/debug/index.js";
4
4
  import { AnimationUtils } from "../engine/engine_animation.js";
5
- import { addComponent } from "../engine/engine_components.js";
6
5
  import { Context } from "../engine/engine_context.js";
7
6
  import { destroy } from "../engine/engine_gameobject.js";
8
7
  import { Gizmos } from "../engine/engine_gizmos.js";
9
8
  import { getLoader } from "../engine/engine_gltf.js";
10
9
  import { BlobStorage } from "../engine/engine_networking_blob.js";
11
10
  import { PreviewHelper } from "../engine/engine_networking_files.js";
12
- import { generateSeed, InstantiateIdProvider } from "../engine/engine_networking_instantiate.js";
11
+ import { InstantiateIdProvider } from "../engine/engine_networking_instantiate.js";
13
12
  import { serializable } from "../engine/engine_serialization_decorator.js";
14
13
  import { fitObjectIntoVolume, getBoundingBox, placeOnSurface } from "../engine/engine_three_utils.js";
15
- import { IGameObject, Model, Vec3 } from "../engine/engine_types.js";
14
+ import { Model, Vec3 } from "../engine/engine_types.js";
16
15
  import { getParam, setParamWithoutReload } from "../engine/engine_utils.js";
17
16
  import { determineMimeTypeFromExtension } from "../engine/engine_utils_format.js";
18
- import { Animation } from "./Animation.js";
19
17
  import { Behaviour } from "./Component.js";
20
18
  import { EventList } from "./EventList.js";
19
+ import { Renderer } from "./Renderer.js";
21
20
 
22
21
  /**
23
22
  * Debug mode can be enabled with the URL parameter `?debugdroplistener`, which
@@ -116,6 +115,8 @@ const blobKeyName = "blob";
116
115
  * If {@link useNetworking} is enabled, the DropListener will automatically synchronize dropped files to other connected clients.
117
116
  * Enable {@link fitIntoVolume} to automatically scale dropped objects to fit within the volume defined by {@link fitVolumeSize}.
118
117
  *
118
+ * - Example: [DropListener Sample](https://droplistener-zubcksz1veaoo.needle.run/)
119
+ *
119
120
  * The following events are dispatched by the DropListener:
120
121
  * - **object-added** - dispatched when a new object is added to the scene
121
122
  * - **file-dropped** - dispatched when a file is dropped into the scene
@@ -214,7 +215,14 @@ export class DropListener extends Behaviour {
214
215
  }
215
216
 
216
217
 
217
-
218
+ awake() {
219
+ for (const ch of this.gameObject.children) {
220
+ if (this.dropArea && ch.contains(this.dropArea)) {
221
+ continue;
222
+ }
223
+ this._addedObjects.push(ch);
224
+ }
225
+ }
218
226
 
219
227
  // #region internals
220
228
 
@@ -223,7 +231,15 @@ export class DropListener extends Behaviour {
223
231
  this.context.renderer.domElement.addEventListener("dragover", this.onDrag);
224
232
  this.context.renderer.domElement.addEventListener("drop", this.onDrop);
225
233
  window.addEventListener("paste", this.handlePaste);
226
- this.context.connection.beginListen("droplistener", this.onNetworkEvent)
234
+ this.context.connection.beginListen("droplistener", this.onNetworkEvent);
235
+ if (isDevEnvironment()) {
236
+ if (this.dropArea) {
237
+ const anyRenderer = this.dropArea.getComponentInChildren(Renderer);
238
+ if (!anyRenderer) {
239
+ console.warn("[DropListener] The assigned DropArea does not seem to have a renderer/mesh. Drag and Drop events will not be detected.");
240
+ }
241
+ }
242
+ }
227
243
  }
228
244
  /** @internal */
229
245
  onDisable(): void {
@@ -287,6 +303,7 @@ export class DropListener extends Behaviour {
287
303
  * @param evt The drag event
288
304
  */
289
305
  private onDrag = (evt: DragEvent) => {
306
+ if(debug) console.debug("DropListener Drag", evt, this.context.connection.allowEditing);
290
307
  if (this.context.connection.allowEditing === false) return;
291
308
  // necessary to get drop event
292
309
  evt.preventDefault();
@@ -298,9 +315,9 @@ export class DropListener extends Behaviour {
298
315
  * @param evt The drop event
299
316
  */
300
317
  private onDrop = async (evt: DragEvent) => {
318
+ if (debug) console.debug("DropListener Drop", evt, this.context.connection.allowEditing);
301
319
  if (this.context.connection.allowEditing === false) return;
302
320
 
303
- if (debug) console.log(evt);
304
321
  if (!evt?.dataTransfer) return;
305
322
  // If the event is marked as handled for droplisteners then ignore it
306
323
  if (evt["droplistener:handled"]) return;
@@ -473,7 +490,7 @@ export class DropListener extends Behaviour {
473
490
  if (doDestroy) {
474
491
  for (const prev of this._addedObjects) {
475
492
  if (prev.parent === this.gameObject) {
476
- destroy(prev, true, true);
493
+ prev.destroy();
477
494
  }
478
495
  }
479
496
  }
@@ -596,24 +613,28 @@ export class DropListener extends Behaviour {
596
613
  * @returns True if the drop is valid (either no drop area is set or the drop occurred inside it)
597
614
  */
598
615
  private testIfIsInDropArea(ctx: DropContext): boolean {
616
+ const screenPoint = this.context.input.convertScreenspaceToRaycastSpace(ctx.screenposition.clone());
617
+ const hits = this.context.physics.raycast({
618
+ screenPoint,
619
+ recursive: true,
620
+ testObject: obj => {
621
+ // Ignore hits on the already added objects, they don't count as part of the dropzone
622
+ if (this._addedObjects.some(o => o.contains(obj))) return false;
623
+ return true;
624
+ }
625
+ });
626
+ if (!hits.length) {
627
+ if (isDevEnvironment()) console.log(`Dropped outside of drop area for DropListener \"${this.name}\".`);
628
+ return false;
629
+ }
630
+
631
+ const hit = hits[0];
599
632
  if (this.dropArea) {
600
- const screenPoint = this.context.input.convertScreenspaceToRaycastSpace(ctx.screenposition.clone());
601
- const hits = this.context.physics.raycast({
602
- targets: [this.dropArea],
603
- screenPoint,
604
- recursive: true,
605
- testObject: obj => {
606
- // Ignore hits on the already added objects, they don't count as part of the dropzone
607
- if (this._addedObjects.includes(obj)) return false;
608
- return true;
609
- }
610
- });
611
- if (!hits.length) {
612
- if (isDevEnvironment()) console.log(`Dropped outside of drop area for DropListener \"${this.name}\".`);
613
- return false;
633
+ if (this.dropArea.contains(hit.object)) {
634
+ return true;
614
635
  }
615
636
  }
616
- return true;
637
+ return false;
617
638
  }
618
639
 
619
640
  }
@@ -12,8 +12,8 @@ import { type IPointerEventHandler, PointerEventData } from "./ui/PointerEvents.
12
12
  import { ObjectRaycaster } from "./ui/Raycaster.js";
13
13
 
14
14
  /**
15
- * The Duplicatable component is used to duplicate a assigned {@link GameObject} when a pointer event occurs on the GameObject.
16
- * It implements the {@link IPointerEventHandler} interface and can be used to expose duplication to the user in the editor without writing code.
15
+ * The Duplicatable component is used to duplicate a assigned {@link GameObject} when a pointer event occurs on the object.
16
+ *
17
17
  * @category Interactivity
18
18
  * @group Components
19
19
  */
@@ -97,7 +97,24 @@ export class EventListEvent<TArgs extends any> extends Event { //implements Arra
97
97
 
98
98
 
99
99
  /**
100
- * The EventList is a class that can be used to create a list of event listeners that can be invoked
100
+ * The EventList is a class that can be used to create a list of event listeners that can be invoked.
101
+ *
102
+ * @example
103
+ * ```ts
104
+ * // create an event list
105
+ * const onClick = new EventList<(event: MouseEvent) => void>();
106
+ *
107
+ * // add an event listener
108
+ * onClick.addEventListener((event) => {
109
+ * console.log("Clicked!", event);
110
+ * });
111
+ *
112
+ * // invoke the event list
113
+ * onClick.invoke(new MouseEvent("click"));
114
+ * ```
115
+ *
116
+ * @category Events
117
+ * @group Utilities
101
118
  */
102
119
  export class EventList<TArgs extends any = any> implements IEventList {
103
120
 
@@ -10,7 +10,10 @@ import { Behaviour } from "./Component.js";
10
10
  const debug = getParam("debuggroundprojection");
11
11
 
12
12
  /**
13
- * GroundProjectedEnv creates a ground projection of the current environment map.
13
+ * GroundProjectedEnv creates a ground projection of the current environment map.
14
+ *
15
+ * - Example https://engine.needle.tools/samples/ground-projection
16
+ *
14
17
  * @category Rendering
15
18
  * @group Components
16
19
  */
@@ -10,6 +10,9 @@ export class UsageMarker extends Behaviour
10
10
  public usedBy: any = null;
11
11
  }
12
12
 
13
-
13
+ /**
14
+ * An empty component that can be used to mark an object as interactable.
15
+ * @group Components
16
+ */
14
17
  /** @deprecated */
15
18
  export class Interactable extends Behaviour {}
@@ -255,7 +255,12 @@ export class OrbitControls extends Behaviour implements ICameraController {
255
255
  * @default 1
256
256
  */
257
257
  @serializable()
258
- targetLerpDuration = 1;
258
+ get targetLerpDuration() { return this._lookTargetLerpDuration; }
259
+ set targetLerpDuration(v) { this._lookTargetLerpDuration = v; }
260
+ private _lookTargetLerpDuration: number = 1;
261
+
262
+ @serializable(Object3D)
263
+ targetBounds: Object3D | null = null;
259
264
 
260
265
  /**
261
266
  * Rotate the camera left (or right) by the specified angle in radians.
@@ -326,7 +331,6 @@ export class OrbitControls extends Behaviour implements ICameraController {
326
331
  private _lookTargetStartPosition: Vector3 = new Vector3();
327
332
  private _lookTargetEndPosition: Vector3 = new Vector3();
328
333
  private _lookTargetLerp01: number = 0;
329
- private _lookTargetLerpDuration: number = 0;
330
334
 
331
335
  private _cameraLerpActive: boolean = false;
332
336
  private _cameraStartPosition: Vector3 = new Vector3();
@@ -685,6 +689,26 @@ export class OrbitControls extends Behaviour implements ICameraController {
685
689
  }
686
690
  }
687
691
 
692
+ if (this.targetBounds) {
693
+ // #region target bounds
694
+ const targetVector = this._controls.target;
695
+ const boundsCenter = this.targetBounds.worldPosition;
696
+ const boundsHalfSize = getTempVector(this.targetBounds.worldScale).multiplyScalar(0.5);
697
+ const min = getTempVector(boundsCenter).sub(boundsHalfSize);
698
+ const max = getTempVector(boundsCenter).add(boundsHalfSize);
699
+ const newTarget = getTempVector(this._controls.target).clamp(min, max);
700
+ const duration = .1;
701
+ if (duration <= 0) targetVector.copy(newTarget);
702
+ else targetVector.lerp(newTarget, this.context.time.deltaTime / duration);
703
+ if (this._lookTargetLerpActive) {
704
+ if (duration <= 0) this._lookTargetEndPosition.copy(newTarget);
705
+ else this._lookTargetEndPosition.lerp(newTarget, this.context.time.deltaTime / (duration * 5));
706
+ }
707
+ if (debug) {
708
+ Gizmos.DrawWireBox(boundsCenter, boundsHalfSize.multiplyScalar(2), 0xffaa00);
709
+ }
710
+ }
711
+
688
712
 
689
713
  if (this._controls) {
690
714
  if (this.debugLog)
@@ -1000,7 +1024,7 @@ export class OrbitControls extends Behaviour implements ICameraController {
1000
1024
  */
1001
1025
  fitCamera(options?: OrbitFitCameraOptions);
1002
1026
  /** @deprecated Use fitCamera(options) */
1003
- fitCamera(objects?: Object3D | Array<Object3D>, options?: Omit<OrbitFitCameraOptions, "objects">);
1027
+ fitCamera(objects?: Object3D | Array<Object3D>, options?: Omit<OrbitFitCameraOptions, "objects">);
1004
1028
  fitCamera(objectsOrOptions?: Object3D | Array<Object3D> | OrbitFitCameraOptions, options?: OrbitFitCameraOptions): void {
1005
1029
 
1006
1030
 
@@ -134,7 +134,12 @@ class TransformWatch {
134
134
  }
135
135
 
136
136
  /**
137
- * A Rigidbody is used together with a Collider to create physical interactions between objects in the scene.
137
+ * A Rigidbody is used together with a Collider to create physical interactions between objects in the scene.
138
+ *
139
+ * - Example: https://samples.needle.tools/physics-basic
140
+ * - Example: https://samples.needle.tools/physics-playground
141
+ * - Example: https://samples.needle.tools/physics-&-animation
142
+ *
138
143
  * @category Physics
139
144
  * @group Components
140
145
  */