Needle Engine

Changes between version 3.48.3 and 3.49.0-beta
Files changed (132) hide show
  1. plugins/vite/alias.js +1 -1
  2. plugins/vite/asap.js +2 -2
  3. plugins/vite/build-pipeline.js +1 -1
  4. plugins/vite/buildinfo.js +1 -1
  5. plugins/vite/defines.js +1 -1
  6. plugins/vite/dependencies.js +1 -1
  7. plugins/vite/dependency-watcher.js +3 -2
  8. plugins/vite/drop.js +3 -3
  9. plugins/vite/facebook-instant-games.js +3 -3
  10. plugins/vite/imports-logger.js +1 -1
  11. plugins/vite/license.js +1 -1
  12. plugins/vite/meta.js +3 -3
  13. plugins/vite/peer.js +3 -3
  14. plugins/vite/poster.js +3 -3
  15. plugins/vite/pwa.js +3 -3
  16. plugins/vite/reload.js +3 -4
  17. src/engine-components/Animation.ts +2 -0
  18. src/engine-components/AnimationUtils.ts +2 -48
  19. src/engine-components/Animator.ts +1 -0
  20. src/engine-components/postprocessing/Effects/Antialiasing.ts +3 -1
  21. src/engine-components-experimental/api.ts +4 -0
  22. src/engine-components/api.ts +41 -0
  23. src/engine-schemes/api.ts +12 -0
  24. src/engine/api.ts +20 -0
  25. src/engine-components/AudioListener.ts +1 -0
  26. src/engine-components/AudioSource.ts +1 -1
  27. src/engine-components/webxr/Avatar.ts +4 -0
  28. src/engine-components/avatar/AvatarEyeLook_Rotation.ts +1 -2
  29. src/engine-components/AxesHelper.ts +1 -0
  30. src/engine-components/ui/BaseUIComponent.ts +4 -2
  31. src/engine-components/export/usdz/extensions/behavior/BehaviourComponents.ts +27 -0
  32. src/engine-components/postprocessing/Effects/BloomEffect.ts +2 -0
  33. src/engine-components/BoxHelperComponent.ts +11 -25
  34. src/engine-components/ui/Button.ts +3 -0
  35. src/engine-components/Camera.ts +16 -1
  36. src/engine-components/ui/Canvas.ts +3 -0
  37. src/engine-components/ui/CanvasGroup.ts +3 -0
  38. src/engine-components/CharacterController.ts +7 -0
  39. src/engine-components/postprocessing/Effects/ChromaticAberration.ts +3 -0
  40. src/engine-components/Collider.ts +4 -1
  41. src/engine-components/postprocessing/Effects/ColorAdjustments.ts +3 -0
  42. src/engine-components/Component.ts +10 -5
  43. src/engine-components/ContactShadows.ts +1 -0
  44. src/engine/debug/debug_spatial_console.ts +0 -1
  45. src/engine-components/DeleteBox.ts +35 -11
  46. src/engine-components/postprocessing/Effects/DepthOfField.ts +3 -0
  47. src/engine-components/DeviceFlag.ts +3 -0
  48. src/engine-components/DragControls.ts +16 -7
  49. src/engine-components/DropListener.ts +2 -0
  50. src/engine-components/Duplicatable.ts +9 -1
  51. src/engine-components/postprocessing/Effects/EffectWrapper.ts +3 -0
  52. src/engine/engine_addressables.ts +1 -3
  53. src/engine/engine_animation.ts +0 -1
  54. src/engine/engine_assetdatabase.ts +2 -0
  55. src/engine/engine_element.ts +5 -2
  56. src/engine/engine_license.ts +2 -0
  57. src/engine/engine_lods.ts +3 -3
  58. src/engine/engine_networking_auto.ts +2 -2
  59. src/engine/engine_networking.ts +3 -2
  60. src/engine/engine_physics.types.ts +3 -0
  61. src/engine/engine_scenetools.ts +2 -1
  62. src/engine/engine_three_utils.ts +11 -10
  63. src/engine/engine_utils_format.ts +1 -1
  64. src/engine/engine_utils.ts +111 -35
  65. src/engine/engine.ts +1 -1
  66. src/engine-components/ui/EventSystem.ts +3 -0
  67. src/engine-components/EventTrigger.ts +3 -2
  68. src/engine-components/Gizmos.ts +4 -1
  69. src/engine-components/export/gltf/GltfExport.ts +3 -0
  70. src/engine-components/ui/Graphic.ts +6 -0
  71. src/engine-components/GridHelper.ts +1 -0
  72. src/engine-components/GroundProjection.ts +36 -14
  73. src/engine-components/ui/Image.ts +6 -0
  74. src/engine/webcomponents/index.ts +1 -1
  75. src/engine-components/ui/InputField.ts +3 -0
  76. src/engine-components/ui/Layout.ts +9 -0
  77. src/engine-components/Light.ts +1 -0
  78. src/engine-components/LODGroup.ts +1 -0
  79. src/engine/webcomponents/logo-element.ts +1 -1
  80. src/asap/needle-asap.ts +1 -0
  81. src/engine/webcomponents/needle menu/needle-menu-spatial.ts +1 -1
  82. src/engine/webcomponents/needle menu/needle-menu.ts +31 -18
  83. src/engine-components/NeedleMenu.ts +4 -2
  84. src/engine/xr/NeedleXRController.ts +6 -2
  85. src/engine-components/Networking.ts +1 -0
  86. src/engine-components/utils/OpenURL.ts +1 -0
  87. src/engine-components/OrbitControls.ts +81 -25
  88. src/engine-components/particlesystem/ParticleSystem.ts +6 -2
  89. src/engine-components/postprocessing/Effects/Pixelation.ts +3 -0
  90. src/engine-components/timeline/PlayableDirector.ts +8 -0
  91. src/engine-components/PlayerColor.ts +1 -0
  92. src/engine-components/postprocessing/PostProcessingEffect.ts +2 -0
  93. src/engine-components/ReflectionProbe.ts +10 -4
  94. src/engine/codegen/register_types.ts +2 -2
  95. src/engine-components/Renderer.ts +3 -0
  96. src/engine-components/RendererInstancing.ts +4 -8
  97. src/engine-components/RigidBody.ts +2 -1
  98. src/engine-components/SceneSwitcher.ts +1 -0
  99. src/engine-components/ScreenCapture.ts +1 -0
  100. src/engine-components/postprocessing/Effects/ScreenspaceAmbientOcclusion.ts +1 -1
  101. src/engine-components/postprocessing/Effects/ScreenspaceAmbientOcclusionN8.ts +1 -1
  102. src/engine-components/ShadowCatcher.ts +1 -0
  103. src/engine-components/postprocessing/Effects/Sharpening.ts +3 -0
  104. src/engine-components/SmoothFollow.ts +1 -0
  105. src/engine-components/SpatialTrigger.ts +7 -0
  106. src/engine-components/SpectatorCamera.ts +4 -1
  107. src/engine-components/SpriteRenderer.ts +1 -0
  108. src/engine-components/SyncedCamera.ts +1 -0
  109. src/engine-components/SyncedRoom.ts +4 -2
  110. src/engine-components/SyncedTransform.ts +1 -0
  111. src/engine-components/ui/Text.ts +3 -0
  112. src/engine-components/export/usdz/ThreeUSDZExporter.ts +7 -5
  113. src/engine-components/postprocessing/Effects/TiltShiftEffect.ts +3 -1
  114. src/engine-components/timeline/TimelineTracks.ts +10 -1
  115. src/engine-components/postprocessing/Effects/Tonemapping.ts +3 -1
  116. src/engine-components/TransformGizmo.ts +1 -0
  117. src/engine-components/export/usdz/USDZExporter.ts +10 -3
  118. src/engine-components/VideoPlayer.ts +1 -0
  119. src/engine-components/postprocessing/Effects/Vignette.ts +3 -1
  120. src/engine-components/Voip.ts +1 -0
  121. src/engine-components/postprocessing/Volume.ts +3 -0
  122. src/engine-components/webxr/WebARCameraBackground.ts +1 -1
  123. src/engine-components/webxr/WebARSessionRoot.ts +2 -0
  124. src/engine-components/webxr/WebXR.ts +1 -0
  125. src/engine-components/webxr/WebXRImageTracking.ts +3 -0
  126. src/engine-components/webxr/WebXRPlaneTracking.ts +1 -0
  127. src/engine-components/webxr/WebXRRig.ts +1 -0
  128. src/engine-components/webxr/controllers/XRControllerFollow.ts +4 -1
  129. src/engine-components/webxr/controllers/XRControllerModel.ts +2 -2
  130. src/engine-components/webxr/controllers/XRControllerMovement.ts +1 -0
  131. src/engine-components/webxr/XRFlag.ts +4 -0
  132. src/engine-components/AnimationUtilsAutoplay.ts +43 -0
plugins/vite/alias.js CHANGED
@@ -88,7 +88,7 @@
88
88
  /** This plugin logs all imports. This helps to find cases where incorrect folders are found/resolved. */
89
89
 
90
90
  const debuggingPlugin = {
91
- name: "needle-alias-debug",
91
+ name: "needle:alias-debug",
92
92
  // needs to run before regular resolver
93
93
  enforce: 'pre',
94
94
  resolveId(id, importer, options) {
plugins/vite/asap.js CHANGED
@@ -17,8 +17,8 @@
17
17
  return {
18
18
  name: 'needle:asap',
19
19
  transformIndexHtml: {
20
- enforce: 'pre',
21
- transform(html, _ctx) {
20
+ order: 'pre',
21
+ handler(html, _ctx) {
22
22
  if (existsSync(process.cwd() + "/node_modules/@needle-tools/engine/src/asap/needle-asap.ts")) {
23
23
  return {
24
24
  html,
plugins/vite/build-pipeline.js CHANGED
@@ -44,7 +44,7 @@
44
44
  let taskFinished = false;
45
45
  let taskSucceeded = false;
46
46
  return {
47
- name: 'needle-buildpipeline',
47
+ name: 'needle:buildpipeline',
48
48
  enforce: "post",
49
49
  apply: 'build',
50
50
  buildEnd() {
plugins/vite/buildinfo.js CHANGED
@@ -13,7 +13,7 @@
13
13
  if (userSettings?.noBuildInfo) return;
14
14
 
15
15
  return {
16
- name: 'needle-buildinfo',
16
+ name: 'needle:buildinfo',
17
17
  apply: "build",
18
18
  enforce: "post",
19
19
  closeBundle: async () => {
plugins/vite/defines.js CHANGED
@@ -20,7 +20,7 @@
20
20
  if (needleEngineConfig?.useRapier === false || userSettings?.useRapier === false) useRapier = false;
21
21
 
22
22
  return {
23
- name: 'needle-defines',
23
+ name: 'needle:defines',
24
24
  enforce: 'pre',
25
25
  config(viteConfig) {
26
26
  // console.log("Update vite defines -------------------------------------------");
plugins/vite/dependencies.js CHANGED
@@ -11,7 +11,7 @@
11
11
  * @type {import('vite').Plugin}
12
12
  */
13
13
  return {
14
- name: 'needle-dependencies',
14
+ name: 'needle:dependencies',
15
15
  enforce: 'pre',
16
16
  config: (config) => {
17
17
  if (config.optimizeDeps?.include?.includes("three-mesh-bvh")) {
plugins/vite/dependency-watcher.js CHANGED
@@ -161,8 +161,9 @@
161
161
  expectedVersion = value;
162
162
  }
163
163
  if (expectedVersion?.length > 0) {
164
- const isRange = expectedVersion.includes("^") || expectedVersion.includes(">") || expectedVersion.includes("<");
165
- if (!isRange) {
164
+ const isRange = expectedVersion.includes("^") || expectedVersion.includes(">") || expectedVersion.includes("<") || expectedVersion.includes("~");
165
+ const isLatest = expectedVersion === "latest";
166
+ if (!isRange && !isLatest) {
166
167
  const packageJsonPath = path.join(depPath, "package.json");
167
168
  /** @type {String} */
168
169
  const installedVersion = JSON.parse(readFileSync(packageJsonPath, "utf8")).version;
plugins/vite/drop.js CHANGED
@@ -11,7 +11,7 @@
11
11
  if (command === "build") return;
12
12
 
13
13
  return {
14
- name: "needle-drop",
14
+ name: "needle:drop",
15
15
  config(config) {
16
16
  if(userSettings)
17
17
  if (!config.server) config.server = {};
@@ -20,8 +20,8 @@
20
20
  setTimeout(() => console.log("Update HMR port to " + config.server.hmr.port));
21
21
  },
22
22
  transformIndexHtml: {
23
- enforce: 'pre',
24
- transform(html, _) {
23
+ order: 'pre',
24
+ handler(html, _) {
25
25
  const file = path.join(__dirname, 'drop-client.js');
26
26
  return [
27
27
  {
plugins/vite/facebook-instant-games.js CHANGED
@@ -65,10 +65,10 @@
65
65
  const outputDir = getOutputDirectory();
66
66
 
67
67
  return {
68
- name: 'needle-facebook-instant-games',
68
+ name: 'needle:facebook-instant-games',
69
69
  transformIndexHtml: {
70
- enforce: 'post',
71
- transform(html, _ctx) {
70
+ order: 'post',
71
+ handler(html, _ctx) {
72
72
  // post transform so we want to linebreak after the vite logs
73
73
  console.log("\n");
74
74
 
plugins/vite/imports-logger.js CHANGED
@@ -19,7 +19,7 @@
19
19
  const logToImportsLogFile = true;
20
20
 
21
21
  return {
22
- name: 'needle-imports-logger',
22
+ name: 'needle:imports-logger',
23
23
  enforce: 'pre',
24
24
  resolveId(id, importer) {
25
25
 
plugins/vite/license.js CHANGED
@@ -11,7 +11,7 @@
11
11
  let license = undefined;
12
12
 
13
13
  return {
14
- name: "needle-license",
14
+ name: "needle:license",
15
15
  enforce: 'pre',
16
16
  async configResolved() {
17
17
  if (userSettings.license) {
plugins/vite/meta.js CHANGED
@@ -22,10 +22,10 @@
22
22
 
23
23
  return {
24
24
  // replace meta tags
25
- name: 'needle-meta',
25
+ name: 'needle:meta',
26
26
  transformIndexHtml: {
27
- enforce: 'pre',
28
- transform(html, _ctx) {
27
+ order: 'pre',
28
+ handler(html, _ctx) {
29
29
 
30
30
  if (userSettings.allowMetaPlugin === false) return [];
31
31
 
plugins/vite/peer.js CHANGED
@@ -11,10 +11,10 @@
11
11
  if (userSettings.noPeer === true) return;
12
12
 
13
13
  return {
14
- name: 'needle-peerjs',
14
+ name: 'needle:peerjs',
15
15
  transformIndexHtml: {
16
- enforce: 'pre',
17
- transform(html, _ctx) {
16
+ order: 'pre',
17
+ handler(html, _ctx) {
18
18
  return {
19
19
  html,
20
20
  tags: [
plugins/vite/poster.js CHANGED
@@ -19,7 +19,7 @@
19
19
  if (userSettings.noPoster) return;
20
20
 
21
21
  return {
22
- name: 'needle-poster',
22
+ name: 'needle:poster',
23
23
  configureServer(server) {
24
24
  server.ws.on('needle:screenshot', async (data, client) => {
25
25
  if (userSettings.noPoster) return;
@@ -55,8 +55,8 @@
55
55
  });
56
56
  },
57
57
  transformIndexHtml: {
58
- enforce: 'pre',
59
- transform(html, ctx) {
58
+ order: 'pre',
59
+ handler(html, ctx) {
60
60
  const file = path.join(__dirname, 'poster-client.js');
61
61
  let scriptContent = fs.readFileSync(file, 'utf8');
62
62
  switch (userSettings.posterFormat) {
plugins/vite/pwa.js CHANGED
@@ -79,7 +79,7 @@
79
79
  // log("PWA options", pwaOptions);
80
80
 
81
81
  return {
82
- name: 'needle-pwa',
82
+ name: 'needle:pwa',
83
83
  apply: 'build',
84
84
  enforce: "post",
85
85
  config(viteConfig) {
@@ -121,8 +121,8 @@
121
121
  }
122
122
  },
123
123
  transformIndexHtml: {
124
- enforce: 'pre',
125
- transform(html, _ctx) {
124
+ order: 'pre',
125
+ handler(html, _ctx) {
126
126
  // see https://vite-pwa-org.netlify.app/guide/auto-update.html
127
127
  // post transform so we want to linebreak after the vite logs
128
128
  console.log("\n");
plugins/vite/reload.js CHANGED
@@ -43,7 +43,7 @@
43
43
  if (projectConfig?.codegenDirectory?.length) ignorePatterns.push(`${projectConfig?.codegenDirectory}/**/*`);
44
44
 
45
45
  return {
46
- name: 'needle-reload',
46
+ name: 'needle:reload',
47
47
  config(config) {
48
48
  if (!config.server) config.server = { watch: { ignored: [] } };
49
49
  else if (!config.server.watch) config.server.watch = { ignored: [] };
@@ -66,8 +66,8 @@
66
66
  return insertScriptHotReloadCode(src, id);
67
67
  },
68
68
  transformIndexHtml: {
69
- enforce: 'pre',
70
- transform(html, _) {
69
+ order: 'pre',
70
+ handler(html, _) {
71
71
  if (config?.allowHotReload === false) return html;
72
72
  if (userSettings?.allowHotReload === false) return html;
73
73
  const file = path.join(__dirname, 'reload-client.js');
@@ -84,7 +84,6 @@
84
84
  },
85
85
  ]
86
86
  }
87
-
88
87
  },
89
88
  }
90
89
  }
src/engine-components/Animation.ts CHANGED
@@ -59,6 +59,7 @@
59
59
 
60
60
  /**
61
61
  * Animation component to play animations on a GameObject
62
+ * @category Animation and Sequencing
62
63
  */
63
64
  export class Animation extends Behaviour implements IAnimationComponent {
64
65
 
@@ -189,6 +190,7 @@
189
190
 
190
191
  /** @internal */
191
192
  awake() {
193
+ this.mixer = undefined;
192
194
  if (debug) console.log("Animation Awake", this.name, this);
193
195
  if (this._tempAnimationsArray) {
194
196
  this.animations = this._tempAnimationsArray;
src/engine-components/AnimationUtils.ts CHANGED
@@ -1,15 +1,5 @@
1
- import { Object3D, PropertyBinding } from "three";
2
- import type { GLTF } from "three/examples/jsm/loaders/GLTFLoader.js";
1
+ import { Object3D } from "three";
3
2
 
4
- import { AnimationUtils } from "../engine/engine_animation.js";
5
- import { addComponent, addNewComponent } from "../engine/engine_components.js";
6
- import { ContextEvent, ContextRegistry } from "../engine/engine_context_registry.js";
7
- import { Animation } from "./Animation.js";
8
- import { Animator } from "./Animator.js";
9
- import { GameObject } from "./Component.js";
10
- import { PlayableDirector } from "./timeline/PlayableDirector.js";
11
-
12
-
13
3
  const $objectAnimationKey = Symbol("objectIsAnimatedData");
14
4
 
15
5
  /** Internal method - This marks an object as being animated. Make sure to always call isAnimated=false if you stop animating the object
@@ -36,40 +26,4 @@
36
26
  if (!obj) return false;
37
27
  const set = obj[$objectAnimationKey] as Set<object>;
38
28
  return set !== undefined && set.size > 0;
39
- }
40
-
41
-
42
-
43
- ContextRegistry.registerCallback(ContextEvent.ContextCreated, args => {
44
- const autoplay = args.context.domElement.getAttribute("autoplay");
45
- if (autoplay !== undefined && (autoplay === "" || autoplay === "true" || autoplay === "1")) {
46
- if (args.files) {
47
- for (const file of args.files) {
48
- const hasAnimation = GameObject.foreachComponent(file.file.scene, comp => {
49
- if (comp.enabled === false) return undefined;
50
- if (comp instanceof Animation && comp.playAutomatically || comp instanceof Animator || comp instanceof PlayableDirector && comp.playOnAwake === true) {
51
- return true;
52
- }
53
- else if (comp instanceof Animation) {
54
- comp.playAutomatically = true;
55
- return true;
56
- }
57
- else if (comp instanceof PlayableDirector) {
58
- comp.playOnAwake = true;
59
- return true;
60
- }
61
- return undefined;
62
- }, true);
63
- if (hasAnimation !== true) {
64
- AnimationUtils.assignAnimationsFromFile(file.file as GLTF, {
65
- createAnimationComponent: (obj, _clip) => {
66
- return addComponent(obj, Animation);
67
- },
68
- });
69
- }
70
- }
71
- }
72
- }
73
- });
74
-
75
-
29
+ }
src/engine-components/Animator.ts CHANGED
@@ -26,6 +26,7 @@
26
26
 
27
27
  /** The Animator component is used to play animations on a GameObject. It is used in combination with an AnimatorController (which is a state machine for animations)
28
28
  * A new AnimatorController can be created from code via `AnimatorController.createFromClips`
29
+ * @category Animation and Sequencing
29
30
  */
30
31
  export class Animator extends Behaviour implements IAnimationComponent {
31
32
 
src/engine-components/postprocessing/Effects/Antialiasing.ts CHANGED
@@ -14,7 +14,9 @@
14
14
  ULTRA = 3
15
15
  }
16
16
 
17
-
17
+ /**
18
+ * @category Effects
19
+ */
18
20
  export class Antialiasing extends PostProcessingEffect {
19
21
  get typeName(): string {
20
22
  return "Antialiasing";
src/engine-components-experimental/api.ts CHANGED
@@ -1,1 +1,5 @@
1
+ /**
2
+ * @module Experimental Components
3
+ */
4
+
1
5
  export * from "./networking/PlayerSync.js";
src/engine-components/api.ts CHANGED
@@ -1,3 +1,39 @@
1
+ /**
2
+ * Contains Needle Engine Core Components.
3
+ *
4
+ * This includes
5
+ * - Interactivity components
6
+ * {@link DragControls}, {@link SmoothFollow}, {@link Duplicatable}, {@link SpatialTrigger}
7
+ *
8
+ * - Everywhere Actions
9
+ * {@link SetActiveOnClick}, {@link PlayAnimationOnClick}, {@link PlayAudioOnClick}, {@link ChangeMaterialOnClick}
10
+ * - Camera and user controls
11
+ * {@link OrbitControls}, {@link CharacterController}
12
+ * - Rendering components
13
+ * {@link Light}, {@link Renderer}, {@link ParticleSystem}, {@link Volume} (post processing), {@link ReflectionProbe}, {@link GroundProjectedEnv}, {@link ShadowCatcher}
14
+ * - Media components
15
+ * {@link AudioSource}, {@link VideoPlayer}
16
+ * - Helpers
17
+ * {@link AxesHelper}, {@link GridHelper}, {@link TransformGizmo}
18
+ * - Asset Management components
19
+ * {@link DropListener}, {@link SceneSwitcher}, {@link GltfExport}
20
+ * - XR components
21
+ * {@link WebXR}, {@link USDZExporter}, {@link XRRig}
22
+ * - Networking components
23
+ * {@link SyncedRoom}, {@link SyncedTransform}, {@link SyncedCamera}, {@link Voip}, {@link ScreenCapture}
24
+ * - Animation components
25
+ * {@link Animator}, {@link Animation}, {@link PlayableDirector}
26
+ * - Physics components
27
+ * {@link Rigidbody}, {@link BoxCollider}, {@link SphereCollider}, {@link MeshCollider}, {@link PhysicsMaterial}
28
+ * - Utilities
29
+ * {@link NeedleMenu}
30
+ * - and more.
31
+ *
32
+ * All these components are available wherever Needle Engine is used.
33
+ *
34
+ * @module Built-in Components
35
+ */
36
+
1
37
  export * from "./codegen/components.js";
2
38
  export { Behaviour, Component, GameObject } from "./Component.js"
3
39
 
@@ -13,6 +49,11 @@
13
49
 
14
50
  import "./CameraUtils.js"
15
51
  import "./AnimationUtils.js"
52
+ import "./AnimationUtilsAutoplay.js"
16
53
 
17
54
  export { DragMode } from "./DragControls.js"
18
55
  export * from "./particlesystem/api.js"
56
+
57
+ // for correct type resolution in JSDoc
58
+ import type { PhysicsMaterial } from "../engine/engine_physics.types.js";
59
+ import type { Animation } from "./Animation.js";
src/engine-schemes/api.ts CHANGED
@@ -1,1 +1,13 @@
1
+ /**
2
+ * This module contains the networking schemes used by Needle Engine.
3
+ * They are used to define the structure of the data that is sent over the network.
4
+ * Networking can use plain text or flatbuffers for serialization.
5
+ * Flatbuffers are more efficient and faster than plain text, but require more setup work.
6
+ *
7
+ * Some core components, like SyncedCamera or SyncedTransform, thus use Flatbuffers for networking to reduce latency and bandwidth.
8
+ *
9
+ * Schemes are compiled with [Flatbuffers 2.0](https://github.com/google/flatbuffers/releases/tag/v2.0.0).
10
+ * @module Networking Schemes
11
+ */
12
+
1
13
  export * from "./schemes.js";
src/engine/api.ts CHANGED
@@ -1,3 +1,23 @@
1
+ /**
2
+ * Contains core functionality for Needle Engine.
3
+ * This includes
4
+ * - Context Management
5
+ * - Asset Loading
6
+ * - Component Lifecycle
7
+ * - Time Handling
8
+ * - XR support
9
+ * - Unified Input Handling
10
+ * - Needle Menu
11
+ * - Networking
12
+ * - Physics, Collisions, Raycasting
13
+ * - Math and Filtering Helpers
14
+ * - Rendering Utilities
15
+ * - Debugging Utilities, Gizmos
16
+ * - User agent detection
17
+ * - and more.
18
+ *
19
+ * @module Engine Core
20
+ */
1
21
 
2
22
  export * from "./debug/index.js";
3
23
  export * from "./engine_addressables.js";
src/engine-components/AudioListener.ts CHANGED
@@ -6,6 +6,7 @@
6
6
 
7
7
  /**
8
8
  * AudioListener represents a listener that can be attached to a GameObject to listen to audio sources in the scene.
9
+ * @category Multimedia
9
10
  */
10
11
  export class AudioListener extends Behaviour {
11
12
 
src/engine-components/AudioSource.ts CHANGED
@@ -34,7 +34,7 @@
34
34
 
35
35
  /** The AudioSource can be used to play audio in the scene.
36
36
  * Use `clip` to set the audio file to play.
37
- * @category Components
37
+ * @category Multimedia
38
38
  */
39
39
  export class AudioSource extends Behaviour {
40
40
 
src/engine-components/webxr/Avatar.ts CHANGED
@@ -17,6 +17,10 @@
17
17
 
18
18
  const flipForwardQuaternion = new Quaternion().setFromAxisAngle(new Vector3(0, 1, 0), Math.PI);
19
19
 
20
+ /**
21
+ * @category XR
22
+ * @category Networking
23
+ */
20
24
  export class Avatar extends Behaviour {
21
25
 
22
26
  @serializable(AssetReference)
src/engine-components/avatar/AvatarEyeLook_Rotation.ts CHANGED
@@ -22,8 +22,7 @@
22
22
  this.brain = GameObject.getComponentInParent(this.gameObject, Avatar_Brain_LookAt);
23
23
  }
24
24
  if (!this.brain) {
25
- console.log("No look at brain found, adding it now")
26
- this.brain = GameObject.addNewComponent(this.gameObject, Avatar_Brain_LookAt);
25
+ this.brain = GameObject.addComponent(this.gameObject, Avatar_Brain_LookAt);
27
26
  }
28
27
  if (this.brain && this.target) {
29
28
  this.brain.controlledTarget = this.target;
src/engine-components/AxesHelper.ts CHANGED
@@ -6,6 +6,7 @@
6
6
 
7
7
  /**
8
8
  * AxesHelper is a component that displays the axes of the object in the scene.
9
+ * @category Helpers
9
10
  */
10
11
  export class AxesHelper extends Behaviour {
11
12
  @serializable()
src/engine-components/ui/BaseUIComponent.ts CHANGED
@@ -26,8 +26,10 @@
26
26
 
27
27
  export const $shadowDomOwner = Symbol("shadowDomOwner");
28
28
 
29
- /** Derive from this class if you want to implement your own UI components
30
- * It provides utility methods and simplifies managing the underlying three-mesh-ui hierarchy
29
+ /**
30
+ * Derive from this class if you want to implement your own UI components.
31
+ * It provides utility methods and simplifies managing the underlying three-mesh-ui hierarchy.
32
+ * @category User Interface
31
33
  */
32
34
  export class BaseUIComponent extends Behaviour {
33
35
 
src/engine-components/export/usdz/extensions/behavior/BehaviourComponents.ts CHANGED
@@ -29,6 +29,9 @@
29
29
  }
30
30
  }
31
31
 
32
+ /**
33
+ * @category Everywhere Actions
34
+ */
32
35
  export class ChangeTransformOnClick extends Behaviour implements IPointerClickHandler, UsdzBehaviour {
33
36
 
34
37
  @serializable(Object3D)
@@ -163,6 +166,9 @@
163
166
  }
164
167
  }
165
168
 
169
+ /**
170
+ * @category Everywhere Actions
171
+ */
166
172
  export class ChangeMaterialOnClick extends Behaviour implements IPointerClickHandler, UsdzBehaviour {
167
173
 
168
174
  /**
@@ -346,6 +352,9 @@
346
352
  }
347
353
  }
348
354
 
355
+ /**
356
+ * @category Everywhere Actions
357
+ */
349
358
  export class SetActiveOnClick extends Behaviour implements IPointerClickHandler, UsdzBehaviour {
350
359
 
351
360
  @serializable(Object3D)
@@ -576,6 +585,9 @@
576
585
  }
577
586
  }
578
587
 
588
+ /**
589
+ * @category Everywhere Actions
590
+ */
579
591
  export class HideOnStart extends Behaviour implements UsdzBehaviour {
580
592
 
581
593
  start() {
@@ -601,6 +613,9 @@
601
613
  }
602
614
  }
603
615
 
616
+ /**
617
+ * @category Everywhere Actions
618
+ */
604
619
  export class EmphasizeOnClick extends Behaviour implements UsdzBehaviour {
605
620
 
606
621
  @serializable()
@@ -629,6 +644,9 @@
629
644
  afterCreateDocument(_ext, _context) { }
630
645
  }
631
646
 
647
+ /**
648
+ * @category Everywhere Actions
649
+ */
632
650
  export class PlayAudioOnClick extends Behaviour implements IPointerClickHandler, UsdzBehaviour {
633
651
 
634
652
  @serializable(AudioSource)
@@ -743,6 +761,9 @@
743
761
  }
744
762
  }
745
763
 
764
+ /**
765
+ * @category Everywhere Actions
766
+ */
746
767
  export class PlayAnimationOnClick extends Behaviour implements IPointerClickHandler, UsdzBehaviour, UsdzAnimation {
747
768
 
748
769
  @serializable(Animator)
@@ -1056,6 +1077,9 @@
1056
1077
  target?: PreliminaryAction;
1057
1078
  }
1058
1079
 
1080
+ /**
1081
+ * @category Everywhere Actions
1082
+ */
1059
1083
  export class VisibilityAction extends PreliminaryAction {
1060
1084
 
1061
1085
  //@type int
@@ -1077,6 +1101,9 @@
1077
1101
  }
1078
1102
  }
1079
1103
 
1104
+ /**
1105
+ * @category Everywhere Actions
1106
+ */
1080
1107
  export class TapGestureTrigger extends PreliminaryTrigger {
1081
1108
 
1082
1109
  }
src/engine-components/postprocessing/Effects/BloomEffect.ts CHANGED
@@ -17,6 +17,8 @@
17
17
  * bloom.scatter.value = 0.5;
18
18
  * volume.add(bloom);
19
19
  * ```
20
+ *
21
+ * @category Effects
20
22
  */
21
23
  export class BloomEffect extends PostProcessingEffect {
22
24
 
src/engine-components/BoxHelperComponent.ts CHANGED
@@ -1,13 +1,16 @@
1
1
  import { Box3, Color, type ColorRepresentation, LineSegments, Object3D, Vector3 } from "three";
2
2
 
3
3
  import { CreateWireCube, Gizmos } from "../engine/engine_gizmos.js";
4
- import { getWorldPosition, getWorldScale } from "../engine/engine_three_utils.js";
4
+ import { getBoundingBox, getWorldPosition, getWorldScale } from "../engine/engine_three_utils.js";
5
5
  import { getParam } from "../engine/engine_utils.js";
6
6
  import { Behaviour } from "./Component.js";
7
7
 
8
8
  const gizmos = getParam("gizmos");
9
9
  const debug = getParam("debugboxhelper");
10
10
 
11
+ /**
12
+ * @category Helpers
13
+ */
11
14
  export class BoxHelperComponent extends Behaviour {
12
15
 
13
16
  private box: Box3 | null = null;
@@ -15,44 +18,27 @@
15
18
  private _lastMatrixUpdateFrame: number = -1;
16
19
  private static _position: Vector3 = new Vector3();
17
20
  private static _size: Vector3 = new Vector3(.01, .01, .01);
21
+ private static _emptyObjectSize: Vector3 = new Vector3(.01, .01, .01);
18
22
 
19
- public isInBox(obj: Object3D, scaleFactor?: number): boolean | undefined {
23
+ public isInBox(obj: Object3D): boolean | undefined {
20
24
  if (!obj) return undefined;
21
25
 
22
- // if (!obj.geometry.boundingBox) obj.geometry.computeBoundingBox();
23
- // if (!obj.geometry.boundingBox) return undefined;
24
-
25
26
  if (!this.box) {
26
27
  this.box = new Box3();
27
28
  }
28
29
 
30
+ getBoundingBox([obj], undefined, undefined, BoxHelperComponent.testBox);
29
31
 
30
- if (obj.type === "Mesh") {
31
- BoxHelperComponent.testBox.setFromObject(obj);
32
- }
33
- else if (obj.type === "Group") {
34
- BoxHelperComponent.testBox.makeEmpty();
35
- if (obj.children.length > 0) {
36
- for (let i = 0; i < obj.children.length; i++) {
37
- const ch = obj.children[i];
38
- if (ch.type === "Mesh") {
39
- BoxHelperComponent.testBox.expandByObject(obj);
40
- }
41
- }
42
- }
43
- }
44
- else {
32
+ if (BoxHelperComponent.testBox.isEmpty()) {
45
33
  const wp = getWorldPosition(obj, BoxHelperComponent._position);
46
- const size = getWorldScale(obj, BoxHelperComponent._size);
47
- if (scaleFactor !== undefined) size.multiplyScalar(scaleFactor);
48
- BoxHelperComponent.testBox.setFromCenterAndSize(wp, size);
34
+ BoxHelperComponent.testBox.setFromCenterAndSize(wp, BoxHelperComponent._emptyObjectSize);
49
35
  }
50
36
 
51
37
  this.updateBox();
52
38
  const intersects = this.box?.intersectsBox(BoxHelperComponent.testBox);
53
39
  if (intersects) {
54
- if (debug)
55
- Gizmos.DrawWireBox3(BoxHelperComponent.testBox, 0xff0000, 5);
40
+ if (debug) Gizmos.DrawWireBox3(BoxHelperComponent.testBox, 0xff0000, 5);
41
+
56
42
  }
57
43
  return intersects;
58
44
  }
src/engine-components/ui/Button.ts CHANGED
@@ -60,6 +60,9 @@
60
60
  selectedTrigger!: string;
61
61
  }
62
62
 
63
+ /**
64
+ * @category User Interface
65
+ */
63
66
  export class Button extends Behaviour implements IPointerEventHandler {
64
67
 
65
68
  /**
src/engine-components/Camera.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { EquirectangularReflectionMapping, Euler, Frustum, Matrix, Matrix4, OrthographicCamera, PerspectiveCamera, Ray, SRGBColorSpace, Vector3 } from "three";
1
+ import { EquirectangularReflectionMapping, Euler, Frustum, Matrix4, OrthographicCamera, PerspectiveCamera, Ray, Vector3 } from "three";
2
2
  import { Texture } from "three";
3
3
 
4
4
  import { isDevEnvironment, showBalloonMessage, showBalloonWarning } from "../engine/debug/index.js";
@@ -27,6 +27,9 @@
27
27
  const debug = getParam("debugcam");
28
28
  const debugscreenpointtoray = getParam("debugscreenpointtoray");
29
29
 
30
+ /**
31
+ * @category Camera Controls
32
+ */
30
33
  export class Camera extends Behaviour implements ICamera {
31
34
 
32
35
  get isCamera() {
@@ -236,7 +239,19 @@
236
239
  private _clearFlags: ClearFlags = ClearFlags.SolidColor;
237
240
  private _skybox?: CameraSkybox;
238
241
 
242
+ /**
243
+ * Get the three.js camera object. This will create a camera if it does not exist yet.
244
+ * @returns {PerspectiveCamera | OrthographicCamera} the three camera
245
+ * @deprecated use {@link threeCamera} instead
246
+ */
239
247
  public get cam(): PerspectiveCamera | OrthographicCamera {
248
+ return this.threeCamera;
249
+ }
250
+ /**
251
+ * Get the three.js camera object. This will create a camera if it does not exist yet.
252
+ * @returns {PerspectiveCamera | OrthographicCamera} the three camera
253
+ */
254
+ public get threeCamera(): PerspectiveCamera | OrthographicCamera {
240
255
  if (this.activeAndEnabled)
241
256
  this.buildCamera();
242
257
  return this._cam!;
src/engine-components/ui/Canvas.ts CHANGED
@@ -24,6 +24,9 @@
24
24
 
25
25
  const debugLayout = getParam("debuguilayout");
26
26
 
27
+ /**
28
+ * @category User Interface
29
+ */
27
30
  export class Canvas extends UIRootComponent implements ICanvas {
28
31
 
29
32
  get isCanvas() {
src/engine-components/ui/CanvasGroup.ts CHANGED
@@ -6,6 +6,9 @@
6
6
  import { type ICanvasGroup, type IHasAlphaFactor } from "./Interfaces.js";
7
7
 
8
8
 
9
+ /**
10
+ * @category User Interface
11
+ */
9
12
  export class CanvasGroup extends Behaviour implements ICanvasGroup {
10
13
  @serializable()
11
14
  get alpha(): number {
src/engine-components/CharacterController.ts CHANGED
@@ -13,6 +13,9 @@
13
13
 
14
14
  const debug = getParam("debugcharactercontroller");
15
15
 
16
+ /**
17
+ * @category Camera Controls
18
+ */
16
19
  export class CharacterController extends Behaviour {
17
20
 
18
21
  @serializable(Vector3)
@@ -102,6 +105,10 @@
102
105
  }
103
106
  }
104
107
 
108
+ /**
109
+ * @category Camera Controls
110
+ * @category Interactivity
111
+ */
105
112
  export class CharacterControllerInput extends Behaviour {
106
113
 
107
114
  @serializable(CharacterController)
src/engine-components/postprocessing/Effects/ChromaticAberration.ts CHANGED
@@ -6,6 +6,9 @@
6
6
  import { VolumeParameter } from "../VolumeParameter.js";
7
7
  import { registerCustomEffectType, VolumeProfile } from "../VolumeProfile.js";
8
8
 
9
+ /**
10
+ * @category Effects
11
+ */
9
12
  export class ChromaticAberration extends PostProcessingEffect {
10
13
 
11
14
  get typeName() {
src/engine-components/Collider.ts CHANGED
@@ -17,7 +17,6 @@
17
17
  * Colliders are used in combination with a Rigidbody to create physical interactions between objects.
18
18
  * Colliders are registered with the physics engine when they are enabled and removed when they are disabled.
19
19
  * @category Physics
20
- * @inheritdoc
21
20
  */
22
21
  export class Collider extends Behaviour implements ICollider {
23
22
 
@@ -100,6 +99,7 @@
100
99
 
101
100
  /**
102
101
  * SphereCollider is a collider that represents a sphere shape.
102
+ * @category Physics
103
103
  */
104
104
  export class SphereCollider extends Collider implements ISphereCollider {
105
105
 
@@ -128,6 +128,7 @@
128
128
 
129
129
  /**
130
130
  * BoxCollider is a collider that represents a box shape.
131
+ * @category Physics
131
132
  */
132
133
  export class BoxCollider extends Collider implements IBoxCollider {
133
134
 
@@ -172,6 +173,7 @@
172
173
  /**
173
174
  * MeshCollider is a collider that represents a mesh shape.
174
175
  * The mesh collider can be used to create a collider from a mesh.
176
+ * @category Physics
175
177
  */
176
178
  export class MeshCollider extends Collider {
177
179
 
@@ -239,6 +241,7 @@
239
241
 
240
242
  /**
241
243
  * CapsuleCollider is a collider that represents a capsule shape.
244
+ * @category Physics
242
245
  */
243
246
  export class CapsuleCollider extends Collider {
244
247
  @serializable(Vector3)
src/engine-components/postprocessing/Effects/ColorAdjustments.ts CHANGED
@@ -7,6 +7,9 @@
7
7
  import { registerCustomEffectType } from "../VolumeProfile.js";
8
8
  import { ToneMappingEffect } from "./Tonemapping.js";
9
9
 
10
+ /**
11
+ * @category Effects
12
+ */
10
13
  export class ColorAdjustments extends PostProcessingEffect {
11
14
 
12
15
  get typeName() {
src/engine-components/Component.ts CHANGED
@@ -294,6 +294,7 @@
294
294
 
295
295
  public static getAllComponents(go: IGameObject | Object3D): Component[] {
296
296
  const componentsList = go.userData?.components;
297
+ if (!componentsList) return [];
297
298
  const newList = [...componentsList];
298
299
  return newList;
299
300
  }
@@ -313,15 +314,17 @@
313
314
  /**
314
315
  * Needle Engine component base class. Component's are the main building blocks of the Needle Engine.
315
316
  * Derive from {@link Behaviour} to implement your own using the provided lifecycle methods.
316
- * Components can be added to threejs objects using `{@link addComponent}` or `{@link GameObject.addComponent}`
317
+ * Components can be added to any {@link Object3D} using {@link addComponent} or {@link GameObject.addComponent}.
317
318
  *
318
- * The most common lifecycle methods are `awake`, `start`, `onEnable`, `onDisable` `update` and `onDestroy`.
319
- * XR specific callbacks include `onEnterXR`, `onLeaveXR`, `onUpdateXR`, `onControllerAdded` and `onControllerRemoved`.
320
- * To receive pointer events implement `onPointerDown`, `onPointerUp`, `onPointerEnter`, `onPointerExit` and `onPointerMove`.
319
+ * The most common lifecycle methods are {@link update}, {@link awake}, {@link start}, {@link onEnable}, {@link onDisable} and {@link onDestroy}.
321
320
  *
321
+ * XR specific callbacks include {@link onEnterXR}, {@link onLeaveXR}, {@link onUpdateXR}, {@link onXRControllerAdded} and {@link onXRControllerRemoved}.
322
+ *
323
+ * To receive pointer events implement {@link onPointerDown}, {@link onPointerUp}, {@link onPointerEnter}, {@link onPointerExit} and {@link onPointerMove}.
324
+ *
322
325
  * @example
323
326
  * ```typescript
324
- * import { Behaviour } from "@engine/engine/engine";
327
+ * import { Behaviour } from "@needle-tools/engine";
325
328
  * export class MyComponent extends Behaviour {
326
329
  * start() {
327
330
  * console.log("Hello World");
@@ -331,6 +334,8 @@
331
334
  * }
332
335
  * }
333
336
  * ```
337
+ *
338
+ * @group Components
334
339
  */
335
340
  export abstract class Component implements IComponent, EventTarget,
336
341
  Partial<INeedleXRSessionEventReceiver>,
src/engine-components/ContactShadows.ts CHANGED
@@ -32,6 +32,7 @@
32
32
 
33
33
  /**
34
34
  * ContactShadows is a component that allows to display contact shadows in the scene.
35
+ * @category Rendering
35
36
  */
36
37
  export class ContactShadows extends Behaviour {
37
38
 
src/engine/debug/debug_spatial_console.ts CHANGED
@@ -6,7 +6,6 @@
6
6
  import { OneEuroFilterXYZ } from "../engine_math.js";
7
7
  import { lookAtObject } from "../engine_three_utils.js";
8
8
  import type { IContext, IGameObject } from "../engine_types.js";
9
- import { getParam } from "../engine_utils.js";
10
9
  import { isDevEnvironment } from "./debug.js";
11
10
  import { onError } from "./debug_overlay.js";
12
11
 
src/engine-components/DeleteBox.ts CHANGED
@@ -1,6 +1,7 @@
1
1
 
2
2
  import { Mesh } from "three";
3
3
 
4
+ import { Gizmos } from "../engine/engine_gizmos.js";
4
5
  import { syncDestroy } from "../engine/engine_networking_instantiate.js";
5
6
  import { getParam } from "../engine/engine_utils.js";
6
7
  import { BoxHelperComponent } from "./BoxHelperComponent.js";
@@ -8,29 +9,52 @@
8
9
  import { UsageMarker } from "./Interactable.js";
9
10
 
10
11
  const debug = getParam("debugdeletable");
12
+ /**
13
+ * A box-shaped area that can be used to delete objects that get into it. Useful for sandbox-style builders or physics simulations.
14
+ * @category Interactivity
15
+ */
16
+ export class DeleteBox extends BoxHelperComponent {
17
+ static _instances: DeleteBox[] = [];
18
+
19
+ onEnable(): void {
20
+ DeleteBox._instances.push(this);
21
+ }
11
22
 
12
- export class DeleteBox extends BoxHelperComponent { }
23
+ onDisable(): void {
24
+ const idx = DeleteBox._instances.indexOf(this);
25
+ if (idx >= 0) DeleteBox._instances.splice(idx, 1);
26
+ }
27
+ }
13
28
 
14
-
29
+ /** Objects with this component can be destroyed by the {@link DeleteBox} component.
30
+ * @category Interactivity
31
+ */
15
32
  export class Deletable extends Behaviour {
16
33
 
17
- private deleteBoxes: DeleteBox[] = [];
18
-
19
- awake() {
20
- this.deleteBoxes = GameObject.findObjectsOfType(DeleteBox, this.context);
21
- }
22
-
23
34
  update(): void {
24
- for (const box of this.deleteBoxes) {
35
+ for (const box of DeleteBox._instances) {
25
36
  const obj = this.gameObject as unknown as Mesh;
26
37
  const res = box.isInBox(obj);
27
38
  if (res === true) {
28
39
  const marker = GameObject.getComponentInParent(this.gameObject, UsageMarker);
29
40
  if (!marker) {
30
- if (debug) console.log("DESTROY", this.gameObject);
41
+ if (debug) {
42
+ try {
43
+ if (box["box"]) {
44
+ const deleteBoxArea = box["box"];
45
+ const deletedObjectArea = BoxHelperComponent["testBox"];
46
+ Gizmos.DrawWireBox3(deleteBoxArea, 0xff0000, 5);
47
+ Gizmos.DrawWireBox3(deletedObjectArea, 0x0000ff, 5);
48
+ console.log("DeleteBox: Destroying", this.gameObject, { deleteBoxArea, deletedObjectArea });
49
+ }
50
+ else {
51
+ console.log("DeleteBox: Destroying", this.gameObject);
52
+ }
53
+ } catch (_e) {}
54
+ }
31
55
  syncDestroy(this.gameObject, this.context.connection);
32
56
  }
33
- else if (debug) console.warn("Can not delete object with usage marker", this.guid, marker)
57
+ else if (debug) console.warn("DeleteBox: Not deleting object with usage marker", this.guid, marker)
34
58
  }
35
59
  }
36
60
  }
src/engine-components/postprocessing/Effects/DepthOfField.ts CHANGED
@@ -15,6 +15,9 @@
15
15
 
16
16
  const debug = getParam("debugpost");
17
17
 
18
+ /**
19
+ * @category Effects
20
+ */
18
21
  export class DepthOfField extends PostProcessingEffect {
19
22
 
20
23
  get typeName() {
src/engine-components/DeviceFlag.ts CHANGED
@@ -10,6 +10,9 @@
10
10
  Mobile = 2 << 0,
11
11
  }
12
12
 
13
+ /**
14
+ * @category Utilities
15
+ */
13
16
  export class DeviceFlag extends Behaviour {
14
17
 
15
18
  @serializable()
src/engine-components/DragControls.ts CHANGED
@@ -42,6 +42,7 @@
42
42
 
43
43
  /**
44
44
  * DragControls allows you to drag objects around in the scene. It can be used to move objects in 2D (screen space) or 3D (world space).
45
+ * @category Interactivity
45
46
  */
46
47
  export class DragControls extends Behaviour implements IPointerEventHandler {
47
48
 
@@ -370,14 +371,10 @@
370
371
  private onLastDragEnd(evt: PointerEventData | null) {
371
372
  if (!this || !this._isDragging) return;
372
373
  this._isDragging = false;
373
- if (!this._dragHelper) return;
374
374
  for (const rb of this._draggingRigidbodies) {
375
375
  rb.setVelocity(rb.smoothedVelocity);
376
376
  }
377
377
  this._draggingRigidbodies.length = 0;
378
- const selected = this._dragHelper.selected;
379
- if (debug) console.log("DRAG END", selected, selected?.visible)
380
- this._dragHelper.setSelected(null, this.context);
381
378
  this._targetObject = null;
382
379
  if (evt?.object) {
383
380
  const sync = GameObject.getComponentInChildren(evt.object, SyncedTransform);
@@ -388,6 +385,11 @@
388
385
  }
389
386
  if (this._marker)
390
387
  this._marker.destroy();
388
+
389
+ if (!this._dragHelper) return;
390
+ const selected = this._dragHelper.selected;
391
+ if (debug) console.log("DRAG END", selected, selected?.visible)
392
+ this._dragHelper.setSelected(null, this.context);
391
393
  }
392
394
  }
393
395
 
@@ -939,9 +941,16 @@
939
941
  // can only handle a single pointer
940
942
  // if there's more, we defer to multi-touch drag handlers
941
943
  if (numberOfPointers > 1) return;
942
-
943
- const draggedObject = this.gameObject as IGameObject;
944
- const dragSource = this._followObject.parent as IGameObject;
944
+ const draggedObject = this.gameObject as IGameObject | null;
945
+ if (!draggedObject || !this._followObject) {
946
+ console.warn("Warning: DragPointerHandler doesn't have a dragged object. This is likely a bug.");
947
+ return;
948
+ }
949
+ const dragSource = this._followObject.parent as IGameObject | null;
950
+ if (!dragSource) {
951
+ console.warn("Warning: DragPointerHandler doesn't have a drag source. This is likely a bug.");
952
+ return;
953
+ }
945
954
  this._followObject.updateMatrix();
946
955
  const dragSourceWP = dragSource.worldPosition;
947
956
  const rayDirection = dragSource.worldForward;
src/engine-components/DropListener.ts CHANGED
@@ -63,6 +63,8 @@
63
63
  * const gltf = evt.detail as GLTF;
64
64
  * });
65
65
  * ```
66
+ *
67
+ * @category Asset Management
66
68
  */
67
69
  export class DropListener extends Behaviour {
68
70
 
src/engine-components/Duplicatable.ts CHANGED
@@ -13,6 +13,7 @@
13
13
  /**
14
14
  * The Duplicatable component is used to duplicate a assigned {@link GameObject} when a pointer event occurs on the GameObject.
15
15
  * It implements the {@link IPointerEventHandler} interface and can be used to expose duplication to the user in the editor without writing code.
16
+ * @category Interactivity
16
17
  */
17
18
  export class Duplicatable extends Behaviour implements IPointerEventHandler {
18
19
 
@@ -122,12 +123,15 @@
122
123
  const res = this.handleDuplication();
123
124
  if (res) {
124
125
  const dragControls = GameObject.getComponent(res, DragControls);
125
- if (!dragControls) console.warn("Duplicated object does not have DragControls");
126
+ if (!dragControls) console.warn("Duplicated object does not have DragControls", res);
126
127
  else {
127
128
  dragControls.onPointerDown(args);
128
129
  this._forwardPointerEvents.set(args.event.space, dragControls);
129
130
  }
130
131
  }
132
+ else {
133
+ console.warn("Could not duplicate object. Has the target object been destroyed?", this);
134
+ }
131
135
  }
132
136
 
133
137
  /** @internal */
@@ -154,6 +158,10 @@
154
158
  if (!this.object) return null;
155
159
  if (this._currentCount >= this.limitCount) return null;
156
160
  if (this.object === this.gameObject) return null;
161
+ if (GameObject.isDestroyed(this.object)) {
162
+ this.object = null;
163
+ return null;
164
+ }
157
165
 
158
166
  this.object.visible = true;
159
167
 
src/engine-components/postprocessing/Effects/EffectWrapper.ts CHANGED
@@ -2,6 +2,9 @@
2
2
 
3
3
  import { EffectProviderResult, PostProcessingEffect } from "../PostProcessingEffect.js";
4
4
 
5
+ /**
6
+ * @category Effects
7
+ */
5
8
  export class EffectWrapper extends PostProcessingEffect {
6
9
 
7
10
  readonly effect: Effect;
src/engine/engine_addressables.ts CHANGED
@@ -14,7 +14,7 @@
14
14
 
15
15
  /**
16
16
  * The Addressables class is used to register and manage {@link AssetReference} types
17
- * It can be accessed via {@link Context.Current} or `{@link Context.addressables}` (e.g. `this.context.addressables` in a component)
17
+ * It can be accessed from components via {@link Context.Current} or {@link Context.addressables} (e.g. `this.context.addressables`)
18
18
  */
19
19
  export class Addressables {
20
20
 
@@ -90,8 +90,6 @@
90
90
  export class AssetReference {
91
91
 
92
92
  /**
93
- * Experimental!
94
- * @internal
95
93
  * Get an AssetReference for a URL to be easily loaded.
96
94
  * AssetReferences are cached so calling this method multiple times with the same arguments will always return the same AssetReference.
97
95
  * @param url The URL of the asset to load. The url can be relative or absolute.
src/engine/engine_animation.ts CHANGED
@@ -52,7 +52,6 @@
52
52
 
53
53
  /**
54
54
  * Utility class for working with animations.
55
- * @category Animation
56
55
  */
57
56
  export class AnimationUtils {
58
57
 
src/engine/engine_assetdatabase.ts CHANGED
@@ -79,6 +79,7 @@
79
79
  disposeObjectResources(obj.customDistanceMaterial);
80
80
  obj.geometry = null;
81
81
  obj.material = null;
82
+ obj.visible = false;
82
83
  }
83
84
  else if (obj instanceof Mesh) {
84
85
  disposeObjectResources(obj.geometry);
@@ -87,6 +88,7 @@
87
88
  disposeObjectResources(obj.customDistanceMaterial);
88
89
  obj.geometry = null;
89
90
  obj.material = null;
91
+ obj.visible = false;
90
92
  }
91
93
  else if (obj instanceof BufferGeometry) {
92
94
  free(obj);
src/engine/engine_element.ts CHANGED
@@ -827,9 +827,12 @@
827
827
  lastCharacterWasSpace = true;
828
828
  }
829
829
  }
830
- console.debug("Generated display name: \"" + name + "\" → \"" + displayName + "\"");
830
+
831
+ if (isDevEnvironment() && name !== displayName)
832
+ console.debug("Generated display name: \"" + name + "\" → \"" + displayName + "\"");
831
833
  return displayName.trim();
832
834
  }
833
- console.debug("Loading: use default name", name);
835
+ if (isDevEnvironment())
836
+ console.debug("Loading: use default name", name);
834
837
  return name;
835
838
  }
src/engine/engine_license.ts CHANGED
@@ -36,12 +36,14 @@
36
36
  }
37
37
 
38
38
  const _licenseCheckResultChangedCallbacks: ((result: boolean) => void)[] = [];
39
+
39
40
  /** @internal */
40
41
  export function onLicenseCheckResultChanged(cb: (result: boolean) => void) {
41
42
  if (hasProLicense() || hasIndieLicense())
42
43
  return cb(true);
43
44
  _licenseCheckResultChangedCallbacks.push(cb);
44
45
  }
46
+
45
47
  function invokeLicenseCheckResultChanged(result: boolean) {
46
48
  for (const cb of _licenseCheckResultChangedCallbacks) {
47
49
  try {
src/engine/engine_lods.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { LODsManager as _LODsManager, NEEDLE_progressive, NEEDLE_progressive_mesh_model, NEEDLE_progressive_plugin } from "@needle-tools/gltf-progressive";
2
- import { LOD_Results } from "@needle-tools/gltf-progressive/src/lods_manager.js";
3
- import { Box3, BufferGeometry, Camera, Mesh, PerspectiveCamera, Scene, Sphere, Vector3, WebGLRenderer } from "three";
1
+ import { LODsManager as _LODsManager, NEEDLE_progressive, NEEDLE_progressive_plugin } from "@needle-tools/gltf-progressive";
2
+ import type { LOD_Results } from "@needle-tools/gltf-progressive/src/lods_manager.js";
3
+ import { Box3, Camera, Mesh, PerspectiveCamera, Scene, Sphere, WebGLRenderer } from "three";
4
4
 
5
5
  import { findResourceUsers } from "./engine_assetdatabase.js";
6
6
  import type { Context } from "./engine_context.js";
src/engine/engine_networking_auto.ts CHANGED
@@ -198,7 +198,7 @@
198
198
  onPropertyChanged: Function,
199
199
  };
200
200
 
201
- export declare type FieldChangedCallbackFn = (newValue: any, previousValue: any) => void | boolean;
201
+ export declare type FieldChangedCallbackFn = (newValue: any, previousValue: any) => void | boolean | any;
202
202
 
203
203
  /**
204
204
  * **Decorate a field to be automatically networked synced**
@@ -207,7 +207,7 @@
207
207
  *
208
208
  * @param onFieldChanged name of a callback function that will be called when the field is changed.
209
209
  * You can also pass in a function like so: syncField(myClass.prototype.myFunctionToBeCalled)
210
- * This function may return false to prevent notifyChanged from being called
210
+ * Note: if you return `false` from this function you'll prevent the field from being synced with other clients
211
211
  * (for example a networked color is sent as a number and may be converted to a color in the receiver again)
212
212
  * Parameters: (newValue, previousValue)
213
213
  */
src/engine/engine_networking.ts CHANGED
@@ -73,8 +73,9 @@
73
73
  }
74
74
 
75
75
  /** The Needle Engine networking server supports the concept of ownership that can be requested.
76
- * The `{@link OwnershipModel}` enum contains possible outgoing (Request) and incoming (Response) events for communicating ownership.
77
- * We recommend using the `OwnershipModel` class instead of dealing with those events directly. */
76
+ * This enum contains possible outgoing (Request*) and incoming (Response*) events for communicating ownership.
77
+ *
78
+ * Typically, using the {@link OwnershipModel} class instead of dealing with those events directly is preferred. */
78
79
  export enum OwnershipEvent {
79
80
  RequestHasOwner = 'request-has-owner',
80
81
  ResponseHasOwner = "response-has-owner",
src/engine/engine_physics.types.ts CHANGED
@@ -7,6 +7,9 @@
7
7
  Maximum = 3,
8
8
  }
9
9
 
10
+ /**
11
+ * Properties for physics simulation, like friction or bounciness.
12
+ */
10
13
  export type PhysicsMaterial = {
11
14
  bounceCombine?: PhysicsMaterialCombine;
12
15
  bounciness?: number;
src/engine/engine_scenetools.ts CHANGED
@@ -39,6 +39,7 @@
39
39
 
40
40
  const printGltf = utils.getParam("printGltf") || utils.getParam("printgltf");
41
41
  const downloadGltf = utils.getParam("downloadgltf");
42
+ const debugFileTypes = utils.getParam("debugfileformat");
42
43
 
43
44
  // const loader = new GLTFLoader();
44
45
  // registerExtensions(loader);
@@ -100,7 +101,7 @@
100
101
  export async function createLoader(url: string, context: Context): Promise<GLTFLoader | FBXLoader | USDZLoader | OBJLoader | null> {
101
102
 
102
103
  const type = await tryDetermineFileTypeFromURL(url) || "unknown";
103
- console.debug("Determined file type: " + type + " for url", url);
104
+ if (debugFileTypes) console.debug("Determined file type: " + type + " for url", url);
104
105
 
105
106
  switch (type) {
106
107
  case "unknown":
src/engine/engine_three_utils.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { AnimationAction, Box3, Box3Helper, Color, Euler, GridHelper, Material, Mesh, MeshStandardMaterial, Object3D, PerspectiveCamera, PlaneGeometry, Quaternion, Scene, ShadowMaterial, Texture, Uniform, Vector3 } from "three";
1
+ import { AnimationAction, Box3, Box3Helper, Color, Euler, GridHelper, Layers, Material, Mesh, MeshStandardMaterial, Object3D, PerspectiveCamera, PlaneGeometry, Quaternion, Scene, ShadowMaterial, Texture, Uniform, Vector3 } from "three";
2
2
  import { ShaderMaterial, WebGLRenderer } from "three";
3
3
  import { GroundedSkybox } from "three/examples/jsm/objects/GroundedSkybox.js";
4
4
 
@@ -548,13 +548,14 @@
548
548
  }
549
549
 
550
550
  /**
551
- * Get the bounding box of a list of objects
552
- * @param objects the objects to get the bounding box from
553
- * @param ignore objects to ignore when calculating the bounding box
554
- * @param output an optional output object to store the result in
551
+ * Get the axis-aligned bounding box of a list of objects.
552
+ * @param objects The objects to get the bounding box from.
553
+ * @param ignore Objects to ignore when calculating the bounding box. Objects that are invisible (gizmos, helpers, etc.) are excluded by default.
554
+ * @param layers The layers to include. Typically the main camera's layers.
555
+ * @param result The result box to store the bounding box in. Returns a new box if not passed in.
555
556
  */
556
- export function getBoundingBox(objects: Object3D[], ignore: ((obj: Object3D) => void | boolean) | Array<Object3D | null | undefined> | undefined = undefined): Box3 {
557
- const box = new Box3();
557
+ export function getBoundingBox(objects: Object3D[], ignore: ((obj: Object3D) => void | boolean) | Array<Object3D | null | undefined> | undefined = undefined, layers: Layers | undefined | null = undefined, result: Box3 | undefined = undefined): Box3 {
558
+ const box = result || new Box3();
558
559
  box.makeEmpty();
559
560
 
560
561
  const emptyChildren = [];
@@ -573,9 +574,9 @@
573
574
  // // Ignore shadow catcher geometry
574
575
  if ((obj as Mesh).material instanceof ShadowMaterial) allowExpanding = false;
575
576
  // ONLY fit meshes
576
- if (!(isMesh(obj))) {
577
- allowExpanding = false;
578
- }
577
+ if (!(isMesh(obj))) allowExpanding = false;
578
+ // Layer test, typically with the main camera
579
+ if (layers && obj.layers.test(layers) === false) allowExpanding = false;
579
580
  if (allowExpanding) {
580
581
  // Ignore things parented to the camera + ignore the camera
581
582
  if (ignore && Array.isArray(ignore) && ignore?.includes(obj)) return;
src/engine/engine_utils_format.ts CHANGED
@@ -38,7 +38,7 @@
38
38
  if (!ext?.length) {
39
39
  ext = urlobj.pathname.split(".").pop()?.toUpperCase();
40
40
  }
41
- console.debug("Use file extension to determine type: " + ext);
41
+ if (debug) console.debug("Use file extension to determine type: " + ext);
42
42
  switch (ext) {
43
43
  case "GLTF":
44
44
  return "gltf";
src/engine/engine_utils.ts CHANGED
@@ -554,9 +554,6 @@
554
554
  };
555
555
 
556
556
 
557
-
558
-
559
-
560
557
  declare global {
561
558
  interface NavigatorUAData {
562
559
  platform: string;
@@ -566,66 +563,145 @@
566
563
  }
567
564
  }
568
565
 
569
- let _isDesktop: boolean | undefined;
566
+ /**
567
+ * Utility function to detect certain device types (mobile, desktop) or browsers
568
+ */
569
+ export namespace DeviceUtilities {
570
+ let _isDesktop: boolean | undefined;
571
+ /** Is MacOS or Windows (and not hololens) */
572
+ export function isDesktop() {
573
+ if (_isDesktop !== undefined) return _isDesktop;
574
+ const ua = window.navigator.userAgent;
575
+ const standalone = /Windows|MacOS|Mac OS/.test(ua);
576
+ const isHololens = /Windows NT/.test(ua) && /Edg/.test(ua) && !/Win64/.test(ua);
577
+ return _isDesktop = standalone && !isHololens && !isiOS();
578
+ }
579
+ let _ismobile: boolean | undefined;
580
+ /** @returns `true` if it's a phone or tablet */
581
+ export function isMobileDevice() {
582
+ if (_ismobile !== undefined) return _ismobile;
583
+ if ((typeof window.orientation !== "undefined") || (navigator.userAgent.indexOf('IEMobile') !== -1)) {
584
+ return _ismobile = true;
585
+ }
586
+ return _ismobile = /iPhone|iPad|iPod|Android|IEMobile/i.test(navigator.userAgent);
587
+ }
588
+ /**
589
+ * @deprecated use {@link isiPad} instead
590
+ */
591
+ export function isIPad() {
592
+ return /iPad/.test(navigator.userAgent);
593
+ }
594
+ /**
595
+ * @returns `true` if it's an iPad by checking the navigator.userAgent
596
+ */
597
+ export function isiPad() {
598
+ return /iPad/.test(navigator.userAgent);
599
+ }
600
+ export function isAndroidDevice() {
601
+ return /Android/.test(navigator.userAgent);
602
+ }
603
+ /** @returns `true` if we're currently using the mozilla XR browser */
604
+ export function isMozillaXR() {
605
+ return /WebXRViewer\//i.test(navigator.userAgent);
606
+ }
607
+ let __isMacOs: boolean | undefined;
608
+ // https://developer.mozilla.org/en-US/docs/Web/API/Navigator/userAgentData
609
+ export function isMacOS() {
610
+ if (__isMacOs !== undefined) return __isMacOs;
611
+ if (navigator.userAgentData) {
612
+ // Use modern UA Client Hints API if available
613
+ return __isMacOs = navigator.userAgentData.platform === 'macOS';
614
+ } else {
615
+ // Fallback to user agent string parsing
616
+ const userAgent = navigator.userAgent.toLowerCase();
617
+ return __isMacOs = userAgent.includes('mac os x') || userAgent.includes('macintosh');
618
+ }
619
+ }
620
+ let __isiOS: boolean | undefined;
621
+ const iosDevices = ['iPad Simulator', 'iPhone Simulator', 'iPod Simulator', 'iPad', 'iPhone', 'iPod'];
622
+ /** @returns `true` for iOS devices like iPad, iPhone, iPod... */
623
+ export function isiOS() {
624
+ if (__isiOS !== undefined) return __isiOS;
625
+ return __isiOS = iosDevices.includes(navigator.platform)
626
+ // iPad on iOS 13 detection
627
+ || (navigator.userAgent.includes("Mac") && "ontouchend" in document)
628
+ }
629
+
630
+ /** @returns `true` if we're currently on safari */
631
+ export function isSafari() {
632
+ return /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
633
+ }
634
+
635
+ /** @returns true if we're currently running on quest */
636
+ export function isQuest() {
637
+ return navigator.userAgent.includes("OculusBrowser");
638
+ }
639
+
640
+ /** @returns `true` if the user allowed to use the microphone */
641
+ export async function microphonePermissionsGranted() {
642
+ try {
643
+ //@ts-ignore
644
+ const res = await navigator.permissions.query({ name: 'microphone' });
645
+ if (res.state === "denied") {
646
+ return false;
647
+ }
648
+ return true;
649
+ }
650
+ catch (err) {
651
+ console.error("Error querying `microphone` permissions.", err);
652
+ return false;
653
+ }
654
+ }
655
+
656
+ }
657
+
658
+
659
+
570
660
  /** Is MacOS or Windows (and not hololens) */
571
661
  export function isDesktop() {
572
- if(_isDesktop !== undefined) return _isDesktop;
573
- const ua = window.navigator.userAgent;
574
- const standalone = /Windows|MacOS|Mac OS/.test(ua);
575
- const isHololens = /Windows NT/.test(ua) && /Edg/.test(ua) && !/Win64/.test(ua);
576
- return _isDesktop = standalone && !isHololens && !isiOS();
662
+ return DeviceUtilities.isDesktop();
577
663
  }
578
664
 
579
- let _ismobile: boolean | undefined;
580
665
  /** @returns `true` if it's a phone or tablet */
581
666
  export function isMobileDevice() {
582
- if (_ismobile !== undefined) return _ismobile;
583
- if ((typeof window.orientation !== "undefined") || (navigator.userAgent.indexOf('IEMobile') !== -1)) {
584
- return _ismobile = true;
585
- }
586
- return _ismobile = /iPhone|iPad|iPod|Android|IEMobile/i.test(navigator.userAgent);
667
+ return DeviceUtilities.isMobileDevice();
587
668
  }
588
669
 
670
+ /** @deprecated use {@link isiPad} instead */
671
+ export function isIPad() {
672
+ return DeviceUtilities.isiPad();
673
+ }
674
+
675
+ export function isiPad() {
676
+ return DeviceUtilities.isiPad();
677
+ }
678
+
589
679
  export function isAndroidDevice() {
590
- return /Android/.test(navigator.userAgent);
680
+ return DeviceUtilities.isAndroidDevice();
591
681
  }
592
682
 
593
683
  /** @returns `true` if we're currently using the mozilla XR browser */
594
684
  export function isMozillaXR() {
595
- return /WebXRViewer\//i.test(navigator.userAgent);
685
+ return DeviceUtilities.isMozillaXR();
596
686
  }
597
687
 
598
- let __isMacOs: boolean | undefined;
599
688
  // https://developer.mozilla.org/en-US/docs/Web/API/Navigator/userAgentData
600
689
  export function isMacOS() {
601
- if (__isMacOs !== undefined) return __isMacOs;
602
- if (navigator.userAgentData) {
603
- // Use modern UA Client Hints API if available
604
- return __isMacOs = navigator.userAgentData.platform === 'macOS';
605
- } else {
606
- // Fallback to user agent string parsing
607
- const userAgent = navigator.userAgent.toLowerCase();
608
- return __isMacOs = userAgent.includes('mac os x') || userAgent.includes('macintosh');
609
- }
690
+ return DeviceUtilities.isMacOS();
610
691
  }
611
692
 
612
- let __isiOS: boolean | undefined;
613
- const iosDevices = ['iPad Simulator', 'iPhone Simulator', 'iPod Simulator', 'iPad', 'iPhone', 'iPod'];
614
693
  /** @returns `true` for iOS devices like iPad, iPhone, iPod... */
615
694
  export function isiOS() {
616
- if (__isiOS !== undefined) return __isiOS;
617
- return __isiOS = iosDevices.includes(navigator.platform)
618
- // iPad on iOS 13 detection
619
- || (navigator.userAgent.includes("Mac") && "ontouchend" in document)
695
+ return DeviceUtilities.isiOS();
620
696
  }
621
697
 
622
698
  /** @returns `true` if we're currently on safari */
623
699
  export function isSafari() {
624
- return /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
700
+ return DeviceUtilities.isSafari();
625
701
  }
626
702
 
627
703
  export function isQuest() {
628
- return navigator.userAgent.includes("OculusBrowser");
704
+ return DeviceUtilities.isQuest();
629
705
  }
630
706
 
631
707
  /** @returns `true` if the user allowed to use the microphone */
src/engine/engine.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import "./engine_hot_reload.js";
2
2
  import "./tests/test_utils.js";
3
3
 
4
- import { RGBAColor } from "../engine-components/js-extensions/RGBAColor.js";
5
4
  import * as engine_scenetools from "./engine_scenetools.js";
6
5
  import * as engine_setup from "./engine_setup.js";
6
+ import { RGBAColor } from "./js-extensions/RGBAColor.js";
7
7
 
8
8
  const engine : any = {
9
9
  ...engine_setup,
src/engine-components/ui/EventSystem.ts CHANGED
@@ -30,6 +30,9 @@
30
30
 
31
31
  declare type IComponentCanMaybeReceiveEvents = IPointerEventHandler & IComponent & { interactable?: boolean };
32
32
 
33
+ /**
34
+ * @category User Interface
35
+ */
33
36
  export class EventSystem extends Behaviour {
34
37
  private static _eventSystemMap = new Map<Context, EventSystem[]>();
35
38
 
src/engine-components/EventTrigger.ts CHANGED
@@ -8,12 +8,13 @@
8
8
  @serializable()
9
9
  eventID!: EventType;
10
10
  @serializable(EventList)
11
- callback: EventList = new EventList();
11
+ callback?: EventList;
12
12
  }
13
13
 
14
14
  /**
15
15
  * The EventTrigger component is used to trigger events when certain pointer events occur on the GameObject.
16
16
  * It implements the {@link IPointerEventHandler} interface and can be used to expose events to the user in the editor without writing code.
17
+ * @category Interactivity
17
18
  */
18
19
  export class EventTrigger extends Behaviour implements IPointerEventHandler {
19
20
 
@@ -26,7 +27,7 @@
26
27
  if (!this.triggers) return;
27
28
  for (const trigger of this.triggers) {
28
29
  if (trigger.eventID === type) {
29
- trigger.callback.invoke();
30
+ trigger.callback?.invoke();
30
31
  }
31
32
  }
32
33
  }
src/engine-components/Gizmos.ts CHANGED
@@ -6,7 +6,10 @@
6
6
  import { FrameEvent } from "../engine/engine_setup.js";
7
7
  import { Behaviour } from "./Component.js";
8
8
 
9
-
9
+ /**
10
+ * BoxGizmo is a component that displays a box around the object in the scene. It can optionally expand to the object's bounds.
11
+ * @category Helpers
12
+ */
10
13
  export class BoxGizmo extends Behaviour {
11
14
  @serializable()
12
15
  objectBounds: boolean = false;
src/engine-components/export/gltf/GltfExport.ts CHANGED
@@ -26,6 +26,9 @@
26
26
  sceneRoot?: Object3D;
27
27
  }
28
28
 
29
+ /**
30
+ * @category Asset Management
31
+ */
29
32
  export class GltfExport extends Behaviour {
30
33
 
31
34
  @serializable()
src/engine-components/ui/Graphic.ts CHANGED
@@ -21,6 +21,9 @@
21
21
  borderOpacity: 1,
22
22
  };
23
23
 
24
+ /**
25
+ * @category User Interface
26
+ */
24
27
  export class Graphic extends BaseUIComponent implements IGraphic, IRectTransformChangedReceiver {
25
28
 
26
29
  get isGraphic() { return true; }
@@ -254,6 +257,9 @@
254
257
  }
255
258
  }
256
259
 
260
+ /**
261
+ * @category User Interface
262
+ */
257
263
  export class MaskableGraphic extends Graphic {
258
264
 
259
265
  private _flippedObject = false;
src/engine-components/GridHelper.ts CHANGED
@@ -6,6 +6,7 @@
6
6
 
7
7
  /**
8
8
  * GridHelper is a component that allows to display a grid in the scene.
9
+ * @category Helpers
9
10
  */
10
11
  export class GridHelper extends Behaviour {
11
12
 
src/engine-components/GroundProjection.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import { ShaderMaterial, Texture } from "three";
2
2
  import { GroundedSkybox as GroundProjection } from 'three/examples/jsm/objects/GroundedSkybox.js';
3
3
 
4
+ import { Gizmos } from "../engine/engine_gizmos.js";
4
5
  import { serializable } from "../engine/engine_serialization_decorator.js";
5
- import { getBoundingBox, getWorldPosition, Graphics, setVisibleInCustomShadowRendering,setWorldPosition } from "../engine/engine_three_utils.js";
6
+ import { getBoundingBox, getTempVector, getWorldScale, Graphics, setVisibleInCustomShadowRendering,setWorldPosition } from "../engine/engine_three_utils.js";
6
7
  import { delayForFrames, getParam, Watch as Watch } from "../engine/engine_utils.js";
7
8
  import { Behaviour } from "./Component.js";
8
9
 
@@ -10,6 +11,7 @@
10
11
 
11
12
  /**
12
13
  * GroundProjectedEnv creates a ground projection of the current environment map.
14
+ * @category Rendering
13
15
  */
14
16
  export class GroundProjectedEnv extends Behaviour {
15
17
 
@@ -41,7 +43,7 @@
41
43
 
42
44
  /**
43
45
  * How far the camera that took the photo was above the ground. A larger value will magnify the downward part of the image.
44
- * @sefault 3
46
+ * @default 3
45
47
  */
46
48
  @serializable()
47
49
  set height(val: number) {
@@ -149,36 +151,52 @@
149
151
  if (!this.gameObject || this.destroyed) {
150
152
  return;
151
153
  }
154
+
155
+ let needsNewAutoFit = true;
152
156
  // offset here must be zero (and not .01) because the plane occlusion (when mesh tracking is active) is otherwise not correct
153
157
  const offset = 0;
154
158
  if (!this._projection || this.context.scene.environment !== this._lastEnvironment || this._height !== this._lastHeight || this._radius !== this._lastRadius) {
155
159
  if (debug)
156
160
  console.log("Create/Update Ground Projection", this.context.scene.environment.name);
157
161
  this._projection?.removeFromParent();
158
- this._projection = new GroundProjection(this.context.scene.environment, this._height, this.radius, 64);
162
+ if (!this._projection || (this.context.scene.environment !== this._lastEnvironment || this._lastHeight !== this._height || this._lastRadius !== this._radius)) {
163
+ try {
164
+ this._projection = new GroundProjection(this.context.scene.environment, this._height, this.radius, 64);
165
+ }
166
+ catch (e) {
167
+ console.error("Failed to enable GroundProjection for environment", e);
168
+ return;
169
+ }
170
+ }
171
+ else
172
+ needsNewAutoFit = false;
159
173
  this._projection.position.y = this._height - offset;
160
174
  this._projection.name = "GroundProjection";
161
175
  setVisibleInCustomShadowRendering(this._projection, false);
162
176
  }
177
+ else {
178
+ needsNewAutoFit = false;
179
+ }
163
180
 
164
- this._lastEnvironment = this.context.scene.environment;
165
- this._lastHeight = this._height;
166
- this._lastRadius = this._radius;
167
181
  if (!this._projection.parent)
168
182
  this.gameObject.add(this._projection);
169
183
 
170
- if (this.autoFit) {
184
+ if (this.autoFit && needsNewAutoFit) {
171
185
  // TODO: should also update the radius (?)
172
186
  this._projection.updateWorldMatrix(true, true);
173
- const scenebounds = getBoundingBox(this.context.scene.children, [this._projection]);
174
- const floor_y = scenebounds.min.y;
187
+ const box = getBoundingBox(this.context.scene.children, [this._projection]);
188
+
189
+ const floor_y = box.min.y;
175
190
  if (floor_y < Infinity) {
176
- const wp = getWorldPosition(this._projection);
177
- wp.x = scenebounds.min.x + (scenebounds.max.x - scenebounds.min.x) * .5;
178
- wp.y = floor_y + this._height - offset;
179
- wp.z = scenebounds.min.z + (scenebounds.max.z - scenebounds.min.z) * .5;
191
+ const wp = getTempVector();
192
+ wp.x = box.min.x + (box.max.x - box.min.x) * .5;
193
+ const scale = getWorldScale(this.gameObject).x;
194
+ wp.y = floor_y + (this._height * scale) - offset;
195
+ wp.z = box.min.z + (box.max.z - box.min.z) * .5;
180
196
  setWorldPosition(this._projection, wp);
181
197
  }
198
+
199
+ if (debug) Gizmos.DrawWireBox3(box, 0x00ff00, 5);
182
200
  }
183
201
 
184
202
  /* TODO realtime adjustments aren't possible anymore with GroundedSkybox (mesh generation)
@@ -187,10 +205,14 @@
187
205
  this.env.height = this._height;
188
206
  */
189
207
 
190
- if (this.context.scene.backgroundBlurriness > 0.001 || this._needsTextureUpdate) {
208
+ if (this.context.scene.backgroundBlurriness > 0.001 && this._needsTextureUpdate) {
191
209
  this.updateBlurriness();
192
210
  }
193
211
 
212
+ this._lastEnvironment = this.context.scene.environment;
213
+ this._lastHeight = this._height;
214
+ this._lastRadius = this._radius;
215
+
194
216
  this._needsTextureUpdate = false;
195
217
  }
196
218
 
src/engine-components/ui/Image.ts CHANGED
@@ -11,6 +11,9 @@
11
11
  rect?: { width: number, height: number };
12
12
  }
13
13
 
14
+ /**
15
+ * @category User Interface
16
+ */
14
17
  export class Image extends MaskableGraphic {
15
18
 
16
19
  set image(img: Texture | null) {
@@ -78,6 +81,9 @@
78
81
  }
79
82
  }
80
83
 
84
+ /**
85
+ * @category User Interface
86
+ */
81
87
  export class RawImage extends MaskableGraphic {
82
88
  @serializable(Texture)
83
89
  get mainTexture(): Texture | undefined {
src/engine/webcomponents/index.ts CHANGED
@@ -1,2 +1,2 @@
1
1
 
2
- export { NeedleMenu } from "./needle-menu.js";
2
+ export { NeedleMenu } from "./needle menu/needle-menu.js";
src/engine-components/ui/InputField.ts CHANGED
@@ -9,6 +9,9 @@
9
9
 
10
10
  const debug = getParam("debuginputfield");
11
11
 
12
+ /**
13
+ * @category User Interface
14
+ */
12
15
  export class InputField extends Behaviour implements IPointerEventHandler {
13
16
 
14
17
  get text(): string {
src/engine-components/ui/Layout.ts CHANGED
@@ -300,6 +300,9 @@
300
300
 
301
301
  }
302
302
 
303
+ /**
304
+ * @category User Interface
305
+ */
303
306
  export class VerticalLayoutGroup extends HorizontalOrVerticalLayoutGroup {
304
307
 
305
308
  protected get primaryAxis() {
@@ -308,6 +311,9 @@
308
311
 
309
312
  }
310
313
 
314
+ /**
315
+ * @category User Interface
316
+ */
311
317
  export class HorizontalLayoutGroup extends HorizontalOrVerticalLayoutGroup {
312
318
 
313
319
  protected get primaryAxis() {
@@ -316,6 +322,9 @@
316
322
 
317
323
  }
318
324
 
325
+ /**
326
+ * @category User Interface
327
+ */
319
328
  export class GridLayoutGroup extends LayoutGroup {
320
329
  protected onCalculateLayout() {
321
330
  }
src/engine-components/Light.ts CHANGED
@@ -88,6 +88,7 @@
88
88
  * The light can be set to cast shadows and the shadow type can be set to hard or soft shadows.
89
89
  * The light can be set to be baked or realtime.
90
90
  * The light can be set to be a main light which will be used for the main directional light in the scene.
91
+ * @category Rendering
91
92
  */
92
93
  export class Light extends Behaviour implements ILight {
93
94
 
src/engine-components/LODGroup.ts CHANGED
@@ -40,6 +40,7 @@
40
40
 
41
41
  /**
42
42
  * LODGroup allows to create a group of LOD levels for an object.
43
+ * @category Rendering
43
44
  */
44
45
  export class LODGroup extends Behaviour {
45
46
 
src/engine/webcomponents/logo-element.ts CHANGED
@@ -20,7 +20,6 @@
20
20
  min-width: fit-content;
21
21
  /* height: 100%; can not have height 100% because of align-items: stretch; in the parent */
22
22
  display: flex;
23
- margin: 0 0.3rem;
24
23
  }
25
24
 
26
25
  .wrapper {
@@ -36,6 +35,7 @@
36
35
  width: 95px;
37
36
  height: 100%;
38
37
  align-self: end;
38
+ margin-left: 0.6rem;
39
39
  }
40
40
  span {
41
41
  font-size: 1rem;
src/asap/needle-asap.ts CHANGED
@@ -30,6 +30,7 @@
30
30
  }
31
31
  }
32
32
 
33
+ return;
33
34
 
34
35
  // if (needleEngineHasLoaded()) {
35
36
  // if (debug) console.log("Skip asap, needle engine has already loaded.");
src/engine/webcomponents/needle menu/needle-menu-spatial.ts CHANGED
@@ -244,7 +244,7 @@
244
244
  lineHeight: 1,
245
245
  backgroundColor: 0xffffff,
246
246
  backgroundOpacity: .55,
247
- borderRadius: .04,
247
+ borderRadius: 1.0,
248
248
  whiteSpace: 'pre-wrap',
249
249
  flexDirection: 'row',
250
250
  alignItems: 'center',
src/engine/webcomponents/needle menu/needle-menu.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { Context } from "../../engine_context.js";
2
- import { hasCommercialLicense, hasProLicense, onLicenseCheckResultChanged } from "../../engine_license.js";
2
+ import { hasCommercialLicense, onLicenseCheckResultChanged } from "../../engine_license.js";
3
3
  import { isLocalNetwork } from "../../engine_networking_utils.js";
4
4
  import { getParam, isMobileDevice } from "../../engine_utils.js";
5
5
  import { onXRSessionStart, XRSessionEventArgs } from "../../xr/events.js";
@@ -314,10 +314,10 @@
314
314
  justify-content: center;
315
315
  align-items: stretch;
316
316
  gap: 0px;
317
- padding: 0 .3rem;
317
+ padding: 0 0rem;
318
318
  }
319
319
 
320
- .wrapper > *, .options > button, ::slotted(*) {
320
+ .wrapper > *, .options > button, .options > select, ::slotted(*) {
321
321
  position: relative;
322
322
  border: none;
323
323
  border-radius: 0;
@@ -326,6 +326,7 @@
326
326
  justify-content: center;
327
327
  align-items: center;
328
328
  max-height: 2.3rem;
329
+ max-width: 100%;
329
330
 
330
331
  /** basic font settings for all entries **/
331
332
  font-size: 1rem;
@@ -342,7 +343,7 @@
342
343
  outline: rgb(0 0 0 / 5%) 1px solid;
343
344
  border: 1px solid rgba(255, 255, 255, .1);
344
345
  box-shadow: 0px 7px 0.5rem 0px rgb(0 0 0 / 6%), inset 0px 0px 1.3rem rgba(0,0,0,.05);
345
- border-radius: 1.1999999999999993rem;
346
+ border-radius: 1.5rem;
346
347
  /**
347
348
  * to make nested background filter work
348
349
  * https://stackoverflow.com/questions/60997948/backdrop-filter-not-working-for-nested-elements-in-chrome
@@ -355,7 +356,7 @@
355
356
  top: 0;
356
357
  left: 0;
357
358
  z-index: -1;
358
- border-radius: 1.1999999999999993rem;
359
+ border-radius: 1.5rem;
359
360
  -webkit-backdrop-filter: blur(8px);
360
361
  backdrop-filter: blur(8px);
361
362
  }
@@ -369,6 +370,7 @@
369
370
  .options {
370
371
  display: flex;
371
372
  flex-direction: row;
373
+ align-items: center;
372
374
  }
373
375
 
374
376
  .options > *, ::slotted(*) {
@@ -376,12 +378,12 @@
376
378
  padding: .4rem .5rem;
377
379
  }
378
380
 
379
- :host .options > button, ::slotted(*) {
381
+ :host .options > *, ::slotted(*) {
380
382
  background: transparent;
381
383
  border: none;
382
384
  white-space: nowrap;
383
385
  transition: all 0.1s linear .02s;
384
- border-radius: 0.8rem;
386
+ border-radius: 1.5rem;
385
387
  user-select: none;
386
388
  }
387
389
  :host .options > *:hover, ::slotted(*:hover) {
@@ -441,6 +443,7 @@
441
443
  :host .has-options .logo {
442
444
  border-left: 1px solid rgba(40,40,40,.4);
443
445
  margin-left: 0.3rem;
446
+ margin-right: 0.5rem;
444
447
  }
445
448
 
446
449
  .logo > span {
@@ -501,6 +504,7 @@
501
504
  }
502
505
  .open .options, .open .foldout {
503
506
  display: flex;
507
+ justify-content: center;
504
508
  }
505
509
  .compact .wrapper {
506
510
  padding: 0;
@@ -564,7 +568,7 @@
564
568
  background: rgb(150,150,150);
565
569
  }
566
570
 
567
- .compact .options > * {
571
+ .compact .options > *, .compact .options > ::slotted(*) {
568
572
  font-size: 1.2rem;
569
573
  padding: .6rem .5rem;
570
574
  width: 100%;
@@ -575,17 +579,22 @@
575
579
  margin-left: 1rem;
576
580
  margin-bottom: .02rem;
577
581
  }
578
- .compact .options > button {
579
- display: flex;
580
- flex-basis: 100%;
581
- min-height: 3rem;
582
+ .compact .options {
583
+ /** e.g. if we have a very wide menu item like a select with long option names we don't want to overflow **/
584
+ max-width: 100%;
585
+
586
+ & > button, & > select {
587
+ display: flex;
588
+ flex-basis: 100%;
589
+ min-height: 3rem;
590
+ }
591
+ & > button.row2 {
592
+ //border: 1px solid red !important;
593
+ display: flex;
594
+ flex: 1;
595
+ flex-basis: 30%;
596
+ }
582
597
  }
583
- .compact .options > button.row2 {
584
- //border: 1px solid red !important;
585
- display: flex;
586
- flex: 1;
587
- flex-basis: 30%;
588
- }
589
598
 
590
599
  /** If there's really not enough space then just hide all options **/
591
600
  @media (max-width: 100px) or (max-height: 100px){
@@ -664,6 +673,7 @@
664
673
  globalThis.open("https://needle.tools", "_blank");
665
674
  });
666
675
 
676
+ try {
667
677
  // if the user has a license then we CAN hide the needle logo
668
678
  onLicenseCheckResultChanged(res => {
669
679
  if (res == true && hasCommercialLicense() && !debugNonCommercial) {
@@ -672,6 +682,9 @@
672
682
  this.#onSetLogoVisible(visible);
673
683
  }
674
684
  });
685
+ } catch (e) {
686
+ console.error("[Needle Menu] License check failed.", e);
687
+ }
675
688
 
676
689
  this.compactMenuButton.addEventListener("click", evt => {
677
690
  evt.preventDefault();
src/engine-components/NeedleMenu.ts CHANGED
@@ -1,10 +1,12 @@
1
+ import type { Context } from '../engine/engine_context.js';
1
2
  import { serializable } from '../engine/engine_serialization.js';
2
3
  import { isMobileDevice } from '../engine/engine_utils.js';
3
4
  import { Behaviour } from './Component.js';
4
5
 
5
6
  /**
6
- * Exposes options to editors to customize the Needle menu.
7
- * From code you can access the menu via `{@link Context.menu}`
7
+ * Exposes options to customize the built-in Needle Menu.
8
+ * From code, you can access the menu via {@link Context.menu}.
9
+ * @category User Interface
8
10
  **/
9
11
  export class NeedleMenu extends Behaviour {
10
12
 
src/engine/xr/NeedleXRController.ts CHANGED
@@ -161,6 +161,7 @@
161
161
  private _hasSelectEvent = false;
162
162
  get hasSelectEvent() { return this._hasSelectEvent; }
163
163
  private _isMxInk = false;
164
+ private _isMxInkFallback = false;
164
165
  private _isMetaQuestTouchController = false;
165
166
 
166
167
  /** Perform a hit test against the XR planes or meshes. shorthand for `xr.getHitTest(controller)`
@@ -439,7 +440,7 @@
439
440
 
440
441
  // HACK: offset for MX Ink on QuestOS v69 and less. This will hopefully not be required with OS v70+ anymore,
441
442
  // when the pen should have its own proper profile and correct ray space.
442
- if (this._isMxInk) {
443
+ if (this._isMxInk && !this._isMxInkFallback) {
443
444
  const offset = getTempVector(0.013, 0.000, -0.028).applyQuaternion(rayQuaternionRaw);
444
445
  rayPositionRaw.add(offset);
445
446
  this._rayPosition.add(offset);
@@ -761,8 +762,11 @@
761
762
  this.getMotionController = fetchProfileCall.then(res => {
762
763
 
763
764
  if (!this.connected) return null;
764
- if (this._isMxInk)
765
+ if (this._isMxInk && !res.assetPath) {
766
+ if (debug) console.log("Falling back to custom MX Ink model", res.profile, res.assetPath);
765
767
  res.assetPath = "https://cdn.needle.tools/static/models/controllers/logitech_vr_stylus_v1.3.1_grip_questos68.glb";
768
+ this._isMxInkFallback = true;
769
+ }
766
770
 
767
771
  this._motioncontroller = new MotionController(
768
772
  this.inputSource,
src/engine-components/Networking.ts CHANGED
@@ -8,6 +8,7 @@
8
8
 
9
9
  /**
10
10
  * The networking component is used to provide a websocket url to the networking system. It implements the {@link INetworkingWebsocketUrlProvider} interface.
11
+ * @category Networking
11
12
  */
12
13
  export class Networking extends Behaviour implements INetworkingWebsocketUrlProvider {
13
14
 
src/engine-components/utils/OpenURL.ts CHANGED
@@ -17,6 +17,7 @@
17
17
 
18
18
  /**
19
19
  * OpenURL behaviour opens a URL in a new tab or window.
20
+ * @category Interactivity
20
21
  */
21
22
  export class OpenURL extends Behaviour implements IPointerClickHandler {
22
23
 
src/engine-components/OrbitControls.ts CHANGED
@@ -46,6 +46,7 @@
46
46
  /** The OrbitControls component is used to control a camera using the [OrbitControls from three.js](https://threejs.org/docs/#examples/en/controls/OrbitControls) library.
47
47
  * The three OrbitControls object can be accessed via the `controls` property.
48
48
  * The object being controlled by the OrbitControls (usually the camera) can be accessed via the `controllerObject` property.
49
+ * @category Camera Controls
49
50
  */
50
51
  export class OrbitControls extends Behaviour implements ICameraController {
51
52
 
@@ -235,6 +236,12 @@
235
236
  private _cameraLerp01: number = 0;
236
237
  private _cameraLerpDuration: number = 0;
237
238
 
239
+ private _fovLerpActive: boolean = false;
240
+ private _fovLerpStartValue: number = 0;
241
+ private _fovLerpEndValue: number = 0;
242
+ private _fovLerp01: number = 0;
243
+ private _fovLerpDuration: number = 0;
244
+
238
245
  private _inputs: number = 0;
239
246
  private _enableTime: number = 0; // use to disable double click when double clicking on UI
240
247
  private _startedListeningToKeyEvents: boolean = false;
@@ -460,8 +467,7 @@
460
467
  this.setTargetFromRaycast();
461
468
  }
462
469
 
463
- if (this._lookTargetLerpActive || this._cameraLerpActive) {
464
-
470
+ if (this._lookTargetLerpActive || this._cameraLerpActive || this._fovLerpActive) {
465
471
  // lerp the camera
466
472
  if (this._cameraLerpActive && this._cameraObject) {
467
473
  this._cameraLerp01 += this.context.time.deltaTime / this._cameraLerpDuration;
@@ -488,6 +494,20 @@
488
494
  this._controls.target.lerpVectors(this._lookTargetStartPosition, this._lookTargetEndPosition, t);
489
495
  }
490
496
  }
497
+
498
+ // lerp the fov
499
+ if (this._fovLerpActive && this._cameraObject) {
500
+ const cam = this._cameraObject as PerspectiveCamera;
501
+ this._fovLerp01 += this.context.time.deltaTime / this._fovLerpDuration;
502
+ if (this._fovLerp01 >= 1) {
503
+ cam.fov = this._fovLerpEndValue;
504
+ this._fovLerpActive = false;
505
+ } else {
506
+ const t = Mathf.easeInOutCubic(this._fovLerp01);
507
+ cam.fov = Mathf.lerp(this._fovLerpStartValue, this._fovLerpEndValue, t);
508
+ }
509
+ cam.updateProjectionMatrix();
510
+ }
491
511
  }
492
512
 
493
513
 
@@ -511,7 +531,7 @@
511
531
  this._controls.maxPolarAngle = this.maxPolarAngle;
512
532
  // set the min/max zoom if it's not a free cam
513
533
  if (!freeCam) {
514
- if (this._camera?.cam?.type === "PerspectiveCamera") {
534
+ if (this._camera?.threeCamera?.type === "PerspectiveCamera") {
515
535
  this._controls.minDistance = this.minZoom;
516
536
  this._controls.maxDistance = this.maxZoom;
517
537
  this._controls.minZoom = 0;
@@ -602,7 +622,9 @@
602
622
  this._cameraEndPosition.copy(position);
603
623
  if (immediateOrDuration === true) {
604
624
  this._cameraLerpActive = false;
605
- this.controllerObject?.position.copy(this._cameraEndPosition);
625
+ if (this._cameraObject) {
626
+ this._cameraObject.position.copy(this._cameraEndPosition);
627
+ }
606
628
  }
607
629
  else if (this._cameraObject) {
608
630
  this._cameraLerpActive = true;
@@ -621,6 +643,26 @@
621
643
  this._cameraLerpActive = false;
622
644
  }
623
645
 
646
+ public setFieldOfView(fov: number | undefined, immediateOrDuration: boolean | number = false) {
647
+ if (!this._controls) return;
648
+ if (typeof fov !== "number") return;
649
+ const cam = this._camera?.threeCamera as PerspectiveCamera;
650
+ if (!cam) return;
651
+ if (immediateOrDuration === true) {
652
+ cam.fov = fov;
653
+ }
654
+ else {
655
+ this._fovLerpActive = true;
656
+ this._fovLerp01 = 0;
657
+ this._fovLerpStartValue = cam.fov;
658
+ this._fovLerpEndValue = fov;
659
+ if (typeof immediateOrDuration === "number") {
660
+ this._fovLerpDuration = immediateOrDuration;
661
+ }
662
+ else this._fovLerpDuration = this.targetLerpDuration;
663
+ }
664
+ }
665
+
624
666
  /** Moves the camera look-at target to a position smoothly.
625
667
  * @param position The position in world space to move the camera target to. If null the camera will stop lerping to the target.
626
668
  * @param immediateOrDuration If true the camera target will move immediately to the new position, otherwise it will lerp. If a number is passed in it will be used as the duration of the lerp.
@@ -760,9 +802,6 @@
760
802
  return;
761
803
  }
762
804
 
763
- if (!options) options = {}
764
- const { immediate = false, centerCamera = "y", cameraNearFar = "auto", fitOffset = 1.1 } = options;
765
-
766
805
  const camera = this._cameraObject as PerspectiveCamera;
767
806
  const controls = this._controls as ThreeOrbitControls | null;
768
807
 
@@ -771,13 +810,17 @@
771
810
  return;
772
811
  }
773
812
 
813
+ if (!options) options = {}
814
+ const { immediate = false, centerCamera = "y", cameraNearFar = "auto", fitOffset = 1.1, fov = camera?.fov } = options;
815
+
774
816
  const size = new Vector3();
775
817
  const center = new Vector3();
776
818
  // TODO would be much better to calculate the bounds in camera space instead of world space -
777
819
  // we would get proper view-dependant fit.
778
820
  // Right now it's independent from where the camera is actually looking from,
779
821
  // and thus we're just getting some maximum that will work for sure.
780
- const box = getBoundingBox(objects, undefined);
822
+ const box = getBoundingBox(objects, undefined, this._camera?.threeCamera?.layers);
823
+ const boxCopy = box.clone();
781
824
 
782
825
  camera.updateMatrixWorld();
783
826
  camera.updateProjectionMatrix();
@@ -800,7 +843,7 @@
800
843
  return;
801
844
  }
802
845
 
803
- const verticalFov = camera.fov;
846
+ const verticalFov = options.fov || camera.fov;
804
847
  const horizontalFov = 2 * Math.atan(Math.tan(verticalFov * Math.PI / 360 / 2) * camera.aspect) / Math.PI * 360;
805
848
  const fitHeightDistance = size.y / (2 * Math.atan(Math.PI * verticalFov / 360));
806
849
  const fitWidthDistance = size.x / (2 * Math.atan(Math.PI * horizontalFov / 360));
@@ -808,17 +851,18 @@
808
851
  const distance = fitOffset * Math.max(fitHeightDistance, fitWidthDistance) + size.z / 2;
809
852
 
810
853
  if (debugCameraFit) {
811
- console.log("Fit camera to objects", fitHeightDistance, fitWidthDistance, "distance", distance);
854
+ console.log("Fit camera to objects", {fitHeightDistance, fitWidthDistance, distance, verticalFov, horizontalFov});
812
855
  }
813
856
 
814
- controls.maxDistance = distance * 10;
815
- controls.minDistance = distance * 0.01;
857
+ this.maxZoom = distance * 10;
858
+ this.minZoom = distance * 0.01;
816
859
 
817
860
  const verticalOffset = 0.05;
818
861
 
819
862
  const lookAt = center.clone();
820
863
  lookAt.y -= size.y * verticalOffset;
821
864
  this.setLookTargetPosition(lookAt, immediate);
865
+ this.setFieldOfView(options.fov, immediate);
822
866
  this.autoTarget = false;
823
867
 
824
868
  if (cameraNearFar == undefined || cameraNearFar == "auto") {
@@ -829,8 +873,18 @@
829
873
  // TODO: this doesnt take the Camera component nearClipPlane into account
830
874
  camera.near = (distance / 100);
831
875
  camera.far = boundsMax + distance * 10;
876
+
877
+ // adjust maxZoom so that the ground projection radius is always inside
878
+ if (groundprojection) {
879
+ this.maxZoom = Math.max(Math.min(this.maxZoom, groundProjectionRadius * 0.5), distance);
880
+ }
832
881
  }
833
882
 
883
+ // ensure we're not clipping out of the current zoom level just because we're fitting
884
+ const currentZoom = controls.getDistance();
885
+ if (currentZoom < this.minZoom) this.minZoom = currentZoom * 0.9;
886
+ if (currentZoom > this.maxZoom) this.maxZoom = currentZoom * 1.1;
887
+
834
888
  camera.updateMatrixWorld();
835
889
  camera.updateProjectionMatrix();
836
890
 
@@ -842,30 +896,35 @@
842
896
  direction.normalize();
843
897
  direction.multiplyScalar(distance);
844
898
  if (centerCamera === "y")
845
- direction.y += -verticalOffset * 4 * size.y;
899
+ direction.y += -verticalOffset * 4 * distance;
846
900
 
901
+ let cameraLocalPosition = center.clone().sub(direction);
847
902
  if (camera.parent) {
848
- const cameraLocalPosition = camera.parent!.worldToLocal(center.clone().sub(direction));
849
- this.setCameraTargetPosition(cameraLocalPosition, immediate);
903
+ cameraLocalPosition = camera.parent.worldToLocal(cameraLocalPosition);
850
904
  }
851
- else console.error(`Can not fit camera ${camera.name} because it has no parent`)
852
-
853
- // setWorldPosition(camera, controls.target.clone().sub(direction));
854
-
905
+ this.setCameraTargetPosition(cameraLocalPosition, immediate);
906
+
855
907
  if (debugCameraFit) {
856
908
  const helper = new Box3Helper(box);
857
909
  this.context.scene.add(helper);
858
910
  setWorldRotation(helper, getWorldRotation(camera));
859
911
  setTimeout(() => {
860
912
  this.context.scene.remove(helper);
861
- }, 10000);
913
+ }, 10_000);
914
+ Gizmos.DrawWireBox3(boxCopy, 0x00ff00, 10);
862
915
 
863
916
  if (!this._haveAttachedKeyboardEvents) {
864
917
  this._haveAttachedKeyboardEvents = true;
865
918
  document.body.addEventListener("keydown", (e) => {
866
919
  if (e.code === "KeyF") {
867
- this.fitCamera({ objects, fitOffset, immediate });
920
+ // random fov for easier debugging of fov-based fitting
921
+ let fov: number | undefined = undefined;
922
+ if (this._cameraObject instanceof PerspectiveCamera) fov = (Math.random() * Math.random()) * 170 + 10;
923
+ this.fitCamera({ objects, fitOffset, immediate: false, fov });
868
924
  }
925
+ if (e.code === "KeyV") {
926
+ if (this._cameraObject instanceof PerspectiveCamera) this._cameraObject.fov = 60;
927
+ }
869
928
  });
870
929
  }
871
930
  }
@@ -874,10 +933,6 @@
874
933
  }
875
934
 
876
935
  private _haveAttachedKeyboardEvents: boolean = false;
877
-
878
- // private onPositionDrag(){
879
-
880
- // }
881
936
  }
882
937
 
883
938
 
@@ -900,4 +955,5 @@
900
955
  /** If set to "y" the camera will be centered in the y axis */
901
956
  centerCamera?: "none" | "y",
902
957
  cameraNearFar?: "keep" | "auto",
958
+ fov?: number,
903
959
  }
src/engine-components/particlesystem/ParticleSystem.ts CHANGED
@@ -641,9 +641,13 @@
641
641
  }
642
642
 
643
643
  /**
644
- * The ParticleSystem component efficiently handles the rendering of particles.
644
+ * The ParticleSystem component efficiently handles the motion and rendering of many individual particles.
645
645
  *
646
646
  * You can add custom behaviours to the particle system to fully customize the behaviour of the particles. See {@link ParticleSystemBaseBehaviour} and {@link ParticleSystem.addBehaviour} for more information.
647
+ *
648
+ * Needle Engine uses [three.quarks](https://github.com/Alchemist0823/three.quarks) under the hood to handle particles.
649
+ *
650
+ * @category Rendering
647
651
  */
648
652
  export class ParticleSystem extends Behaviour implements IParticleSystem {
649
653
 
@@ -893,7 +897,7 @@
893
897
  this._particleSystem.behaviors.length = 0;
894
898
  return true;
895
899
  }
896
- /** Get the particlesystem behaviours. This can be used to fully customize the behaviour of the particles. */
900
+ /** Get the underlying three.quarks particle system behaviours. This can be used to fully customize the behaviour of the particles. */
897
901
  get behaviours(): Behavior[] | null {
898
902
  if (!this._particleSystem) return null;
899
903
  return this._particleSystem.behaviors;
src/engine-components/postprocessing/Effects/Pixelation.ts CHANGED
@@ -5,6 +5,9 @@
5
5
  import { VolumeParameter } from "../VolumeParameter.js";
6
6
  import { registerCustomEffectType } from "../VolumeProfile.js";
7
7
 
8
+ /**
9
+ * @category Effects
10
+ */
8
11
  export class PixelationEffect extends PostProcessingEffect {
9
12
  get typeName(): string {
10
13
  return "PixelationEffect";
src/engine-components/timeline/PlayableDirector.ts CHANGED
@@ -56,6 +56,7 @@
56
56
  * The PlayableDirector component is the main component to control timelines in needle engine.
57
57
  * It is used to play, pause, stop and evaluate timelines.
58
58
  * Assign a TimelineAsset to the `playableAsset` property to start playing a timeline.
59
+ * @category Animation and Sequencing
59
60
  */
60
61
  export class PlayableDirector extends Behaviour {
61
62
 
@@ -278,6 +279,13 @@
278
279
  }
279
280
 
280
281
  /**
282
+ * @returns all animation tracks of the timeline
283
+ */
284
+ get animationTracks() {
285
+ return this._animationTracks;
286
+ }
287
+
288
+ /**
281
289
  * @returns all audio tracks of the timeline
282
290
  */
283
291
  get audioTracks(): Tracks.AudioTrackHandler[] {
src/engine-components/PlayerColor.ts CHANGED
@@ -9,6 +9,7 @@
9
9
  /**
10
10
  * PlayerColor assigns a unique color for each user in the room to the object it is attached to.
11
11
  * The color is generated based on the user's ID.
12
+ * @category Networking
12
13
  */
13
14
  export class PlayerColor extends Behaviour {
14
15
 
src/engine-components/postprocessing/PostProcessingEffect.ts CHANGED
@@ -47,6 +47,8 @@
47
47
  * }
48
48
  * registerCustomEffectType("Antialiasing", Antialiasing)
49
49
  * ```
50
+ *
51
+ * @category Effects
50
52
  */
51
53
  export abstract class PostProcessingEffect extends Component implements IEffectProvider, IEditorModification {
52
54
 
src/engine-components/ReflectionProbe.ts CHANGED
@@ -13,6 +13,9 @@
13
13
  const $reflectionProbeKey = Symbol("reflectionProbeKey");
14
14
  const $originalMaterial = Symbol("original material");
15
15
 
16
+ /**
17
+ * @category Rendering
18
+ */
16
19
  export class ReflectionProbe extends Behaviour {
17
20
 
18
21
  private static _probes: Map<Context, ReflectionProbe[]> = new Map();
@@ -31,10 +34,13 @@
31
34
  return probe;
32
35
  }
33
36
  }
34
- else if (probe.isInBox(object, undefined)) {
37
+ /*
38
+ // TODO not supported right now, as we'd have to pass the ReflectionProbe scale through as well.
39
+ else if (probe.isInBox(object)) {
35
40
  if (debug) console.log("Found reflection probe", object.name, probe.name);
36
41
  return probe;
37
42
  }
43
+ */
38
44
  }
39
45
  }
40
46
  }
@@ -69,8 +75,8 @@
69
75
 
70
76
  private _boxHelper?: BoxHelperComponent;
71
77
 
72
- private isInBox(obj: Object3D, scaleFactor?: number) {
73
- return this._boxHelper?.isInBox(obj, scaleFactor);
78
+ private isInBox(obj: Object3D) {
79
+ return this._boxHelper?.isInBox(obj);
74
80
  }
75
81
 
76
82
  constructor() {
@@ -82,7 +88,7 @@
82
88
  }
83
89
 
84
90
  awake() {
85
- this._boxHelper = this.gameObject.addNewComponent(BoxHelperComponent) as BoxHelperComponent;
91
+ this._boxHelper = this.gameObject.addComponent(BoxHelperComponent) as BoxHelperComponent;
86
92
  this._boxHelper.updateBox(true);
87
93
  if (debug)
88
94
  this._boxHelper.showHelper(0x555500, true);
src/engine/codegen/register_types.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /* eslint-disable */
2
2
  import { TypeStore } from "./../engine_typestore.js"
3
-
3
+
4
4
  // Import types
5
5
  import { __Ignore } from "../../engine-components/codegen/components.js";
6
6
  import { ActionBuilder } from "../../engine-components/export/usdz/extensions/behavior/BehavioursBuilder.js";
@@ -220,7 +220,7 @@
220
220
  import { XRFlag } from "../../engine-components/webxr/XRFlag.js";
221
221
  import { XRRig } from "../../engine-components/webxr/WebXRRig.js";
222
222
  import { XRState } from "../../engine-components/webxr/XRFlag.js";
223
-
223
+
224
224
  // Register types
225
225
  TypeStore.add("__Ignore", __Ignore);
226
226
  TypeStore.add("ActionBuilder", ActionBuilder);
src/engine-components/Renderer.ts CHANGED
@@ -197,6 +197,9 @@
197
197
 
198
198
  }
199
199
 
200
+ /**
201
+ * @category Rendering
202
+ */
200
203
  export class Renderer extends Behaviour implements IRenderer {
201
204
 
202
205
  /** Enable or disable instancing for an object. This will create a Renderer component if it does not exist yet.
src/engine-components/RendererInstancing.ts CHANGED
@@ -314,7 +314,7 @@
314
314
 
315
315
  // console.log(geometry.name, geometry.uuid);
316
316
 
317
- // if (!this.validateGeometry(geometry)) return false;
317
+ if (!this.validateGeometry(geometry)) return false;
318
318
 
319
319
  // const validationMethod = this.inst["_validateGeometry"];
320
320
  // if (!validationMethod) throw new Error("InstancedMesh does not have a _validateGeometry method");
@@ -411,12 +411,6 @@
411
411
  add(handle: InstanceHandle) {
412
412
  const geo = handle.object.geometry as BufferGeometry;
413
413
 
414
- if (!this.validateGeometry(geo)) {
415
- if (debugInstancing) console.error("Cannot add instance, invalid geometry", this.name, geo);
416
- else console.warn("Cannot add instance, invalid geometry: " + handle.name);
417
- return false;
418
- }
419
-
420
414
  if (this.mustGrow(geo)) {
421
415
  if (this.allowResize) {
422
416
  this.grow(geo);
@@ -501,7 +495,8 @@
501
495
  continue;
502
496
  }
503
497
  if (!geometry.hasAttribute(attributeName)) {
504
- console.warn(`BatchedMesh: Added geometry missing "${attributeName}". All geometries must have consistent attributes.`);
498
+ if (isDevEnvironment())
499
+ console.warn(`BatchedMesh: Added geometry missing "${attributeName}". All geometries must have consistent attributes.`);
505
500
  // geometry.setAttribute(attributeName, batchGeometry.getAttribute(attributeName).clone());
506
501
  return false;
507
502
  // throw new Error(`BatchedMesh: Added geometry missing "${attributeName}". All geometries must have consistent attributes.`);
@@ -517,6 +512,7 @@
517
512
  }
518
513
 
519
514
  private markNeedsUpdate() {
515
+ if (debugInstancing) console.warn("Marking instanced mesh dirty", this.name);
520
516
  this._needUpdateBounds = true;
521
517
  // this.inst.instanceMatrix.needsUpdate = true;
522
518
  }
src/engine-components/RigidBody.ts CHANGED
@@ -133,7 +133,8 @@
133
133
  }
134
134
 
135
135
  /**
136
- * A Rigidbody is used together with a Collider to create physical interactions between objects in the scene.
136
+ * A Rigidbody is used together with a Collider to create physical interactions between objects in the scene.
137
+ * @category Physics
137
138
  */
138
139
  export class Rigidbody extends Behaviour implements IRigidbody {
139
140
 
src/engine-components/SceneSwitcher.ts CHANGED
@@ -104,6 +104,7 @@
104
104
  * });
105
105
  * ```
106
106
  *
107
+ * @category Asset Management
107
108
  */
108
109
  export class SceneSwitcher extends Behaviour {
109
110
 
src/engine-components/ScreenCapture.ts CHANGED
@@ -70,6 +70,7 @@
70
70
  * By default the component will start sharing the screen when the user clicks on the object this component is attached to. You can set {@link device} This behaviour can be disabled by setting `allowStartOnClick` to false.
71
71
  * It is also possible to start the stream manually from your code by calling the {@link share} method.
72
72
  *
73
+ * @category Networking
73
74
  */
74
75
  export class ScreenCapture extends Behaviour implements IPointerClickHandler {
75
76
 
src/engine-components/postprocessing/Effects/ScreenspaceAmbientOcclusion.ts CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  /** Screenspace Ambient Occlusion post-processing effect.
10
10
  * We recommend using ScreenSpaceAmbientOcclusionN8 instead.
11
- * @category Postprocessing
11
+ * @category Effects
12
12
  */
13
13
  export class ScreenSpaceAmbientOcclusion extends PostProcessingEffect {
14
14
 
src/engine-components/postprocessing/Effects/ScreenspaceAmbientOcclusionN8.ts CHANGED
@@ -19,7 +19,7 @@
19
19
  }
20
20
 
21
21
  /** Screen Space Ambient Occlusion (SSAO) effect.
22
- * @category Postprocessing
22
+ * @category Effects
23
23
  * @link [N8AO documentation](https://github.com/N8python/n8ao)
24
24
  */
25
25
  export class ScreenSpaceAmbientOcclusionN8 extends PostProcessingEffect {
src/engine-components/ShadowCatcher.ts CHANGED
@@ -20,6 +20,7 @@
20
20
  /**
21
21
  * ShadowCatcher can be added to an Object3D to make it render shadows (or light) in the scene. It can also be used to create a shadow mask, or to occlude light.
22
22
  * If the GameObject is a Mesh, it will apply a shadow-catching material to it - otherwise it will create a quad with the shadow-catching material.
23
+ * @category Rendering
23
24
  */
24
25
  export class ShadowCatcher extends Behaviour {
25
26
 
src/engine-components/postprocessing/Effects/Sharpening.ts CHANGED
@@ -4,6 +4,9 @@
4
4
  import { serializable } from "../../../engine/engine_serialization.js";
5
5
  import { PostProcessingEffect } from "../PostProcessingEffect.js";
6
6
 
7
+ /**
8
+ * @category Effects
9
+ */
7
10
  export class SharpeningEffect extends PostProcessingEffect {
8
11
 
9
12
  get typeName() {
src/engine-components/SmoothFollow.ts CHANGED
@@ -9,6 +9,7 @@
9
9
  /**
10
10
  * SmoothFollow makes the {@link Object3D} (`GameObject`) smoothly follow another target {@link Object3D}.
11
11
  * It can follow the target's position, rotation, or both.
12
+ * @category Interactivity
12
13
  */
13
14
  export class SmoothFollow extends Behaviour {
14
15
 
src/engine-components/SpatialTrigger.ts CHANGED
@@ -16,6 +16,9 @@
16
16
  return layer1.test(layer2);
17
17
  }
18
18
 
19
+ /**
20
+ * @category Interactivity
21
+ */
19
22
  export class SpatialTriggerReceiver extends Behaviour {
20
23
 
21
24
  // currently Layers in unity but maybe this should be a string or plane number? Or should it be a bitmask to allow receivers use multiple triggers?
@@ -78,6 +81,10 @@
78
81
  }
79
82
  }
80
83
 
84
+ /**
85
+ * A trigger that can be used to detect if an object is inside a box.
86
+ * @category Interactivity
87
+ */
81
88
  export class SpatialTrigger extends Behaviour {
82
89
 
83
90
  static triggers: SpatialTrigger[] = [];
src/engine-components/SpectatorCamera.ts CHANGED
@@ -24,6 +24,9 @@
24
24
 
25
25
  const debug = getParam("debugspectator");
26
26
 
27
+ /**
28
+ * @category Networking
29
+ */
27
30
  export class SpectatorCamera extends Behaviour {
28
31
 
29
32
  cam: Camera | null = null;
@@ -441,7 +444,7 @@
441
444
  constructor(context: Context, spectator: SpectatorCamera) {
442
445
  this.context = context;
443
446
  this.spectator = spectator;
444
- console.log("Click other avatars or cameras to follow them. Press ESC to exit spectator mode.");
447
+ console.log("[Spectator Camera] Click other avatars or cameras to follow them. Press ESC to exit spectator mode.");
445
448
  this.context.domElement.addEventListener("keydown", (evt) => {
446
449
  if(!this.spectator.useKeys) return;
447
450
  const key = evt.key;
src/engine-components/SpriteRenderer.ts CHANGED
@@ -83,6 +83,7 @@
83
83
 
84
84
  /**
85
85
  * A sprite is a mesh that represents a 2D image
86
+ * @category Rendering
86
87
  */
87
88
  export class Sprite {
88
89
 
src/engine-components/SyncedCamera.ts CHANGED
@@ -63,6 +63,7 @@
63
63
  /**
64
64
  * SyncedCamera is a component that syncs the camera position and rotation of all users in the room.
65
65
  * A prefab can be set to represent the remote cameras visually in the scene.
66
+ * @category Networking
66
67
  */
67
68
  export class SyncedCamera extends Behaviour {
68
69
 
src/engine-components/SyncedRoom.ts CHANGED
@@ -32,6 +32,8 @@
32
32
  * const myObject = new Object3D();
33
33
  * myObject.addComponent(SyncedRoom, { joinRandomRoom: true, roomPrefix: "myApp_" });
34
34
  * ```
35
+ *
36
+ * @category Networking
35
37
  */
36
38
  export class SyncedRoom extends Behaviour {
37
39
 
@@ -176,7 +178,7 @@
176
178
 
177
179
  if (this.requireRoomParameter && !hasRoomParameter) {
178
180
  if (debug || isDevEnvironment())
179
- console.warn("SyncedRoom: Missing required room parameter \"" + this.urlParameterName + "\" in url - will not connect.\nTo allow joining a room without a query parameter you can set \"requireRoomParameter\" to false.");
181
+ console.warn("[SyncedRoom] Missing required room parameter \"" + this.urlParameterName + "\" in url - will not connect.\nTo allow joining a room without a query parameter you can set \"requireRoomParameter\" to false.");
180
182
  return false;
181
183
  }
182
184
 
@@ -189,7 +191,7 @@
189
191
  this.roomName = this._roomPrefix + this.roomName;
190
192
 
191
193
  if (this.roomName.length <= 0) {
192
- console.warn("SyncedRoom: Room name is not set so we can not join a networked room.\nPlease choose one of the following options to fix this:\nA) Set a room name in the SyncedRoom component\nB) Set a room name in the URL parameter \"?" + this.urlParameterName + "=my_room\"\nC) Set \"joinRandomRoom\" to true");
194
+ console.warn("[SyncedRoom] Room name is not set so we can not join a networked room.\nPlease choose one of the following options to fix this:\nA) Set a room name in the SyncedRoom component\nB) Set a room name in the URL parameter \"?" + this.urlParameterName + "=my_room\"\nC) Set \"joinRandomRoom\" to true");
193
195
  return false;
194
196
  }
195
197
 
src/engine-components/SyncedTransform.ts CHANGED
@@ -51,6 +51,7 @@
51
51
 
52
52
  /**
53
53
  * SyncedTransform is a behaviour that syncs the transform of a game object over the network.
54
+ * @category Networking
54
55
  */
55
56
  export class SyncedTransform extends Behaviour {
56
57
 
src/engine-components/ui/Text.ts CHANGED
@@ -39,6 +39,9 @@
39
39
  BoldAndItalic = 3,
40
40
  }
41
41
 
42
+ /**
43
+ * @category User Interface
44
+ */
42
45
  export class Text extends Graphic implements IHasAlphaFactor, ICanvasEventReceiver {
43
46
 
44
47
  @serializable()
src/engine-components/export/usdz/ThreeUSDZExporter.ts CHANGED
@@ -664,7 +664,8 @@
664
664
  const options = {
665
665
  allBehaviorTargets,
666
666
  debug: false,
667
- boneReparentings: allReparentingObjects
667
+ boneReparentings: allReparentingObjects,
668
+ quickLookCompatible: context.quickLookCompatible,
668
669
  };
669
670
  if (this.debug) logUsdHierarchy(context.document, "Hierarchy BEFORE pruning", options);
670
671
  prune( context.document, options );
@@ -927,7 +928,8 @@
927
928
  function prune ( object: USDObject, options : {
928
929
  allBehaviorTargets: Set<string>,
929
930
  debug: boolean,
930
- boneReparentings: Set<string>
931
+ boneReparentings: Set<string>,
932
+ quickLookCompatible: boolean,
931
933
  } ) {
932
934
 
933
935
  let allChildsWerePruned = true;
@@ -956,7 +958,7 @@
956
958
  const isBehaviorSourceOrTarget = options.allBehaviorTargets.has(object.uuid);
957
959
 
958
960
  // check if this object has any material or geometry
959
- const isVisible = object.geometry || object.material || object.camera || object.skinnedMesh || false;
961
+ const isVisible = object.geometry || object.material || (object.camera && !options.quickLookCompatible) || object.skinnedMesh || false;
960
962
 
961
963
  // check if this object is part of any reparenting
962
964
  const isBoneReparenting = options.boneReparentings.has(object.uuid);
@@ -1450,7 +1452,7 @@
1450
1452
  if (isSkinnedMesh)
1451
1453
  _apiSchemas.push("SkelBindingAPI");
1452
1454
  }
1453
- else if ( camera )
1455
+ else if ( camera && !context.quickLookCompatible)
1454
1456
  writer.beginBlock( `def Camera "${name}"`, "(", false );
1455
1457
  else if ( model.type !== undefined)
1456
1458
  writer.beginBlock( `def ${model.type} "${name}"` );
@@ -1514,7 +1516,7 @@
1514
1516
  if (model.visibility !== undefined)
1515
1517
  writer.appendLine(`token visibility = "${model.visibility}"`);
1516
1518
 
1517
- if ( camera ) {
1519
+ if ( camera && !context.quickLookCompatible) {
1518
1520
 
1519
1521
  if ( 'isOrthographicCamera' in camera && camera.isOrthographicCamera ) {
1520
1522
 
src/engine-components/postprocessing/Effects/TiltShiftEffect.ts CHANGED
@@ -5,7 +5,9 @@
5
5
  import { VolumeParameter } from "../VolumeParameter.js";
6
6
  import { registerCustomEffectType } from "../VolumeProfile.js";
7
7
 
8
-
8
+ /**
9
+ * @category Effects
10
+ */
9
11
  export class TiltShiftEffect extends PostProcessingEffect {
10
12
  get typeName(): string {
11
13
  return "TiltShiftEffect";
src/engine-components/timeline/TimelineTracks.ts CHANGED
@@ -136,15 +136,24 @@
136
136
 
137
137
  // TODO: add support for clip clamp modes (loop, pingpong, clamp)
138
138
  export class AnimationTrackHandler extends TrackHandler {
139
+ /** @internal */
139
140
  models: Array<Models.ClipModel> = [];
141
+ /** @internal */
140
142
  trackOffset?: Models.TrackOffset;
141
143
 
144
+ /** The object that is being animated. */
142
145
  target?: Object3D;
143
146
  /** The AnimationMixer, should be shared with the animator if an animator is bound */
144
147
  mixer?: AnimationMixer;
145
148
  clips: Array<AnimationClip> = [];
146
149
  actions: Array<AnimationAction> = [];
147
150
 
151
+ /**
152
+ * You can use the weight to blend the timeline animation tracks with multiple animation tracks on the same object.
153
+ * @default 1
154
+ */
155
+ weight: number = 1;
156
+
148
157
  /** holds data/info about clips differences */
149
158
  private _actionOffsets: Array<AnimationClipOffsetData> = [];
150
159
  private _didBind: boolean = false;
@@ -351,7 +360,7 @@
351
360
 
352
361
  if (isActive) {
353
362
  // const clip = this.clips[i];
354
- let weight = 1;
363
+ let weight = this.weight;
355
364
  weight *= this.evaluateWeight(time, i, this.models, isActive);
356
365
  weight *= this.director.weight;
357
366
 
src/engine-components/postprocessing/Effects/Tonemapping.ts CHANGED
@@ -63,7 +63,9 @@
63
63
  }
64
64
  }
65
65
 
66
-
66
+ /**
67
+ * @category Effects
68
+ */
67
69
  export class ToneMappingEffect extends PostProcessingEffect {
68
70
 
69
71
  get typeName() {
src/engine-components/TransformGizmo.ts CHANGED
@@ -9,6 +9,7 @@
9
9
 
10
10
  /**
11
11
  * TransformGizmo is a component that displays a gizmo for transforming the object in the scene.
12
+ * @category Helpers
12
13
  */
13
14
  export class TransformGizmo extends Behaviour {
14
15
 
src/engine-components/export/usdz/USDZExporter.ts CHANGED
@@ -9,6 +9,8 @@
9
9
  import { WebXRButtonFactory } from "../../../engine/webcomponents/WebXRButtons.js";
10
10
  import { InternalUSDZRegistry } from "../../../engine/xr/usdz.js"
11
11
  import { Behaviour, GameObject } from "../../Component.js";
12
+ import { ContactShadows } from "../../ContactShadows.js";
13
+ import { GroundProjectedEnv } from "../../GroundProjection.js";
12
14
  import { Renderer } from "../../Renderer.js"
13
15
  import { SpriteRenderer } from "../../SpriteRenderer.js";
14
16
  import { WebARSessionRoot } from "../../webxr/WebARSessionRoot.js";
@@ -61,6 +63,7 @@
61
63
  * usdz.autoExportAudioSources = true;
62
64
  * usdz.exportAsync();
63
65
  * ```
66
+ * @category XR
64
67
  */
65
68
  export class USDZExporter extends Behaviour {
66
69
 
@@ -387,12 +390,16 @@
387
390
  exporter.debug = debug;
388
391
  exporter.pruneUnusedNodes = !debugUsdzPruning;
389
392
  exporter.keepObject = (object) => {
393
+ let keep = true;
390
394
  // This explicitly removes geometry and material data from disabled renderers.
391
395
  // Note that this is different to the object itself being active –
392
396
  // here, we have an active object with a disabled renderer.
393
- const renderer = GameObject.getComponent(object, Renderer)
394
- if (renderer && !renderer.enabled) return false;
395
- return true;
397
+ const renderer = GameObject.getComponent(object, Renderer);
398
+ if (renderer && !renderer.enabled) keep = false;
399
+ if (keep && GameObject.getComponentInParent(object, ContactShadows)) keep = false;
400
+ if (keep && GameObject.getComponentInParent(object, GroundProjectedEnv)) keep = false;
401
+ if (debug && !keep) console.log("USDZExporter: Discarding object", object);
402
+ return keep;
396
403
  }
397
404
 
398
405
  // Collect invisible objects so that we can disable them if
src/engine-components/VideoPlayer.ts CHANGED
@@ -54,6 +54,7 @@
54
54
  * playOnAwake: true,
55
55
  * });
56
56
  * ```
57
+ * @category Multimedia
57
58
  */
58
59
  export class VideoPlayer extends Behaviour {
59
60
 
src/engine-components/postprocessing/Effects/Vignette.ts CHANGED
@@ -5,7 +5,9 @@
5
5
  import { VolumeParameter } from "../VolumeParameter.js";
6
6
  import { registerCustomEffectType } from "../VolumeProfile.js";
7
7
 
8
-
8
+ /**
9
+ * @category Effects
10
+ */
9
11
  export class Vignette extends PostProcessingEffect {
10
12
  get typeName(): string {
11
13
  return "Vignette";
src/engine-components/Voip.ts CHANGED
@@ -14,6 +14,7 @@
14
14
  /**
15
15
  * The Voice over IP component (VoIP) allows you to send and receive audio streams to other users in the same networked room.
16
16
  * It requires a networking connection to be working (e.g. by having an active SyncedRoom component in the scene or by connecting to a room manually).
17
+ * @category Networking
17
18
  */
18
19
  export class Voip extends Behaviour {
19
20
 
src/engine-components/postprocessing/Volume.ts CHANGED
@@ -39,6 +39,9 @@
39
39
  * pixelation.granularity.value = 10;
40
40
  * volume.addEffect(pixelation);
41
41
  * ```
42
+ *
43
+ * @category Rendering
44
+ * @category Effects
42
45
  */
43
46
  export class Volume extends Behaviour implements IEditorModificationReceiver, IPostProcessingManager {
44
47
 
src/engine-components/webxr/WebARCameraBackground.ts CHANGED
@@ -145,7 +145,7 @@
145
145
  // discussion on exactly this:
146
146
  // https://discourse.threejs.org/t/using-a-webgltexture-as-texture-for-three-js/46245/8
147
147
  // HACK from https://stackoverflow.com/a/55084367 to inject a custom texture into three.js
148
- const texProps = this.context.renderer.properties.get(this.threeTexture);
148
+ const texProps = this.context.renderer.properties.get(this.threeTexture) as { __webglTexture: WebGLTexture | null };
149
149
  texProps.__webglTexture = glImage;
150
150
 
151
151
  if (this.backgroundPlane) {
src/engine-components/webxr/WebARSessionRoot.ts CHANGED
@@ -29,6 +29,8 @@
29
29
  * console.log("Scene has been placed in AR");
30
30
  * });
31
31
  * ```
32
+ *
33
+ * @category XR
32
34
  */
33
35
  export class WebARSessionRoot extends Behaviour {
34
36
 
src/engine-components/webxr/WebXR.ts CHANGED
@@ -26,6 +26,7 @@
26
26
  /**
27
27
  * WebXR component to enable VR, AR and Quicklook on iOS in your scene.
28
28
  * It provides a simple wrapper around the {@link NeedleXRSession} API and adds some additional features like creating buttons or enabling default movement behaviour.
29
+ * @category XR
29
30
  */
30
31
  export class WebXR extends Behaviour {
31
32
 
src/engine-components/webxr/WebXRImageTracking.ts CHANGED
@@ -226,6 +226,9 @@
226
226
  }
227
227
  }
228
228
 
229
+ /**
230
+ * @category XR
231
+ */
229
232
  export class WebXRImageTracking extends Behaviour {
230
233
 
231
234
  @serializable(WebXRImageTrackingModel)
src/engine-components/webxr/WebXRPlaneTracking.ts CHANGED
@@ -49,6 +49,7 @@
49
49
 
50
50
  /**
51
51
  * Use this component to track planes and meshes in the real world when in immersive-ar (e.g. on Oculus Quest).
52
+ * @category XR
52
53
  */
53
54
  export class WebXRPlaneTracking extends Behaviour {
54
55
 
src/engine-components/webxr/WebXRRig.ts CHANGED
@@ -13,6 +13,7 @@
13
13
  /**
14
14
  * A user in XR (VR or AR) is parented to an XR rig during the session.
15
15
  * When moving through the scene the rig is moved instead of the user.
16
+ * @category XR
16
17
  */
17
18
  export class XRRig extends Behaviour implements IXRRig {
18
19
 
src/engine-components/webxr/controllers/XRControllerFollow.ts CHANGED
@@ -7,7 +7,10 @@
7
7
  import { Behaviour } from "../../Component.js";
8
8
 
9
9
 
10
- /** Add this script to an object and set `side` to make the object follow a specific controller */
10
+ /**
11
+ * Add this script to an object and set `side` to make the object follow a specific controller.
12
+ * @category XR
13
+ * */
11
14
  export class XRControllerFollow extends Behaviour {
12
15
 
13
16
  // override active and enabled here so that we always receive xr update events
src/engine-components/webxr/controllers/XRControllerModel.ts CHANGED
@@ -271,7 +271,7 @@
271
271
  const expectedHandModelName = controller.side === "left" ? "left." : "right.";
272
272
  const customHand = controller.side === "left" ? this.customLeftHand : this.customRightHand;
273
273
  if (customHand) {
274
- if (!customHand.uri.includes(expectedHandModelName)) {
274
+ if (!customHand.url.includes(expectedHandModelName)) {
275
275
  console.warn("XRControllerModel: custom hand model must be named " + expectedHandModelName);
276
276
  showBalloonWarning("Custom Hand: unexpected name, please see the console for details");
277
277
  }
@@ -290,7 +290,7 @@
290
290
  // The hand mesh should not receive raycasts
291
291
  object.traverse(child => {
292
292
  child.layers.set(2);
293
- if (NeedleXRSession.active?.isPassThrough)
293
+ if (NeedleXRSession.active?.isPassThrough && !customHand)
294
294
  this.makeOccluder(child);
295
295
  if (child instanceof Mesh) {
296
296
  NEEDLE_progressive.assignMeshLOD(child, 0);
src/engine-components/webxr/controllers/XRControllerMovement.ts CHANGED
@@ -22,6 +22,7 @@
22
22
  declare type HitPointObject = Object3D & { material: Material & { opacity: number } }
23
23
  /**
24
24
  * XRControllerMovement is a component that allows to move the XR rig using the XR controller input.
25
+ * @category XR
25
26
  */
26
27
  export class XRControllerMovement extends Behaviour implements XRMovementBehaviour {
27
28
 
src/engine-components/webxr/XRFlag.ts CHANGED
@@ -60,6 +60,10 @@
60
60
  }
61
61
  }
62
62
 
63
+ /**
64
+ * @category XR
65
+ * @category Utilities
66
+ */
63
67
  export class XRFlag extends Behaviour {
64
68
 
65
69
  private static registry: XRFlag[] = [];
src/engine-components/AnimationUtilsAutoplay.ts ADDED
@@ -0,0 +1,43 @@
1
+ import type { GLTF } from "three/examples/jsm/loaders/GLTFLoader.js";
2
+
3
+ import { AnimationUtils } from "../engine/engine_animation.js";
4
+ import { addComponent } from "../engine/engine_components.js";
5
+ import { ContextEvent, ContextRegistry } from "../engine/engine_context_registry.js";
6
+ import { Animation } from "./Animation.js";
7
+ import { Animator } from "./Animator.js";
8
+ import { GameObject } from "./Component.js";
9
+ import { PlayableDirector } from "./timeline/PlayableDirector.js";
10
+
11
+ ContextRegistry.registerCallback(ContextEvent.ContextCreated, args => {
12
+ const autoplay = args.context.domElement.getAttribute("autoplay");
13
+ if (autoplay !== undefined && (autoplay === "" || autoplay === "true" || autoplay === "1")) {
14
+ if (args.files) {
15
+ for (const file of args.files) {
16
+ const hasAnimation = GameObject.foreachComponent(file.file.scene, comp => {
17
+ if (comp.enabled === false) return undefined;
18
+ if (comp instanceof Animation && comp.playAutomatically || comp instanceof Animator || comp instanceof PlayableDirector && comp.playOnAwake === true) {
19
+ return true;
20
+ }
21
+ else if (comp instanceof Animation) {
22
+ comp.playAutomatically = true;
23
+ return true;
24
+ }
25
+ else if (comp instanceof PlayableDirector) {
26
+ comp.playOnAwake = true;
27
+ return true;
28
+ }
29
+ return undefined;
30
+ }, true);
31
+ if (hasAnimation !== true) {
32
+ AnimationUtils.assignAnimationsFromFile(file.file as GLTF, {
33
+ createAnimationComponent: (obj, _clip) => {
34
+ return addComponent(obj, Animation);
35
+ },
36
+ });
37
+ }
38
+ }
39
+ }
40
+ }
41
+ });
42
+
43
+