Needle Engine

Changes between version 3.32.11-alpha and 3.31.2
Files changed (229) hide show
  1. src/engine-schemes/vrUserStateBuffer.fbs +0 -0
  2. plugins/next/next.js +13 -4
  3. src/engine-schemes/README.md +0 -2
  4. src/engine-components/export/usdz/extensions/behavior/Actions.ts +2 -3
  5. src/engine-components/AlignmentConstraint.ts +2 -3
  6. src/engine-components/Animation.ts +3 -4
  7. src/engine-components/export/usdz/extensions/Animation.ts +4 -5
  8. src/engine-components/AnimationCurve.ts +2 -18
  9. src/engine-components/AnimationUtils.ts +3 -4
  10. src/engine-components/export/usdz/utils/animationutils.ts +4 -5
  11. src/engine-components/Animator.ts +5 -6
  12. src/engine-components/AnimatorController.ts +9 -10
  13. src/engine-components/postprocessing/Effects/Antialiasing.ts +0 -1
  14. src/engine-components/api.ts +8 -6
  15. src/engine/api.ts +22 -21
  16. src/engine-components/export/usdz/extensions/behavior/AudioExtension.ts +2 -3
  17. src/engine-components/AudioListener.ts +1 -2
  18. src/engine-components/AudioSource.ts +8 -13
  19. src/engine-components/avatar/Avatar_Brain_LookAt.ts +4 -5
  20. src/engine-components/avatar/Avatar_MouthShapes.ts +3 -4
  21. src/engine-components/webxr/Avatar.ts +0 -221
  22. src/engine-components/avatar/AvatarBlink_Simple.ts +2 -3
  23. src/engine-components/avatar/AvatarEyeLook_Rotation.ts +4 -5
  24. src/engine-components/AvatarLoader.ts +5 -6
  25. src/engine-components/AxesHelper.ts +2 -3
  26. src/engine-components/ui/BaseUIComponent.ts +25 -27
  27. src/engine-components/BasicIKConstraint.ts +2 -3
  28. src/engine-components/export/usdz/extensions/behavior/Behaviour.ts +1 -1
  29. src/engine-components/export/usdz/extensions/behavior/BehaviourComponents.ts +13 -12
  30. src/engine-components/export/usdz/extensions/behavior/BehavioursBuilder.ts +2 -2
  31. src/engine-components/postprocessing/Effects/Bloom.ts +0 -1
  32. src/engine-components/BoxHelperComponent.ts +3 -4
  33. src/engine-components/ui/Button.ts +10 -10
  34. src/engine-components/Camera.ts +13 -12
  35. src/engine-components/CameraUtils.ts +7 -8
  36. src/engine-components/ui/Canvas.ts +20 -38
  37. src/engine-components/ui/CanvasGroup.ts +3 -3
  38. src/engine-components/CharacterController.ts +4 -5
  39. src/engine-components/postprocessing/Effects/ChromaticAberration.ts +0 -1
  40. src/engine-components/Collider.ts +5 -6
  41. src/engine-components/postprocessing/Effects/ColorAdjustments.ts +1 -2
  42. src/engine-components/Component.ts +19 -112
  43. src/engine-components/codegen/components.ts +15 -10
  44. src/engine-components/ContactShadows.ts +3 -3
  45. src/engine/debug/debug_console.ts +4 -8
  46. src/engine/debug/debug_overlay.ts +7 -8
  47. src/engine/debug/debug.ts +2 -2
  48. src/engine-components/DeleteBox.ts +0 -1
  49. src/engine-components/postprocessing/Effects/DepthOfField.ts +12 -6
  50. src/engine-components/DeviceFlag.ts +1 -1
  51. src/engine-components/DragControls.ts +182 -939
  52. src/engine-components/DropListener.ts +4 -5
  53. src/engine-components/Duplicatable.ts +91 -73
  54. src/engine/engine_addressables.ts +6 -7
  55. src/engine/engine_assetdatabase.ts +1 -2
  56. src/engine/engine_camera.ts +1 -2
  57. src/engine/engine_components.ts +7 -8
  58. src/engine/engine_context.ts +68 -110
  59. src/engine/engine_create_objects.ts +1 -13
  60. src/engine/engine_element_loading.ts +11 -24
  61. src/engine/engine_element_overlay.ts +0 -17
  62. src/engine/engine_element.ts +10 -35
  63. src/engine/engine_gameobject.ts +9 -9
  64. src/engine/engine_gizmos.ts +20 -61
  65. src/engine/engine_gltf_builtin_components.ts +9 -11
  66. src/engine/engine_gltf.ts +3 -4
  67. src/engine/engine_hot_reload.ts +2 -2
  68. src/engine/engine_input.ts +185 -474
  69. src/engine/engine_license.ts +6 -9
  70. src/engine/engine_lifecycle_api.ts +4 -28
  71. src/engine/engine_lifecycle_functions_internal.ts +2 -2
  72. src/engine/engine_lightdata.ts +3 -4
  73. src/engine/engine_loaders.ts +3 -3
  74. src/engine/engine_mainloop_utils.ts +7 -34
  75. src/engine/engine_networking_auto.ts +1 -1
  76. src/engine/engine_networking_files_default_components.ts +1 -2
  77. src/engine/engine_networking_files.ts +7 -8
  78. src/engine/engine_networking_instantiate.ts +12 -17
  79. src/engine/engine_networking_peer.ts +1 -2
  80. src/engine/engine_networking_streams.ts +7 -8
  81. src/engine/engine_networking.ts +8 -12
  82. src/engine/engine_physics_rapier.ts +44 -56
  83. src/engine/engine_physics.ts +18 -23
  84. src/engine/engine_playerview.ts +1 -2
  85. src/engine/engine_scenelighting.ts +4 -5
  86. src/engine/engine_scenetools.ts +8 -9
  87. src/engine/engine_serialization_builtin_serializer.ts +6 -7
  88. src/engine/engine_serialization_core.ts +7 -8
  89. src/engine/engine_serialization.ts +5 -4
  90. src/engine/engine_shaders.ts +3 -4
  91. src/engine/engine_texture.ts +1 -2
  92. src/engine/engine_three_utils.ts +4 -18
  93. src/engine/engine_time.ts +1 -2
  94. src/engine/engine_types.ts +4 -25
  95. src/engine/engine_typestore.ts +4 -0
  96. src/engine/engine_util_decorator.ts +2 -3
  97. src/engine/engine_utils_screenshot.ts +1 -2
  98. src/engine/engine_utils.ts +6 -71
  99. src/engine/engine_xr.ts +0 -2
  100. src/engine/engine.ts +3 -3
  101. src/engine-components/ui/EventSystem.ts +178 -237
  102. src/engine-components/EventTrigger.ts +2 -2
  103. src/engine/extensions/EXT_texture_exr.ts +2 -3
  104. src/engine/extensions/extension_utils.ts +1 -2
  105. src/engine-components/export/usdz/Extension.ts +1 -2
  106. src/engine/extensions/extensions.ts +11 -12
  107. src/engine-components/js-extensions/ExtensionUtils.ts +0 -1
  108. src/engine-components/FlyControls.ts +1 -2
  109. src/engine-components/Fog.ts +1 -2
  110. src/engine-components/Gizmos.ts +4 -5
  111. src/engine-components/export/gltf/GltfExport.ts +6 -6
  112. src/engine-components/ui/Graphic.ts +7 -8
  113. src/engine-components/GridHelper.ts +3 -4
  114. src/engine-components/GroundProjection.ts +5 -11
  115. src/engine-components/ui/Image.ts +1 -2
  116. src/engine-components/export/usdz/index.ts +3 -3
  117. src/engine-components/postprocessing/index.ts +2 -2
  118. src/engine-components/timeline/index.ts +2 -2
  119. src/engine-components/webxr/index.ts +3 -2
  120. src/engine/extensions/index.ts +2 -2
  121. src/engine/xr/index.ts +0 -5
  122. src/engine-components/ui/InputField.ts +4 -4
  123. src/engine-components/Interactable.ts +14 -6
  124. src/engine/xr/internal.ts +0 -35
  125. src/engine-components/Joints.ts +0 -1
  126. src/engine-components/ui/Layout.ts +3 -3
  127. src/engine-components/Light.ts +13 -10
  128. src/engine-components/LODGroup.ts +4 -5
  129. src/engine-components/debug/LogStats.ts +1 -1
  130. src/engine-components/utils/LookAt.ts +4 -4
  131. src/engine-components/LookAtConstraint.ts +2 -3
  132. src/engine/extensions/NEEDLE_animator_controller_model.ts +2 -3
  133. src/engine/extensions/NEEDLE_components.ts +6 -7
  134. src/engine/extensions/NEEDLE_gameobject_data.ts +3 -3
  135. src/engine/extensions/NEEDLE_lighting_settings.ts +6 -7
  136. src/engine/extensions/NEEDLE_lightmaps.ts +7 -8
  137. src/engine/extensions/NEEDLE_persistent_assets.ts +2 -3
  138. src/engine/extensions/NEEDLE_progressive.ts +3 -5
  139. src/engine/extensions/NEEDLE_render_objects.ts +18 -18
  140. src/engine/extensions/NEEDLE_techniques_webgl.ts +5 -8
  141. src/needle-engine.ts +3 -0
  142. src/engine/xr/NeedleXRController.ts +0 -639
  143. src/engine/xr/NeedleXRSession.ts +0 -1247
  144. src/engine/xr/NeedleXRSync.ts +0 -221
  145. src/engine-components/NestedGltf.ts +4 -4
  146. src/engine-components/Networking.ts +1 -1
  147. src/engine-components/js-extensions/Object3D.ts +12 -11
  148. src/engine-components/OffsetConstraint.ts +3 -4
  149. src/engine-components/utils/OpenURL.ts +40 -8
  150. src/engine-components/OrbitControls.ts +15 -15
  151. src/engine-components/ui/Outline.ts +2 -3
  152. src/engine-components/ParticleSystem.ts +33 -33
  153. src/engine-components/ParticleSystemModules.ts +17 -66
  154. src/engine-components/ParticleSystemSubEmitter.ts +3 -4
  155. src/engine-components/postprocessing/Effects/Pixelation.ts +3 -4
  156. src/engine-components/timeline/PlayableDirector.ts +9 -10
  157. src/engine-components/PlayerColor.ts +14 -19
  158. src/engine-components-experimental/networking/PlayerSync.ts +26 -113
  159. src/engine-components/ui/PointerEvents.ts +30 -118
  160. src/engine-components/postprocessing/PostProcessingEffect.ts +4 -5
  161. src/engine-components/postprocessing/PostProcessingHandler.ts +4 -5
  162. src/engine-components-experimental/Presentation.ts +1 -1
  163. src/engine-components/ui/Raycaster.ts +8 -27
  164. src/engine-components/ui/RaycastUtils.ts +1 -2
  165. src/engine-components/ui/RectTransform.ts +5 -6
  166. src/engine-components/ReflectionProbe.ts +2 -3
  167. src/engine/codegen/register_types.ts +28 -17
  168. src/engine-components/Renderer.ts +38 -33
  169. src/engine-components/RendererLightmap.ts +2 -3
  170. src/engine-components/js-extensions/RGBAColor.ts +1 -2
  171. src/engine-components/RigidBody.ts +6 -7
  172. src/engine-components/SceneSwitcher.ts +12 -13
  173. src/engine/xr/SceneTransition.ts +0 -77
  174. src/engine-schemes/schemes.ts +1 -2
  175. src/engine-components/ScreenCapture.ts +8 -9
  176. src/engine-components/postprocessing/Effects/ScreenspaceAmbientOcclusion.ts +0 -1
  177. src/engine-components/postprocessing/Effects/ScreenspaceAmbientOcclusionN8.ts +2 -3
  178. src/engine-components/ShadowCatcher.ts +3 -4
  179. src/engine-components/timeline/SignalAsset.ts +2 -2
  180. src/engine-components/Skybox.ts +9 -10
  181. src/engine-components/SmoothFollow.ts +4 -5
  182. src/engine-components/ui/SpatialHtml.ts +0 -1
  183. src/engine-components/SpatialTrigger.ts +3 -4
  184. src/engine-components/SpectatorCamera.ts +33 -23
  185. src/engine-components/SpriteRenderer.ts +3 -4
  186. src/engine-components/SyncedCamera.ts +13 -13
  187. src/engine-components/SyncedRoom.ts +2 -2
  188. src/engine-components/SyncedTransform.ts +7 -25
  189. src/engine-components/webxr/TeleportTarget.ts +0 -9
  190. src/engine/xr/TempXRContext.ts +0 -183
  191. src/engine/tests/test_utils.ts +1 -1
  192. src/engine-components/TestRunner.ts +5 -6
  193. src/engine-components/ui/Text.ts +8 -9
  194. src/engine-components/export/usdz/ThreeUSDZExporter.ts +32 -33
  195. src/engine-components/postprocessing/Effects/TiltShiftEffect.ts +3 -4
  196. src/engine-components/timeline/TimelineTracks.ts +22 -60
  197. src/engine-components/postprocessing/Effects/Tonemapping.ts +0 -1
  198. src/engine-components/TransformGizmo.ts +5 -6
  199. src/engine-components/webxr/types.ts +0 -4
  200. src/engine/extensions/usage_tracker.ts +1 -2
  201. src/engine-components/export/usdz/USDZExporter.ts +92 -21
  202. src/engine-components/export/usdz/extensions/USDZText.ts +5 -6
  203. src/engine-components/export/usdz/extensions/USDZUI.ts +6 -7
  204. src/engine-components/ui/Utils.ts +2 -4
  205. src/engine/xr/utils.ts +0 -40
  206. src/engine-components/js-extensions/Vector.ts +1 -2
  207. src/engine-components/VideoPlayer.ts +6 -7
  208. src/engine-components/postprocessing/Effects/Vignette.ts +2 -3
  209. src/engine-components/Voip.ts +5 -6
  210. src/engine-components/postprocessing/Volume.ts +9 -7
  211. src/engine-components/postprocessing/VolumeParameter.ts +7 -0
  212. src/engine-schemes/vr-user-state-buffer.ts +30 -37
  213. src/engine-components/webxr/WebARCameraBackground.ts +53 -46
  214. src/engine-components/webxr/WebARSessionRoot.ts +29 -400
  215. src/engine-components/webxr/WebXR.ts +673 -207
  216. src/engine-components/webxr/WebXRAvatar.ts +300 -10
  217. src/engine-components/webxr/WebXRButtons.ts +0 -291
  218. src/engine-components/webxr/WebXRImageTracking.ts +77 -69
  219. src/engine-components/webxr/WebXRPlaneTracking.ts +49 -56
  220. src/engine-components/webxr/WebXRRig.ts +8 -45
  221. src/engine-components/webxr/controllers/XRControllerFollow.ts +0 -67
  222. src/engine-components/webxr/controllers/XRControllerModel.ts +0 -253
  223. src/engine-components/webxr/controllers/XRControllerMovement.ts +0 -328
  224. src/engine-components/webxr/XRFlag.ts +0 -143
  225. src/engine/xr/XRRig.ts +0 -9
  226. src/engine-components/webxr/WebXRController.ts +1168 -0
  227. src/engine-components/webxr/WebXRGrabRendering.ts +151 -0
  228. src/engine-components/webxr/WebXRSync.ts +463 -0
  229. src/engine-components/XRFlag.ts +139 -0
src/engine-schemes/vrUserStateBuffer.fbs CHANGED
File without changes
plugins/next/next.js CHANGED
@@ -16,15 +16,18 @@
16
16
  * @returns {import('next').NextConfig}
17
17
  */
18
18
  export const needleNext = (nextConfig, userSettings) => {
19
- if (!nextConfig) nextConfig = {
20
- reactStrictMode: true,
21
- };
19
+ console.log("Apply 🌵 needle next config");
22
20
 
21
+ if (!nextConfig)
22
+ nextConfig = {
23
+ reactStrictMode: true,
24
+ };
25
+
23
26
  // add transpile packages
24
27
  if (!nextConfig.transpilePackages) {
25
28
  nextConfig.transpilePackages = [];
26
29
  }
27
- nextConfig.transpilePackages.push("three", "peerjs", "@needle-tools/engine", "three-mesh-ui");
30
+ nextConfig.transpilePackages.push("three", "peerjs", "three-mesh-ui");
28
31
 
29
32
  // add webpack config
30
33
  if (!nextConfig.webpack) nextConfig.webpack = nextWebPack;
@@ -37,6 +40,12 @@
37
40
  }
38
41
  /** @param {import ('next').NextConfig config } */
39
42
  function nextWebPack(config, { buildId, dev, isServer, defaultLoaders, webpack }) {
43
+
44
+ // workaround: fix type finding in production builds
45
+ console.log("Apply next webpack config concatentateModules=false, pathinfo=verbose");
46
+ config.optimization.concatenateModules = false;
47
+ config.output.pathinfo = 'verbose';
48
+
40
49
  const meta = getMeta();
41
50
  let useRapier = true;
42
51
  if (userSettings.useRapier === false) useRapier = false;
src/engine-schemes/README.md DELETED
@@ -1,2 +0,0 @@
1
- Using flatbuffer compiler 2.0
2
- https://github.com/google/flatbuffers/releases/tag/v2.0.0
src/engine-components/export/usdz/extensions/behavior/Actions.ts CHANGED
@@ -1,7 +1,6 @@
1
- import { BufferGeometry,Material, Matrix4, Object3D } from "three";
2
-
3
- import { USDDocument,USDObject } from "../../ThreeUSDZExporter.js";
1
+ import { Object3D, Matrix4, Material, BufferGeometry } from "three";
4
2
  import { ActionBuilder, ActionModel } from "./BehavioursBuilder.js";
3
+ import { USDObject, USDDocument } from "../../ThreeUSDZExporter.js";
5
4
 
6
5
  export abstract class DocumentAction {
7
6
 
src/engine-components/AlignmentConstraint.ts CHANGED
@@ -1,8 +1,7 @@
1
+ import { Behaviour, GameObject } from "./Component.js";
2
+ import * as utils from "./../engine/engine_three_utils.js";
1
3
  import { Vector3 } from "three";
2
-
3
4
  import { serializable } from "../engine/engine_serialization_decorator.js";
4
- import * as utils from "./../engine/engine_three_utils.js";
5
- import { Behaviour, GameObject } from "./Component.js";
6
5
 
7
6
  export class AlignmentConstraint extends Behaviour {
8
7
 
src/engine-components/Animation.ts CHANGED
@@ -1,11 +1,10 @@
1
+ import { Behaviour } from "./Component.js";
1
2
  import { AnimationAction, AnimationClip, AnimationMixer, LoopOnce, LoopRepeat } from "three";
2
-
3
+ import { MixerEvent } from "./Animator.js";
4
+ import { serializable } from "../engine/engine_serialization_decorator.js";
3
5
  import { Mathf } from "../engine/engine_math.js";
4
- import { serializable } from "../engine/engine_serialization_decorator.js";
5
6
  import type { Vec2 } from "../engine/engine_types.js";
6
7
  import { getParam } from "../engine/engine_utils.js";
7
- import { MixerEvent } from "./Animator.js";
8
- import { Behaviour } from "./Component.js";
9
8
 
10
9
  const debug = getParam("debuganimation");
11
10
 
src/engine-components/export/usdz/extensions/Animation.ts CHANGED
@@ -1,10 +1,9 @@
1
- import { AnimationClip, Bone,Interpolant, KeyframeTrack, Matrix4, Object3D, PropertyBinding, Quaternion, Vector3 } from "three";
2
-
1
+ import { GameObject } from "../../../Component.js";
3
2
  import { getParam } from "../../../../engine/engine_utils.js";
3
+ import { USDObject, buildMatrix, findStructuralNodesInBoneHierarchy, usdNumberFormatting as fn, getPathToSkeleton } from "../ThreeUSDZExporter.js";
4
+ import type { IUSDExporterExtension } from "../Extension.js";
5
+ import { Object3D, Matrix4, Vector3, Quaternion, Interpolant, AnimationClip, KeyframeTrack, PropertyBinding, Bone } from "three";
4
6
  import { Animator } from "../../../Animator.js";
5
- import { GameObject } from "../../../Component.js";
6
- import type { IUSDExporterExtension } from "../Extension.js";
7
- import { buildMatrix, findStructuralNodesInBoneHierarchy, getPathToSkeleton,usdNumberFormatting as fn, USDObject } from "../ThreeUSDZExporter.js";
8
7
 
9
8
  const debug = getParam("debugusdzanimation");
10
9
  const debugSerialization = getParam("debugusdzanimationserialization");
src/engine-components/AnimationCurve.ts CHANGED
@@ -23,22 +23,6 @@
23
23
  @serializable(Keyframe)
24
24
  keys!: Array<Keyframe>;
25
25
 
26
- clone() {
27
- const curve = new AnimationCurve();
28
- curve.keys = this.keys?.map(k => {
29
- const key = new Keyframe();
30
- key.time = k.time;
31
- key.value = k.value;
32
- key.inTangent = k.inTangent;
33
- key.inWeight = k.inWeight;
34
- key.outTangent = k.outTangent;
35
- key.outWeight = k.outWeight;
36
- key.weightedMode = k.weightedMode;
37
- return key;
38
- }) || [];
39
- return curve;
40
- }
41
-
42
26
  get duration(): number {
43
27
  if (!this.keys || this.keys.length == 0) return 0;
44
28
  return this.keys[this.keys.length - 1].time;
@@ -54,9 +38,9 @@
54
38
  for (let i = 0; i < this.keys.length; i++) {
55
39
  const kf = this.keys[i];
56
40
  if (kf.time <= time) {
57
- const hasNextKeyframe = i + 1 < this.keys.length;
41
+ const hasNextKeyframe = i+1 < this.keys.length;
58
42
  if (hasNextKeyframe) {
59
- const nextKf = this.keys[i + 1];
43
+ const nextKf = this.keys[i+1];
60
44
  // if the next
61
45
  if (nextKf.time < time) continue;
62
46
  // tangents are set to Infinity if interpolation is set to constant - in that case we should always return the floored value
src/engine-components/AnimationUtils.ts CHANGED
@@ -1,12 +1,11 @@
1
- import { Object3D } from "three";
2
1
  import type { GLTF } from "three/examples/jsm/loaders/GLTFLoader.js";
3
-
2
+ import { ContextEvent, ContextRegistry } from "../engine/engine_context_registry.js";
4
3
  import { addNewComponent } from "../engine/engine_components.js";
5
- import { ContextEvent, ContextRegistry } from "../engine/engine_context_registry.js";
4
+ import { Animator } from "./Animator.js";
6
5
  import { Animation } from "./Animation.js";
7
- import { Animator } from "./Animator.js";
8
6
  import { GameObject } from "./Component.js";
9
7
  import { PlayableDirector } from "./timeline/PlayableDirector.js";
8
+ import { Object3D } from "three";
10
9
 
11
10
 
12
11
  const $objectAnimationKey = Symbol("objectIsAnimatedData");
src/engine-components/export/usdz/utils/animationutils.ts CHANGED
@@ -1,10 +1,9 @@
1
- import { AnimationClip,Object3D } from "three";
2
-
3
- import { getParam } from "../../../../engine/engine_utils.js";
1
+ import { Animator } from "../../../Animator.js";
4
2
  import { Animation } from "../../../Animation.js";
5
- import { Animator } from "../../../Animator.js";
3
+ import { Object3D, AnimationClip } from "three";
4
+ import { AnimationExtension } from "../extensions/Animation.js";
6
5
  import { Behaviour, GameObject } from "../../../Component.js";
7
- import { AnimationExtension } from "../extensions/Animation.js";
6
+ import { getParam } from "../../../../engine/engine_utils.js";
8
7
  import { PlayAnimationOnClick } from "../extensions/behavior/BehaviourComponents.js";
9
8
 
10
9
  const debug = getParam("debugusdz");
src/engine-components/Animator.ts CHANGED
@@ -1,12 +1,11 @@
1
- import type { AnimationAction, AnimationActionLoopStyles, AnimationMixer } from "three";
2
-
3
- import { Mathf } from "../engine/engine_math.js";
4
- import { serializable } from "../engine/engine_serialization_decorator.js";
1
+ import { Behaviour } from "./Component.js";
2
+ import type { AnimationActionLoopStyles, AnimationAction, AnimationMixer } from "three";
5
3
  import { getParam } from "../engine/engine_utils.js";
6
4
  import type { AnimatorControllerModel } from "../engine/extensions/NEEDLE_animator_controller_model.js";
5
+ import { AnimatorController } from "./AnimatorController.js";
6
+ import { serializable } from "../engine/engine_serialization_decorator.js";
7
+ import { Mathf } from "../engine/engine_math.js";
7
8
  import { getObjectAnimated } from "./AnimationUtils.js";
8
- import { AnimatorController } from "./AnimatorController.js";
9
- import { Behaviour } from "./Component.js";
10
9
 
11
10
  const debug = getParam("debuganimator");
12
11
 
src/engine-components/AnimatorController.ts CHANGED
@@ -1,16 +1,15 @@
1
+ import { Animator } from "./Animator.js";
2
+ import type { AnimatorControllerModel, Condition, State, Transition } from "../engine/extensions/NEEDLE_animator_controller_model.js";
3
+ import { AnimatorConditionMode, AnimatorControllerParameterType, AnimatorStateInfo, createMotion, StateMachineBehaviour } from "../engine/extensions/NEEDLE_animator_controller_model.js";
1
4
  import { AnimationAction, AnimationClip, AnimationMixer, AxesHelper, Euler, KeyframeTrack, LoopOnce, Object3D, Quaternion, Vector3 } from "three";
2
-
5
+ import { deepClone, getParam } from "../engine/engine_utils.js";
6
+ import { Context } from "../engine/engine_setup.js";
7
+ import { TypeStore } from "../engine/engine_typestore.js";
8
+ import { SerializationContext, TypeSerializer, assign } from "../engine/engine_serialization_core.js";
9
+ import { Mathf } from "../engine/engine_math.js";
10
+ import { isAnimationAction } from "../engine/engine_three_utils.js";
3
11
  import { isDevEnvironment } from "../engine/debug/index.js";
4
- import { Mathf } from "../engine/engine_math.js";
5
12
  import { InstantiateIdProvider } from "../engine/engine_networking_instantiate.js";
6
- import { assign,SerializationContext, TypeSerializer } from "../engine/engine_serialization_core.js";
7
- import { Context } from "../engine/engine_setup.js";
8
- import { isAnimationAction } from "../engine/engine_three_utils.js";
9
- import { TypeStore } from "../engine/engine_typestore.js";
10
- import { deepClone, getParam } from "../engine/engine_utils.js";
11
- import type { AnimatorControllerModel, Condition, State, Transition } from "../engine/extensions/NEEDLE_animator_controller_model.js";
12
- import { AnimatorConditionMode, AnimatorControllerParameterType, AnimatorStateInfo, createMotion, StateMachineBehaviour } from "../engine/extensions/NEEDLE_animator_controller_model.js";
13
- import { Animator } from "./Animator.js";
14
13
 
15
14
  const debug = getParam("debuganimatorcontroller");
16
15
  const debugRootMotion = getParam("debugrootmotion");
src/engine-components/postprocessing/Effects/Antialiasing.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import { EdgeDetectionMode, SMAAEffect, SMAAPreset } from "postprocessing";
2
-
3
2
  import { serializable } from "../../../engine/engine_serialization.js";
4
3
  import { type EffectProviderResult, PostProcessingEffect } from "../PostProcessingEffect.js";
5
4
  import { VolumeParameter } from "../VolumeParameter.js";
src/engine-components/api.ts CHANGED
@@ -1,18 +1,20 @@
1
+ export { Behaviour, Component, GameObject } from "./Component.js"
1
2
  export * from "./codegen/components.js";
2
- export { Behaviour, Component, GameObject } from "./Component.js"
3
3
 
4
4
  // We dont want to export everything in the extensions
5
- export { ClearFlags } from "./Camera.js"
5
+ export * from "./js-extensions/RGBAColor.js";
6
+ export * from "./js-extensions/Object3D.js";
7
+ export * from "./XRFlag.js"
8
+
6
9
  export * from "./export/index.js"
7
- export * from "./js-extensions/Object3D.js";
8
- export * from "./js-extensions/RGBAColor.js";
9
10
  export * from "./postprocessing/index.js"
10
- export { type ISceneEventListener } from "./SceneSwitcher.js";
11
11
  export * from "./timeline/index.js"
12
12
  export * from "./ui/index.js"
13
13
  export * from "./webxr/index.js"
14
- export * from "./webxr/XRFlag.js"
15
14
 
15
+ export { ClearFlags } from "./Camera.js"
16
+ export { type ISceneEventListener } from "./SceneSwitcher.js";
17
+
16
18
  import "./CameraUtils.js"
17
19
  import "./AnimationUtils.js"
18
20
 
src/engine/api.ts CHANGED
@@ -1,42 +1,42 @@
1
1
 
2
- export * from "./debug/index.js";
2
+ export * from "./extensions/index.js";
3
3
  export * from "./engine_addressables.js";
4
4
  export * from "./engine_application.js";
5
5
  export * from "./engine_assetdatabase.js";
6
+ export * from "./engine_create_objects.js";
7
+ export * from "./engine_components_internal.js";
6
8
  export * from "./engine_components.js";
7
9
  export * from "./engine_components_internal.js";
8
- export * from "./engine_components_internal.js";
9
- export * from "./engine_constants.js";
10
+ export * from "./engine_context_registry.js";
10
11
  export * from "./engine_context.js";
11
- export * from "./engine_context_registry.js";
12
12
  export * from "./engine_coroutine.js"
13
- export * from "./engine_create_objects.js";
13
+ export * from "./engine_constants.js";
14
+ export * from "./debug/index.js";
14
15
  export * from "./engine_element.js";
16
+ export * from "./engine_element_loading.js";
15
17
  export * from "./engine_element_attributes.js";
16
- export * from "./engine_element_loading.js";
17
- export * from "./engine_gameobject.js";
18
18
  export { Gizmos } from "./engine_gizmos.js"
19
19
  export * from "./engine_gltf.js";
20
20
  export * from "./engine_hot_reload.js";
21
- export * from "./engine_input.js";
22
- export { InstancingUtil } from "./engine_instancing.js";
23
- export { hasIndieLicense,hasProLicense } from "./engine_license.js";
24
- export * from "./engine_lifecycle_api.js";
25
- export * from "./engine_math.js";
21
+ export * from "./engine_gameobject.js";
26
22
  export * from "./engine_networking.js";
23
+ export * from "./engine_networking_types.js";
27
24
  export { syncField } from "./engine_networking_auto.js";
28
25
  export * from "./engine_networking_files.js";
29
26
  export * from "./engine_networking_instantiate.js";
30
- export * from "./engine_networking_peer.js";
31
27
  export * from "./engine_networking_streams.js";
32
- export * from "./engine_networking_types.js";
33
28
  export * from "./engine_networking_utils.js";
29
+ export * from "./engine_networking_peer.js";
34
30
  export * from "./engine_patcher.js";
31
+ export * from "./engine_playerview.js";
35
32
  export * from "./engine_physics.js";
36
33
  export * from "./engine_physics.types.js";
37
34
  export * from "./engine_physics_rapier.js";
38
- export * from "./engine_playerview.js";
39
35
  export * from "./engine_scenelighting.js";
36
+ export * from "./engine_input.js";
37
+ export * from "./engine_lifecycle_api.js";
38
+ export * from "./engine_math.js";
39
+ export * from "./js-extensions/index.js";
40
40
  export * from "./engine_scenetools.js";
41
41
  export * from "./engine_serialization.js";
42
42
  export { type ISerializable } from "./engine_serialization_core.js";
@@ -44,11 +44,12 @@
44
44
  export * from "./engine_three_utils.js";
45
45
  export * from "./engine_time.js";
46
46
  export * from "./engine_types.js";
47
- export { registerType,TypeStore } from "./engine_typestore.js";
48
- export { prefix,validate } from "./engine_util_decorator.js";
49
- export * from "./engine_utils.js";
50
47
  export * from "./engine_utils_screenshot.js";
51
48
  export * from "./engine_web_api.js";
52
- export * from "./engine_xr.js";
53
- export * from "./extensions/index.js";
54
- export * from "./js-extensions/index.js";
49
+ export * from "./engine_utils.js";
50
+
51
+ export { TypeStore, registerType } from "./engine_typestore.js";
52
+
53
+ export { InstancingUtil } from "./engine_instancing.js";
54
+ export { validate, prefix } from "./engine_util_decorator.js";
55
+ export { hasProLicense, hasIndieLicense } from "./engine_license.js";
src/engine-components/export/usdz/extensions/behavior/AudioExtension.ts CHANGED
@@ -1,9 +1,8 @@
1
- import { Object3D } from "three";
2
-
3
- import { AudioSource } from "../../../../AudioSource.js";
4
1
  import { GameObject } from "../../../../Component.js";
5
2
  import type { IUSDExporterExtension } from "../../Extension.js";
6
3
  import { USDObject, USDWriter, USDZExporterContext } from "../../ThreeUSDZExporter.js";
4
+ import { Object3D } from "three";
5
+ import { AudioSource } from "../../../../AudioSource.js";
7
6
 
8
7
  export class AudioExtension implements IUSDExporterExtension {
9
8
 
src/engine-components/AudioListener.ts CHANGED
@@ -1,8 +1,7 @@
1
+ import { Behaviour, GameObject } from "./Component.js";
1
2
  import { AudioListener as ThreeAudioListener } from "three";
2
-
3
3
  import { AudioSource } from "./AudioSource.js";
4
4
  import { Camera } from "./Camera.js";
5
- import { Behaviour, GameObject } from "./Component.js";
6
5
 
7
6
 
8
7
  export class AudioListener extends Behaviour {
src/engine-components/AudioSource.ts CHANGED
@@ -1,12 +1,11 @@
1
+ import { Behaviour, GameObject } from "./Component.js";
2
+ import { PositionalAudioHelper } from 'three/examples/jsm/helpers/PositionalAudioHelper.js';
3
+ import { AudioListener } from "./AudioListener.js";
4
+ import * as utils from "../engine/engine_utils.js";
5
+ import { serializable } from "../engine/engine_serialization_decorator.js";
6
+ import { ApplicationEvents } from "../engine/engine_application.js";
1
7
  import { Audio, AudioContext, AudioLoader, PositionalAudio } from "three";
2
- import { PositionalAudioHelper } from 'three/examples/jsm/helpers/PositionalAudioHelper.js';
3
-
4
8
  import { isDevEnvironment } from "../engine/debug/index.js";
5
- import { ApplicationEvents } from "../engine/engine_application.js";
6
- import { serializable } from "../engine/engine_serialization_decorator.js";
7
- import * as utils from "../engine/engine_utils.js";
8
- import { AudioListener } from "./AudioListener.js";
9
- import { Behaviour, GameObject } from "./Component.js";
10
9
 
11
10
 
12
11
  const debug = utils.getParam("debugaudio");
@@ -141,7 +140,7 @@
141
140
  if (!listener && this.context.mainCamera) listener = GameObject.addNewComponent(this.context.mainCamera, AudioListener);
142
141
  if (listener?.listener) {
143
142
  this.sound = new PositionalAudio(listener.listener);
144
- this.gameObject?.add(this.sound);
143
+ this.gameObject.add(this.sound);
145
144
  }
146
145
  else if (debug) console.warn("No audio listener found in scene - can not play audio");
147
146
  }
@@ -158,9 +157,6 @@
158
157
  }
159
158
 
160
159
  onEnable(): void {
161
- if (this.sound)
162
- this.gameObject.add(this.sound);
163
-
164
160
  if (!AudioSource.userInteractionRegistered) {
165
161
  AudioSource.registerWaitForAllowAudio(() => {
166
162
  if (this.enabled && !this.destroyed && this.shouldPlay)
@@ -332,7 +328,6 @@
332
328
  if (this.sound && !this.sound.isPlaying) {
333
329
  const muted = this.context.application.muted;
334
330
  if (muted) this.sound.setVolume(0);
335
- this.gameObject?.add(this.sound);
336
331
 
337
332
  if (this.clip instanceof MediaStream) {
338
333
 
@@ -416,7 +411,7 @@
416
411
  this._hasEnded = true;
417
412
  if (debug)
418
413
  console.log("Audio clip ended", this.clip);
419
- this.dispatchEvent(new CustomEvent("ended", { detail: this }));
414
+ this.sound.dispatchEvent({ type: 'ended', target: this });
420
415
  }
421
416
 
422
417
  // this.gameObject.position.x = Math.sin(time.time) * 2;
src/engine-components/avatar/Avatar_Brain_LookAt.ts CHANGED
@@ -1,12 +1,11 @@
1
1
  import * as THREE from "three";
2
-
3
- import { OwnershipModel } from "../../engine/engine_networking.js";
4
- import type { IModel } from "../../engine/engine_networking_types.js";
5
- import { Context } from "../../engine/engine_setup.js";
6
- import * as utils from "../../engine/engine_three_utils.js";
7
2
  import { TypeStore } from "../../engine/engine_typestore.js";
8
3
  import { Behaviour, GameObject } from "../Component.js";
9
4
  import { AvatarMarker } from "../webxr/WebXRAvatar.js";
5
+ import * as utils from "../../engine/engine_three_utils.js";
6
+ import { OwnershipModel } from "../../engine/engine_networking.js";
7
+ import { Context } from "../../engine/engine_setup.js";
8
+ import type { IModel } from "../../engine/engine_networking_types.js";
10
9
 
11
10
  export class Avatar_POI {
12
11
 
src/engine-components/avatar/Avatar_MouthShapes.ts CHANGED
@@ -1,10 +1,9 @@
1
- import { Object3D } from "three";
2
-
3
- import { serializable } from "../../engine/engine_serialization_decorator.js";
4
- import * as utils from "../../engine/engine_utils.js";
5
1
  import { Behaviour, GameObject } from "../Component.js";
6
2
  import { Voip } from "../Voip.js";
7
3
  import { AvatarMarker } from "../webxr/WebXRAvatar.js";
4
+ import * as utils from "../../engine/engine_utils.js";
5
+ import { Object3D } from "three";
6
+ import { serializable } from "../../engine/engine_serialization_decorator.js";
8
7
 
9
8
  const debug = utils.getParam("debugmouth");
10
9
 
src/engine-components/webxr/Avatar.ts DELETED
@@ -1,221 +0,0 @@
1
- import { Object3D, Quaternion, Vector3 } from "three";
2
-
3
- import { AssetReference } from "../../engine/engine_addressables.js";
4
- import { ObjectUtils, PrimitiveType } from "../../engine/engine_create_objects.js";
5
- import { serializable } from "../../engine/engine_serialization_decorator.js";
6
- import { IGameObject } from "../../engine/engine_types.js";
7
- import { getParam,PromiseAllWithErrors } from "../../engine/engine_utils.js";
8
- import { NeedleXREventArgs, NeedleXRSession, NeedleXRUtils } from "../../engine/xr/index.js";
9
- import { PlayerState } from "../../engine-components-experimental/networking/PlayerSync.js";
10
- import { Behaviour, GameObject } from "../Component.js";
11
- import { SyncedTransform } from "../SyncedTransform.js";
12
- import { AvatarMarker } from "./WebXRAvatar.js";
13
- import { XRFlag } from "./XRFlag.js";
14
-
15
- const debug = getParam("debugwebxr");
16
-
17
- const flipForwardQuaternion = new Quaternion().setFromAxisAngle(new Vector3(0, 1, 0), Math.PI);
18
-
19
- export class Avatar extends Behaviour {
20
-
21
- @serializable(AssetReference)
22
- head?: AssetReference;
23
-
24
- @serializable(AssetReference)
25
- leftHand?: AssetReference;
26
-
27
- @serializable(AssetReference)
28
- rightHand?: AssetReference;
29
-
30
- private _syncTransforms?: SyncedTransform[];
31
-
32
- async onEnterXR(_args: NeedleXREventArgs) {
33
- if (!this.activeAndEnabled) return;
34
- if (debug) console.warn("AVATAR ENTER XR", this.guid, this.sourceId, this, this.activeAndEnabled)
35
- if (this._syncTransforms)
36
- this._syncTransforms.length = 0;
37
- await this.prepareAvatar();
38
-
39
- const playerstate = PlayerState.getFor(this);
40
- if (playerstate?.owner) {
41
- const marker = this.gameObject.addNewComponent(AvatarMarker)!;
42
- marker.avatar = this.gameObject;
43
- marker.connectionId = playerstate.owner;
44
- }
45
- else if(this.context.connection.isConnected) console.error("No player state found for avatar", this);
46
- }
47
-
48
- onLeaveXR(_args: NeedleXREventArgs): void {
49
- const marker = this.gameObject.getComponent(AvatarMarker);
50
- if (marker) {
51
- marker.destroy();
52
- }
53
- }
54
-
55
- onUpdateXR(args: NeedleXREventArgs): void {
56
- if (!this.activeAndEnabled) return;
57
-
58
- const isLocalPlayer = PlayerState.isLocalPlayer(this);
59
- if (!isLocalPlayer) return;
60
-
61
- const xr = args.xr;
62
- // make sure the avatar is inside the active rig
63
- if (xr.rig && xr.rig.gameObject !== this.gameObject.parent) {
64
- this.gameObject.position.set(0, 0, 0);
65
- this.gameObject.rotation.set(0, 0, 0);
66
- this.gameObject.scale.set(1, 1, 1);
67
- xr.rig.gameObject.add(this.gameObject);
68
- }
69
- // this.gameObject.position.copy(xr.rig!.gameObject.position);
70
- // this.gameObject.quaternion.copy(xr.rig!.gameObject.quaternion);
71
- // this.gameObject.scale.set(1, 1, 1);
72
-
73
-
74
- if (this._syncTransforms && isLocalPlayer) {
75
- for (const sync of this._syncTransforms) {
76
- sync.fastMode = true;
77
- if (!sync.isOwned())
78
- sync.requestOwnership();
79
- }
80
- }
81
-
82
-
83
- // synchronize head
84
- if (this.head && this.context.mainCamera) {
85
- const headObj = this.head.asset as IGameObject;
86
- headObj.position.copy(this.context.mainCamera.position);
87
- headObj.quaternion.copy(this.context.mainCamera.quaternion);
88
- headObj.quaternion.x *= -1;
89
-
90
- // HACK: XRFlag limitation workaround to make sure first person user head is never rendered
91
- if (this.context.time.frameCount % 10 === 0) {
92
- const xrflags = GameObject.getComponentsInChildren(this.head.asset, XRFlag);
93
- for (const flag of xrflags) {
94
- flag.enabled = false;
95
- flag.gameObject.visible = false;
96
- }
97
- }
98
- }
99
-
100
- // synchronize hands
101
- const leftCtrl = args.xr.leftController;
102
- const leftObj = this.leftHand?.asset as Object3D;
103
- if (leftCtrl && leftObj) {
104
- leftObj.position.copy(leftCtrl.gripPosition);
105
- leftObj.quaternion.copy(leftCtrl.gripQuaternion);
106
- leftObj.quaternion.multiply(flipForwardQuaternion);
107
- leftObj.visible = leftCtrl.isTracking;
108
- }
109
-
110
- const right = args.xr.rightController;
111
- if (right && this.rightHand?.asset) {
112
- const rightObj = this.rightHand.asset as Object3D;
113
- rightObj.position.copy(right.gripPosition);
114
- rightObj.quaternion.copy(right.gripQuaternion);
115
- rightObj.quaternion.multiply(flipForwardQuaternion);
116
- rightObj.visible = right.isTracking;
117
- }
118
- }
119
-
120
- onBeforeRender(): void {
121
- if (this.context.time.frame % 10 === 0)
122
- this.updateRemoteAvatarVisibility();
123
- }
124
-
125
-
126
- private updateRemoteAvatarVisibility() {
127
- if (this.context.connection.isConnected) {
128
- const state = PlayerState.getFor(this);
129
- if (state && state.isLocalPlayer == false) {
130
-
131
- const sync = NeedleXRSession.getXRSync(this.context);
132
- if (sync) {
133
- if (sync.hasState(state.owner)) {
134
- this.tryFindAvatarObjectsIfMissing();
135
-
136
- const leftObj = this.leftHand?.asset as Object3D;
137
- if (leftObj) {
138
- leftObj.visible = sync?.isTracking(state.owner, "left") ?? false;
139
- }
140
- const rightObj = this.rightHand?.asset as Object3D;
141
- if (rightObj) {
142
- rightObj.visible = sync?.isTracking(state.owner, "right") ?? false;
143
- }
144
- }
145
- }
146
-
147
- // HACK: XRFlag limitation workaround to make sure first person user head of OTHER users is ALWAYS rendered
148
- if (this.head?.asset) {
149
- const xrflags = GameObject.getComponentsInChildren(this.head.asset, XRFlag);
150
- for (const flag of xrflags) {
151
- flag.enabled = false;
152
- flag.gameObject.visible = true;
153
- }
154
- }
155
- }
156
- }
157
- }
158
-
159
-
160
-
161
- private tryFindAvatarObjectsIfMissing() {
162
- // if no avatar objects are set, try to find them
163
- if (!this.head || !this.leftHand || !this.rightHand) {
164
- const res = { head: this.head, leftHand: this.leftHand, rightHand: this.rightHand };
165
- NeedleXRUtils.tryFindAvatarObjects(this.gameObject, this.sourceId || "", res);
166
- if (res.head) this.head = res.head;
167
- if (res.leftHand) this.leftHand = res.leftHand;
168
- if (res.rightHand) this.rightHand = res.rightHand;
169
- }
170
- }
171
-
172
- private async prepareAvatar() {
173
- // if no avatar objects are set, try to find them
174
- this.tryFindAvatarObjectsIfMissing();
175
-
176
- if (!this.head) {
177
- const head = new Object3D();
178
- head.name = "Head";
179
- const cube = ObjectUtils.createPrimitive(PrimitiveType.Cube);
180
- head.add(cube);
181
- this.gameObject.add(head);
182
- this.head = new AssetReference("", this.sourceId, head);
183
- if (debug) console.log("Create head", head);
184
- }
185
-
186
- if (!this.rightHand) {
187
- const rightHand = new Object3D();
188
- rightHand.name = "Right Hand";
189
- this.gameObject.add(rightHand);
190
- this.rightHand = new AssetReference("", this.sourceId, rightHand);
191
- if (debug) console.log("Create right hand", rightHand);
192
- }
193
-
194
- if (!this.leftHand) {
195
- const leftHand = new Object3D();
196
- leftHand.name = "Left Hand";
197
- this.gameObject.add(leftHand);
198
- this.leftHand = new AssetReference("", this.sourceId, leftHand);
199
- if (debug) console.log("Create left hand", leftHand);
200
- }
201
-
202
- await this.loadAvatarObjects(this.head, this.leftHand, this.rightHand);
203
-
204
- if (PlayerState.isLocalPlayer(this.gameObject)) {
205
- this._syncTransforms = GameObject.getComponentsInChildren(this.gameObject, SyncedTransform);
206
- }
207
- }
208
-
209
-
210
- private async loadAvatarObjects(head: AssetReference, left: AssetReference, right: AssetReference) {
211
- const pHead = head.loadAssetAsync();
212
- const pHandLeft = left.loadAssetAsync();
213
- const pHandRight = right.loadAssetAsync();
214
- const promises = new Array<Promise<any>>();
215
- if (pHead) promises.push(pHead);
216
- if (pHandLeft) promises.push(pHandLeft);
217
- if (pHandRight) promises.push(pHandRight);
218
- const res = await PromiseAllWithErrors(promises);
219
- if (debug) console.log("Avatar loaded results:", res);
220
- }
221
- }
src/engine-components/avatar/AvatarBlink_Simple.ts CHANGED
@@ -1,8 +1,7 @@
1
1
  import { Object3D } from "three";
2
-
2
+ import { Behaviour, GameObject } from "../Component.js";
3
+ import { XRFlag, XRState } from "../XRFlag.js";
3
4
  import { serializable } from "../../engine/engine_serialization_decorator.js";
4
- import { Behaviour, GameObject } from "../Component.js";
5
- import { XRFlag, XRState } from "../webxr/XRFlag.js";
6
5
 
7
6
 
8
7
  export class AvatarBlink_Simple extends Behaviour {
src/engine-components/avatar/AvatarEyeLook_Rotation.ts CHANGED
@@ -1,11 +1,10 @@
1
+ import { Behaviour, GameObject } from "../Component.js";
2
+ import * as utils from "../../engine/engine_three_utils.js"
1
3
  import * as THREE from "three";
4
+ import { Avatar_Brain_LookAt } from "./Avatar_Brain_LookAt.js";
5
+ import { serializable } from "../../engine/engine_serialization_decorator.js";
2
6
  import { Object3D } from "three";
3
7
 
4
- import { serializable } from "../../engine/engine_serialization_decorator.js";
5
- import * as utils from "../../engine/engine_three_utils.js"
6
- import { Behaviour, GameObject } from "../Component.js";
7
- import { Avatar_Brain_LookAt } from "./Avatar_Brain_LookAt.js";
8
-
9
8
  export class AvatarEyeLook_Rotation extends Behaviour {
10
9
 
11
10
  @serializable(Object3D)
src/engine-components/AvatarLoader.ts CHANGED
@@ -1,13 +1,12 @@
1
- import { Box3, Object3D, Vector3 } from "three";
2
1
  import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
3
-
4
- import { InstantiateOptions } from "../engine/engine_gameobject.js";
5
- import { getLoader } from "../engine/engine_gltf.js";
2
+ import * as utils from "../engine/engine_utils.js"
6
3
  import * as loaders from "../engine/engine_loaders.js"
7
4
  import { Context } from "../engine/engine_setup.js";
8
- import * as utils from "../engine/engine_utils.js"
5
+ import { GameObject } from "./Component.js";
9
6
  import { download_file } from "../engine/engine_web_api.js";
10
- import { GameObject } from "./Component.js";
7
+ import { getLoader } from "../engine/engine_gltf.js";
8
+ import { InstantiateOptions } from "../engine/engine_gameobject.js";
9
+ import { Box3, Object3D, Vector3 } from "three";
11
10
 
12
11
  const debug = utils.getParam("debugavatar");
13
12
 
src/engine-components/AxesHelper.ts CHANGED
@@ -1,8 +1,7 @@
1
- import { AxesHelper as _AxesHelper } from "three";
2
-
1
+ import { Behaviour } from "./Component.js";
3
2
  import * as params from "../engine/engine_default_parameters.js";
4
3
  import { serializable } from "../engine/engine_serialization_decorator.js";
5
- import { Behaviour } from "./Component.js";
4
+ import { AxesHelper as _AxesHelper } from "three";
6
5
 
7
6
  export class AxesHelper extends Behaviour {
8
7
  @serializable()
src/engine-components/ui/BaseUIComponent.ts CHANGED
@@ -1,12 +1,11 @@
1
1
  // import { Canvas } from './Canvas.js';
2
- import { AxesHelper, Object3D } from 'three';
3
2
  import * as ThreeMeshUI from 'three-mesh-ui';
4
-
5
- import { showGizmos } from '../../engine/engine_default_parameters.js';
6
- import { getParam } from '../../engine/engine_utils.js';
7
3
  import { Behaviour, GameObject } from "../Component.js";
8
4
  import { EventSystem } from "./EventSystem.js";
5
+ import { showGizmos } from '../../engine/engine_default_parameters.js';
6
+ import { AxesHelper, Object3D } from 'three';
9
7
  import type { ICanvas } from './Interfaces.js';
8
+ import { getParam } from '../../engine/engine_utils.js';
10
9
  export const includesDir = "./include";
11
10
 
12
11
  const debug = getParam("debugshadowcomponents");
@@ -25,38 +24,22 @@
25
24
 
26
25
  export const $shadowDomOwner = Symbol("shadowDomOwner");
27
26
 
28
- /** Derive from this class if you want to implement your own UI components
29
- * It provides utility methods and simplifies managing the underlying three-mesh-ui hierarchy
30
- */
31
27
  export class BaseUIComponent extends Behaviour {
32
28
 
33
- /** Is this object on the root of the UI hierarchy ? */
34
29
  isRoot() { return this.Root?.gameObject === this.gameObject; }
35
30
 
36
- /** Access the parent canvas component */
37
31
  get canvas() {
38
32
  const cv = this.Root as any as ICanvas;
39
33
  if (cv?.isCanvas) return cv;
40
34
  return null;
41
35
  }
42
- /** @deprecated use `canvas` */
43
- protected get Canvas() {
44
- return this.canvas;
45
- }
46
36
 
47
- /** Mark the UI dirty which will trigger an THREE-Mesh-UI update */
48
37
  markDirty() {
49
38
  EventSystem.markUIDirty(this.context);
50
39
  }
51
40
 
52
- /** the underlying three-mesh-ui */
53
- get shadowComponent() { return this._shadowComponent }
54
- private set shadowComponent(val: Object3D | null) {
55
- this._shadowComponent = val;
56
- }
41
+ shadowComponent: ThreeMeshUI.Block | null = null;
57
42
 
58
- private _shadowComponent: Object3D | null = null;
59
-
60
43
  private _controlsChildLayout = true;
61
44
  get controlsChildLayout(): boolean { return this._controlsChildLayout; }
62
45
  set controlsChildLayout(val: boolean) {
@@ -75,6 +58,11 @@
75
58
  return this._root;
76
59
  }
77
60
 
61
+ // TODO: rename to canvas
62
+ protected get Canvas() {
63
+ return this.canvas;
64
+ }
65
+
78
66
  // private _intermediate?: Object3D;
79
67
  protected _parentComponent?: BaseUIComponent | null = undefined;
80
68
 
@@ -89,10 +77,7 @@
89
77
  super.onEnable();
90
78
  }
91
79
 
92
- /** Add a three-mesh-ui object to the UI hierarchy
93
- * @param container the three-mesh-ui object to add
94
- * @param parent the parent component to add the object to
95
- */
80
+ //@ts-ignore
96
81
  protected addShadowComponent(container: any, parent?: BaseUIComponent) {
97
82
 
98
83
  this.removeShadowComponent();
@@ -149,7 +134,21 @@
149
134
  if(debug) console.log(this.shadowComponent)
150
135
  }
151
136
 
152
- protected setShadowComponentOwner(current: ThreeMeshUI.MeshUIBaseElement | Object3D | null | undefined) {
137
+
138
+ set(_state: object) {
139
+ // if (!this.shadowComponent) return;
140
+ // this.traverseOwnedShadowComponents(this.shadowComponent, this, o => {
141
+ // for (const ch of o.children) {
142
+ // console.log(this, ch);
143
+ // if (ch.isUI && typeof ch.set === "function") {
144
+ // // ch.set(state);
145
+ // // ch.update(true, true, true);
146
+ // }
147
+ // }
148
+ // })
149
+ }
150
+
151
+ protected setShadowComponentOwner(current: Object3D | null | undefined) {
153
152
  if (!current) return;
154
153
  // TODO: only traverse our own hierarchy, we can stop if we find another owner
155
154
  if (current[$shadowDomOwner] === undefined || current[$shadowDomOwner] === this) {
@@ -172,7 +171,6 @@
172
171
  }
173
172
  }
174
173
 
175
- /** Remove the underlying UI object from the hierarchy */
176
174
  protected removeShadowComponent() {
177
175
  if (this.shadowComponent) {
178
176
  this.shadowComponent.removeFromParent();
src/engine-components/BasicIKConstraint.ts CHANGED
@@ -1,8 +1,7 @@
1
+ import { Behaviour, GameObject } from "./Component.js";
2
+ import * as utils from "./../engine/engine_three_utils.js";
1
3
  import { Vector3 } from "three";
2
4
 
3
- import * as utils from "./../engine/engine_three_utils.js";
4
- import { Behaviour, GameObject } from "./Component.js";
5
-
6
5
  export class BasicIKConstraint extends Behaviour {
7
6
 
8
7
  private from!: GameObject;
src/engine-components/export/usdz/extensions/behavior/Behaviour.ts CHANGED
@@ -1,8 +1,8 @@
1
- import { getParam } from "../../../../../engine/engine_utils.js";
2
1
  import { GameObject } from "../../../../Component.js";
3
2
  import type { IUSDExporterExtension } from "../../Extension.js";
4
3
  import { USDObject, USDWriter, USDZExporterContext } from "../../ThreeUSDZExporter.js";
5
4
  import { BehaviorModel } from "./BehavioursBuilder.js";
5
+ import { getParam } from "../../../../../engine/engine_utils.js";
6
6
 
7
7
  const debug = getParam("debugusdz");
8
8
 
src/engine-components/export/usdz/extensions/behavior/BehaviourComponents.ts CHANGED
@@ -1,20 +1,21 @@
1
- import { Group,Material, Mesh, Object3D, Quaternion, Vector3 } from "three";
2
-
3
- import { isDevEnvironment, showBalloonWarning } from "../../../../../engine/debug/index.js";
4
- import { serializable } from "../../../../../engine/engine_serialization_decorator.js";
5
- import { getWorldPosition, getWorldQuaternion, getWorldScale, setWorldPosition, setWorldQuaternion, setWorldScale } from "../../../../../engine/engine_three_utils.js";
6
- import type { State } from "../../../../../engine/extensions/NEEDLE_animator_controller_model.js";
7
- import { NEEDLE_progressive } from "../../../../../engine/extensions/NEEDLE_progressive.js";
1
+ import { Behaviour, GameObject } from "../../../../Component.js";
8
2
  import { Animator } from "../../../../Animator.js";
9
- import { AudioSource } from "../../../../AudioSource.js";
10
- import { Behaviour, GameObject } from "../../../../Component.js";
11
3
  import { Renderer } from "../../../../Renderer.js";
4
+ import { serializable } from "../../../../../engine/engine_serialization_decorator.js";
12
5
  import type { IPointerClickHandler, PointerEventData } from "../../../../ui/PointerEvents.js";
13
- import { ObjectRaycaster,Raycaster } from "../../../../ui/Raycaster.js";
6
+ import { AnimationExtension, RegisteredAnimationInfo, type UsdzAnimation } from "../Animation.js";
7
+ import { getWorldPosition, getWorldQuaternion, getWorldScale, setWorldPosition, setWorldQuaternion, setWorldScale } from "../../../../../engine/engine_three_utils.js";
8
+
9
+ import { Object3D, Material, Vector3, Quaternion, Mesh, Group } from "three";
14
10
  import { USDDocument, USDObject, USDZExporterContext } from "../../ThreeUSDZExporter.js";
15
- import { AnimationExtension, RegisteredAnimationInfo, type UsdzAnimation } from "../Animation.js";
11
+
16
12
  import type { BehaviorExtension, UsdzBehaviour } from "./Behaviour.js";
17
- import { ActionBuilder, ActionModel, AuralMode, BehaviorModel, GroupActionModel, type IBehaviorElement, MotionType, MultiplePerformOperation,PlayAction, Space, TriggerBuilder } from "./BehavioursBuilder.js";
13
+ import { ActionBuilder, ActionModel, AuralMode, BehaviorModel, type IBehaviorElement, MotionType, PlayAction, Space, TriggerBuilder, GroupActionModel, MultiplePerformOperation } from "./BehavioursBuilder.js";
14
+ import { AudioSource } from "../../../../AudioSource.js";
15
+ import { NEEDLE_progressive } from "../../../../../engine/extensions/NEEDLE_progressive.js";
16
+ import { isDevEnvironment, showBalloonWarning } from "../../../../../engine/debug/index.js";
17
+ import { Raycaster, ObjectRaycaster } from "../../../../ui/Raycaster.js";
18
+ import type { State } from "../../../../../engine/extensions/NEEDLE_animator_controller_model.js";
18
19
 
19
20
  function ensureRaycaster(obj: GameObject) {
20
21
  if (!obj) return;
src/engine-components/export/usdz/extensions/behavior/BehavioursBuilder.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { Object3D } from "three";
2
+ import { USDDocument, USDObject, USDWriter, makeNameSafeForUSD } from "../../ThreeUSDZExporter.js";
2
3
 
4
+ import { BehaviorExtension } from "./Behaviour.js";
3
5
  import { getParam } from "../../../../../engine/engine_utils.js";
4
- import { makeNameSafeForUSD,USDDocument, USDObject, USDWriter } from "../../ThreeUSDZExporter.js";
5
- import { BehaviorExtension } from "./Behaviour.js";
6
6
 
7
7
  const debug = getParam("debugusdz");
8
8
 
src/engine-components/postprocessing/Effects/Bloom.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import { BlendFunction, BloomEffect, SelectiveBloomEffect } from "postprocessing";
2
-
3
2
  import { serializable } from "../../../engine/engine_serialization.js";
4
3
  import { PostProcessingEffect } from "../PostProcessingEffect.js";
5
4
  import { VolumeParameter } from "../VolumeParameter.js";
src/engine-components/BoxHelperComponent.ts CHANGED
@@ -1,9 +1,8 @@
1
- import { Box3, Color, type ColorRepresentation, LineSegments, Object3D, Vector3 } from "three";
2
-
1
+ import { Behaviour } from "./Component.js";
2
+ import { getParam } from "../engine/engine_utils.js";
3
3
  import { CreateWireCube, Gizmos } from "../engine/engine_gizmos.js";
4
4
  import { getWorldPosition, getWorldScale } from "../engine/engine_three_utils.js";
5
- import { getParam } from "../engine/engine_utils.js";
6
- import { Behaviour } from "./Component.js";
5
+ import { Box3, Color, type ColorRepresentation, LineSegments, Object3D, Vector3 } from "three";
7
6
 
8
7
  const gizmos = getParam("gizmos");
9
8
  const debug = getParam("debugboxhelper");
src/engine-components/ui/Button.ts CHANGED
@@ -1,15 +1,15 @@
1
- import { showBalloonMessage } from "../../engine/debug/index.js";
2
- import { Gizmos } from "../../engine/engine_gizmos.js";
3
- import { PointerType } from "../../engine/engine_input.js";
4
- import { serializable } from "../../engine/engine_serialization_decorator.js";
5
- import { getParam } from "../../engine/engine_utils.js";
6
- import { Animator } from "../Animator.js";
7
1
  import { Behaviour, GameObject } from "../Component.js";
8
2
  import { EventList } from "../EventList.js";
3
+ import type { IPointerClickHandler, IPointerEnterHandler, IPointerEventHandler, IPointerExitHandler, PointerEventData } from "./PointerEvents.js";
4
+ import { Image } from "./Image.js";
9
5
  import { RGBAColor } from "../js-extensions/RGBAColor.js";
10
- import { Image } from "./Image.js";
11
- import type { IPointerClickHandler, IPointerEnterHandler, IPointerEventHandler, IPointerExitHandler, PointerEventData } from "./PointerEvents.js";
6
+ import { serializable } from "../../engine/engine_serialization_decorator.js";
7
+ import { Animator } from "../Animator.js";
8
+ import { getParam } from "../../engine/engine_utils.js";
9
+ import { showBalloonMessage } from "../../engine/debug/index.js";
12
10
  import { GraphicRaycaster, ObjectRaycaster, Raycaster } from "./Raycaster.js";
11
+ import { PointerType } from "../../engine/engine_input.js";
12
+ import { Gizmos } from "../../engine/engine_gizmos.js";
13
13
 
14
14
  const debug = getParam("debugbutton");
15
15
 
@@ -120,10 +120,10 @@
120
120
  }
121
121
 
122
122
  onPointerClick(args: PointerEventData) {
123
- if (!this.interactable) return;
123
+ if (!this.interactable || args.pointerId !== 0) return;
124
124
 
125
- if (args.button !== 0 && args.event.pointerType === PointerType.Mouse) return;
126
125
  // Button clicks should only run with left mouse button while using mouse
126
+ if(args.pointerId !== 0 && this.context.input.getIsMouse(args.pointerId)) return;
127
127
  if (debug) {
128
128
  console.warn("Button Click", this.onClick);
129
129
  showBalloonMessage("CLICKED button " + this.name + " at " + this.context.time.frameCount);
src/engine-components/Camera.ts CHANGED
@@ -1,17 +1,17 @@
1
- import { EquirectangularReflectionMapping, OrthographicCamera, PerspectiveCamera, Ray, SRGBColorSpace, Vector3 } from "three";
2
- import { Texture } from "three";
3
-
1
+ import { Behaviour, GameObject } from "./Component.js";
2
+ import { getParam } from "../engine/engine_utils.js";
3
+ import { serializable } from "../engine/engine_serialization_decorator.js";
4
+ import { RGBAColor } from "./js-extensions/RGBAColor.js";
5
+ import { Context, XRSessionMode } from "../engine/engine_setup.js";
6
+ import type { ICamera } from "../engine/engine_types.js"
4
7
  import { isDevEnvironment, showBalloonMessage, showBalloonWarning } from "../engine/debug/index.js";
8
+ import { getWorldPosition } from "../engine/engine_three_utils.js";
5
9
  import { Gizmos } from "../engine/engine_gizmos.js";
6
- import { serializable } from "../engine/engine_serialization_decorator.js";
7
- import { Context } from "../engine/engine_setup.js";
10
+
11
+ import { EquirectangularReflectionMapping, OrthographicCamera, PerspectiveCamera, Ray, SRGBColorSpace, Vector3 } from "three";
12
+ import { OrbitControls } from "./OrbitControls.js";
8
13
  import { RenderTexture } from "../engine/engine_texture.js";
9
- import { getWorldPosition } from "../engine/engine_three_utils.js";
10
- import type { ICamera } from "../engine/engine_types.js"
11
- import { getParam } from "../engine/engine_utils.js";
12
- import { Behaviour, GameObject } from "./Component.js";
13
- import { RGBAColor } from "./js-extensions/RGBAColor.js";
14
- import { OrbitControls } from "./OrbitControls.js";
14
+ import { Texture } from "three";
15
15
 
16
16
  export enum ClearFlags {
17
17
  Skybox = 1,
@@ -350,6 +350,7 @@
350
350
  if (this._backgroundBlurriness !== undefined)
351
351
  this.context.scene.backgroundBlurriness = this._backgroundBlurriness;
352
352
  if (this._backgroundIntensity !== undefined)
353
+ //@ts-ignore
353
354
  this.context.scene.backgroundIntensity = this._backgroundIntensity;
354
355
 
355
356
  break;
@@ -391,7 +392,7 @@
391
392
  if (debug)
392
393
  showBalloonMessage("Environment blend mode: " + environmentBlendMode + " on " + navigator.userAgent);
393
394
  let transparent = environmentBlendMode === 'additive' || environmentBlendMode === 'alpha-blend';
394
- if (context.isInAR) {
395
+ if (context.xrSessionMode === XRSessionMode.ImmersiveAR) {
395
396
  if (environmentBlendMode === "opaque") {
396
397
  // workaround for Quest 2 returning opaque when it should be alpha-blend
397
398
  // check user agent if this is the Quest browser and return true if so
src/engine-components/CameraUtils.ts CHANGED
@@ -1,15 +1,14 @@
1
+ import { OrbitControls } from "./OrbitControls.js";
2
+ import { addNewComponent, getOrAddComponent } from "../engine/engine_components.js";
1
3
  import { Object3D } from "three";
2
-
4
+ import type { ICamera, IContext } from "../engine/engine_types.js";
5
+ import { RGBAColor } from "./js-extensions/RGBAColor.js";
6
+ import { ContextEvent, ContextRegistry } from "../engine/engine_context_registry.js";
3
7
  import { getCameraController } from "../engine/engine_camera.js";
4
- import { addNewComponent, getOrAddComponent } from "../engine/engine_components.js";
5
- import { Context } from "../engine/engine_context.js";
6
- import { ContextEvent, ContextRegistry } from "../engine/engine_context_registry.js";
8
+ import { Camera, ClearFlags } from "./Camera.js";
7
9
  import { NeedleEngineHTMLElement } from "../engine/engine_element.js";
8
- import type { ICamera, IContext } from "../engine/engine_types.js";
9
10
  import { getParam } from "../engine/engine_utils.js";
10
- import { Camera, ClearFlags } from "./Camera.js";
11
- import { RGBAColor } from "./js-extensions/RGBAColor.js";
12
- import { OrbitControls } from "./OrbitControls.js";
11
+ import { Context } from "../engine/engine_context.js";
13
12
 
14
13
  const debug = getParam("debugmissingcamera");
15
14
 
src/engine-components/ui/Canvas.ts CHANGED
@@ -1,19 +1,17 @@
1
- import { Matrix4, Object3D } from "three";
2
- import * as ThreeMeshUI from 'three-mesh-ui'
3
-
4
- import { Mathf } from "../../engine/engine_math.js";
1
+ import { updateRenderSettings as updateRenderSettingsRecursive } from "./Utils.js";
5
2
  import { serializable } from "../../engine/engine_serialization_decorator.js";
6
3
  import { FrameEvent } from "../../engine/engine_setup.js";
7
- import { getParam } from "../../engine/engine_utils.js";
8
- import { NeedleXREventArgs } from "../../engine/xr/index.js";
4
+ import { BaseUIComponent, UIRootComponent } from "./BaseUIComponent.js";
5
+ import { GameObject } from "../Component.js";
6
+ import { Matrix4, Object3D } from "three";
7
+ import { RectTransform } from "./RectTransform.js";
8
+ import type { ICanvas, ICanvasEventReceiver, ILayoutGroup, IRectTransform } from "./Interfaces.js";
9
9
  import { Camera } from "../Camera.js";
10
- import { GameObject } from "../Component.js";
11
- import { BaseUIComponent, UIRootComponent } from "./BaseUIComponent.js";
12
10
  import { EventSystem } from "./EventSystem.js";
13
- import type { ICanvas, ICanvasEventReceiver, ILayoutGroup, IRectTransform } from "./Interfaces.js";
11
+ import * as ThreeMeshUI from 'three-mesh-ui'
12
+ import { getParam } from "../../engine/engine_utils.js";
14
13
  import { LayoutGroup } from "./Layout.js";
15
- import { RectTransform } from "./RectTransform.js";
16
- import { updateRenderSettings as updateRenderSettingsRecursive } from "./Utils.js";
14
+ import { Mathf } from "../../engine/engine_math.js";
17
15
 
18
16
  export enum RenderMode {
19
17
  ScreenSpaceOverlay = 0,
@@ -202,30 +200,19 @@
202
200
  }
203
201
  }
204
202
 
205
- onEnterXR(args: NeedleXREventArgs): void {
206
- if (this.screenspace) {
207
- if (args.xr.isVR || args.xr.isPassThrough) {
208
- this.gameObject.visible = false;
209
- }
210
- }
211
- }
212
- onLeaveXR(args: NeedleXREventArgs): void {
213
- if (this.screenspace) {
214
- if (args.xr.isVR || args.xr.isPassThrough) {
215
- this.gameObject.visible = true;
216
- }
217
- }
218
- }
219
-
220
203
  onBeforeRenderRoutine = () => {
221
- this.previousParent = this.gameObject.parent;
222
- if ((this.context.xr?.isVR || this.context.xr?.isPassThrough) && this.screenspace) {
223
- // see https://linear.app/needle/issue/NE-4114
224
- this.gameObject.visible = false;
225
- this.gameObject.removeFromParent();
204
+ if (this.context.isInVR) {
205
+ this.onUpdateRenderMode();
206
+ this.handleLayoutUpdates();
207
+ // TODO TMUI @swingingtom - For VR this is so we don't have text clipping
208
+ this.shadowComponent?.updateMatrixWorld(true);
209
+ this.shadowComponent?.updateWorldMatrix(true, true);
210
+ this.invokeBeforeRenderEvents();
211
+ EventSystem.ensureUpdateMeshUI(ThreeMeshUI, this.context, true);
226
212
  return;
227
213
  }
228
214
 
215
+ this.previousParent = this.gameObject.parent;
229
216
  // console.log(this.previousParent?.name + "/" + this.gameObject.name);
230
217
 
231
218
  if (this.renderOnTop || this.screenspace) {
@@ -244,12 +231,7 @@
244
231
  }
245
232
 
246
233
  onAfterRenderRoutine = () => {
247
- if ((this.context.xr?.isVR || this.context.xr?.isPassThrough) && this.screenspace) {
248
- this.previousParent?.add(this.gameObject);
249
- // this is currently causing an error during XR (https://linear.app/needle/issue/NE-4114)
250
- // this.gameObject.visible = true;
251
- return;
252
- }
234
+ if(this.context.isInVR) return;
253
235
  if ((this.screenspace || this.renderOnTop) && this.previousParent && this.context.mainCamera) {
254
236
  if (this.screenspace) {
255
237
  const camObj = this.context.mainCamera;
@@ -294,7 +276,7 @@
294
276
  for (const ch of this._rectTransforms) {
295
277
  if (matrixWorldChanged) ch.markDirty();
296
278
  let layout = this._layoutGroups.get(ch.gameObject);
297
- if (ch.isDirty && !layout) {
279
+ if(ch.isDirty && !layout){
298
280
  layout = ch.gameObject.getComponentInParent(LayoutGroup) as LayoutGroup;
299
281
  }
300
282
  if (ch.isDirty || layout?.isDirty) {
src/engine-components/ui/CanvasGroup.ts CHANGED
@@ -1,9 +1,9 @@
1
- import { serializable } from "../../engine/engine_serialization_decorator.js";
1
+ import { Graphic } from "./Graphic.js";
2
2
  import { FrameEvent } from "../../engine/engine_setup.js";
3
3
  import { Behaviour, GameObject } from "../Component.js";
4
+ import { type ICanvasGroup, type IHasAlphaFactor } from "./Interfaces.js";
5
+ import { serializable } from "../../engine/engine_serialization_decorator.js";
4
6
  import { BaseUIComponent } from "./BaseUIComponent.js";
5
- import { Graphic } from "./Graphic.js";
6
- import { type ICanvasGroup, type IHasAlphaFactor } from "./Interfaces.js";
7
7
 
8
8
 
9
9
  export class CanvasGroup extends Behaviour implements ICanvasGroup {
src/engine-components/CharacterController.ts CHANGED
@@ -1,15 +1,14 @@
1
1
  import { Quaternion, Ray, Vector2, Vector3 } from "three";
2
-
3
2
  import { Mathf } from "../engine/engine_math.js";
4
- import { RaycastOptions } from "../engine/engine_physics.js";
5
3
  import { serializable } from "../engine/engine_serialization.js";
6
- import { getWorldPosition } from "../engine/engine_three_utils.js";
7
4
  import { Collision } from "../engine/engine_types.js";
8
- import { getParam } from "../engine/engine_utils.js";
9
- import { Animator } from "./Animator.js"
10
5
  import { CapsuleCollider } from "./Collider.js";
11
6
  import { Behaviour, GameObject } from "./Component.js";
12
7
  import { Rigidbody } from "./RigidBody.js";
8
+ import { Animator } from "./Animator.js"
9
+ import { RaycastOptions } from "../engine/engine_physics.js";
10
+ import { getWorldPosition } from "../engine/engine_three_utils.js";
11
+ import { getParam } from "../engine/engine_utils.js";
13
12
 
14
13
  const debug = getParam("debugcharactercontroller");
15
14
 
src/engine-components/postprocessing/Effects/ChromaticAberration.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import { ChromaticAberrationEffect } from "postprocessing";
2
2
  import { Vector2 } from "three";
3
-
4
3
  import { serializable } from "../../../engine/engine_serialization.js";
5
4
  import { type EffectProviderResult, PostProcessingEffect } from "../PostProcessingEffect.js";
6
5
  import { VolumeParameter } from "../VolumeParameter.js";
src/engine-components/Collider.ts CHANGED
@@ -1,14 +1,13 @@
1
+ import { Behaviour } from "./Component.js";
2
+ import { Rigidbody } from "./RigidBody.js";
3
+ import { serializable } from "../engine/engine_serialization_decorator.js";
1
4
  import { Group, Mesh, Vector3 } from "three"
2
-
3
- import type { PhysicsMaterial } from "../engine/engine_physics.types.js";
4
- import { serializable } from "../engine/engine_serialization_decorator.js";
5
- import { getWorldScale } from "../engine/engine_three_utils.js";
6
5
  // import { IColliderProvider, registerColliderProvider } from "../engine/engine_physics.js";
7
6
  import type { IBoxCollider, ICollider, ISphereCollider } from "../engine/engine_types.js";
7
+ import { getWorldScale } from "../engine/engine_three_utils.js";
8
+ import type { PhysicsMaterial } from "../engine/engine_physics.types.js";
8
9
  import { validate } from "../engine/engine_util_decorator.js";
9
10
  import { unwatchWrite, watchWrite } from "../engine/engine_utils.js";
10
- import { Behaviour } from "./Component.js";
11
- import { Rigidbody } from "./RigidBody.js";
12
11
 
13
12
 
14
13
  export class Collider extends Behaviour implements ICollider {
src/engine-components/postprocessing/Effects/ColorAdjustments.ts CHANGED
@@ -1,10 +1,9 @@
1
1
  import { BrightnessContrastEffect, HueSaturationEffect } from "postprocessing";
2
- import { LinearToneMapping, NoToneMapping } from "three";
3
-
4
2
  import { serializable } from "../../../engine/engine_serialization.js";
5
3
  import { type EffectProviderResult, PostProcessingEffect } from "../PostProcessingEffect.js";
6
4
  import { VolumeParameter } from "../VolumeParameter.js";
7
5
  import { registerCustomEffectType } from "../VolumeProfile.js";
6
+ import { LinearToneMapping, NoToneMapping } from "three";
8
7
 
9
8
 
10
9
  export class ColorAdjustments extends PostProcessingEffect {
src/engine-components/Component.ts CHANGED
@@ -1,17 +1,15 @@
1
- import { Euler, Object3D, Quaternion, Scene, Vector3 } from "three";
2
-
3
- import { isDevEnvironment } from "../engine/debug/index.js";
4
- import { addNewComponent, destroyComponentInstance, findObjectOfType, findObjectsOfType, getComponent, getComponentInChildren, getComponentInParent, getComponents, getComponentsInChildren, getComponentsInParent, getOrAddComponent, moveComponentInstance, removeComponent } from "../engine/engine_components.js";
1
+ import { Mathf } from "../engine/engine_math.js";
2
+ import * as threeutils from "../engine/engine_three_utils.js";
5
3
  import { activeInHierarchyFieldName } from "../engine/engine_constants.js";
6
- import { destroy, findByGuid, foreachComponent, HideFlags, IInstantiateOptions, instantiate, isActiveInHierarchy, isActiveSelf, isDestroyed, isUsingInstancing, markAsInstancedRendered, setActive } from "../engine/engine_gameobject.js";
4
+ import { Context, FrameEvent } from "../engine/engine_setup.js";
7
5
  import * as main from "../engine/engine_mainloop_utils.js";
8
6
  import { syncDestroy, syncInstantiate } from "../engine/engine_networking_instantiate.js";
9
- import { Context, FrameEvent } from "../engine/engine_setup.js";
10
- import * as threeutils from "../engine/engine_three_utils.js";
11
- import type { Collision, Constructor, ConstructorConcrete, GuidsMap, ICollider, IComponent, IGameObject, SourceIdentifier } from "../engine/engine_types.js";
12
- import { INeedleXRSessionEventReceiver, NeedleXRControllerEventArgs, NeedleXREventArgs } from "../engine/engine_xr.js";
13
- import { IPointerEventHandler, PointerEventData } from "./ui/PointerEvents.js";
7
+ import type { ConstructorConcrete, SourceIdentifier, IComponent, IGameObject, Constructor, GuidsMap, Collision, ICollider } from "../engine/engine_types.js";
8
+ import { addNewComponent, destroyComponentInstance, findObjectOfType, findObjectsOfType, getComponent, getComponentInChildren, getComponentInParent, getComponents, getComponentsInChildren, getComponentsInParent, getOrAddComponent, moveComponentInstance, removeComponent } from "../engine/engine_components.js";
9
+ import { findByGuid, destroy, InstantiateOptions, instantiate, HideFlags, foreachComponent, markAsInstancedRendered, isActiveInHierarchy, isActiveSelf, isUsingInstancing, setActive, isDestroyed, IInstantiateOptions } from "../engine/engine_gameobject.js";
14
10
 
11
+ import { Euler, Object3D, Quaternion, Scene, Vector3 } from "three";
12
+ import { showBalloonWarning, isDevEnvironment } from "../engine/debug/index.js";
15
13
 
16
14
  // export interface ISerializationCallbackReceiver {
17
15
  // onBeforeSerialize?(): object | void;
@@ -125,7 +123,7 @@
125
123
  main.addScriptToArrays(comp, context!);
126
124
  if (comp.__internalDidAwakeAndStart) return;
127
125
  if (context!.new_script_start.includes(comp) === false) {
128
- context!.new_script_start.push(comp as Component);
126
+ context!.new_script_start.push(comp as Behaviour);
129
127
  }
130
128
  }, true);
131
129
  }
@@ -256,7 +254,7 @@
256
254
  return getComponentsInParent(go, typeName, arr);
257
255
  }
258
256
 
259
- public static getAllComponents(go: IGameObject | Object3D): Component[] {
257
+ public static getAllComponents(go: IGameObject | Object3D): Behaviour[] {
260
258
  const componentsList = go.userData?.components;
261
259
  const newList = [...componentsList];
262
260
  return newList;
@@ -296,7 +294,7 @@
296
294
  abstract set worldQuaternion(val: Quaternion);
297
295
  abstract get worldQuaternion(): Quaternion;
298
296
  abstract set worldRotation(val: Vector3);
299
- abstract get worldRotation(): Vector3;
297
+ abstract get worldRotation(): Vector3;
300
298
  abstract set worldScale(val: Vector3);
301
299
  abstract get worldScale(): Vector3;
302
300
 
@@ -307,28 +305,17 @@
307
305
 
308
306
 
309
307
 
310
- /** Needle Engine component base class. Derive from this component to implement your own using the provided lifecycle methods. Components can be added to threejs objects using `GameObject.addComponent`.
311
- *
312
- * The most common lifecycle methods are `awake`, `start`, `onEanble`, `onDisable` `update` and `onDestroy`.
313
- * XR specific callbacks include `onEnterXR`, `onLeaveXR`, `onUpdateXR`, `onControllerAdded` and `onControllerRemoved`.
314
- * To receive pointer events implement `onPointerDown`, `onPointerUp`, `onPointerEnter`, `onPointerExit` and `onPointerMove`.
315
- */
316
- export abstract class Component implements IComponent, EventTarget,
317
- Partial<INeedleXRSessionEventReceiver>,
318
- Partial<IPointerEventHandler>
319
- {
308
+ export class Component implements IComponent, EventTarget {
320
309
 
321
310
  get isComponent(): boolean { return true; }
322
311
 
323
312
  private __context: Context | undefined;
324
- /** Use the context to get access to many Needle Engine features and use physics, timing, access the camera or scene */
325
313
  get context(): Context {
326
314
  return this.__context ?? Context.Current;
327
315
  }
328
316
  set context(context: Context) {
329
317
  this.__context = context;
330
318
  }
331
- /** shorthand for `this.context.scene` */
332
319
  get scene(): Scene { return this.context.scene; }
333
320
 
334
321
  get layer(): number {
@@ -368,7 +355,7 @@
368
355
  return this.gameObject?.userData.hideFlags;
369
356
  }
370
357
 
371
- /** @returns true if the object is enabled and active in the hierarchy */
358
+
372
359
  get activeAndEnabled(): boolean {
373
360
  if (this.destroyed) return false;
374
361
  if (this.__isEnabled === false) return false;
@@ -398,27 +385,19 @@
398
385
  this.gameObject[activeInHierarchyFieldName] = val;
399
386
  }
400
387
 
401
- /** the object this component is attached to. Note that this is a threejs Object3D with some additional features */
402
388
  gameObject!: GameObject;
403
- /** the unique identifier for this component */
404
389
  guid: string = "invalid";
405
- /** holds the source identifier this object was created with/from (e.g. if it was part of a glTF file the sourceId holds the url to the glTF) */
406
390
  sourceId?: SourceIdentifier;
407
391
  // transform: Object3D = nullObject;
408
392
 
409
393
  /** called on a component with a map of old to new guids (e.g. when instantiate generated new guids and e.g. timeline track bindings needs to remape them) */
410
394
  resolveGuids?(guidsMap: GuidsMap): void;
411
395
 
412
- /** called once when the component becomes active for the first time (once per component)
413
- * This is the first callback to be called */
396
+ /** called once when the component becomes active for the first time */
414
397
  awake() { }
415
- /** called every time when the component gets enabled (this is invoked after awake and before start)
416
- * or when it becomes active in the hierarchy (e.g. if a parent object or this.gameObject gets set to visible)
417
- */
398
+ /** called every time when the component gets enabled (this is invoked after awake and before start) */
418
399
  onEnable() { }
419
- /** called every time the component gets disabled or if a parent object (or this.gameObject) gets set to invisible */
420
400
  onDisable() { }
421
- /** Called when the component gets destroyed */
422
401
  onDestroy() {
423
402
  this.__destroyed = true;
424
403
  }
@@ -430,17 +409,11 @@
430
409
  /** Called for all scripts when the context gets paused or unpaused */
431
410
  onPausedChanged?(isPaused: boolean, wasPaused: boolean): void;
432
411
 
433
- /** called at the beginning of a frame (once per component) */
434
412
  start?(): void;
435
- /** first callback in a frame (called every frame when implemented) */
436
413
  earlyUpdate?(): void;
437
- /** regular callback in a frame (called every frame when implemented) */
438
414
  update?(): void;
439
- /** late callback in a frame (called every frame when implemented) */
440
415
  lateUpdate?(): void;
441
- /** called before the scene gets rendered in the main update loop */
442
416
  onBeforeRender?(frame: XRFrame | null): void;
443
- /** called after the scene was rendered */
444
417
  onAfterRender?(): void;
445
418
 
446
419
  onCollisionEnter?(col: Collision);
@@ -451,79 +424,18 @@
451
424
  onTriggerStay?(col: ICollider);
452
425
  onTriggerExit?(col: ICollider);
453
426
 
454
-
455
- /** Optional callback, you can implement this to only get callbacks for VR or AR sessions if necessary.
456
- * @returns true if the mode is supported (if false the mode is not supported by this ciomponent and it will not receive XR callbacks for this mode)
457
- */
458
- supportsXR?(mode: XRSessionMode): boolean;
459
- /** Called before the XR session is requested. Use this callback if you want to modify the session init features */
460
- onBeforeXR?(mode: XRSessionMode, args: XRSessionInit): void;
461
- /** Callback when this component joins a xr session (or becomes active in a running XR session) */
462
- onEnterXR?(args: NeedleXREventArgs): void;
463
- /** Callback when a xr session updates (while it is still active in XR session) */
464
- onUpdateXR?(args: NeedleXREventArgs): void;
465
- /** Callback when this component exists a xr session (or when it becomes inactive in a running XR session) */
466
- onLeaveXR?(args: NeedleXREventArgs): void;
467
- /** Callback when a controller is connected/added while in a XR session
468
- * OR when the component joins a running XR session that has already connected controllers
469
- * OR when the component becomes active during a running XR session that has already connected controllers */
470
- onXRControllerAdded?(args: NeedleXRControllerEventArgs): void;
471
- /** callback when a controller is removed while in a XR session
472
- * OR when the component becomes inactive during a running XR session
473
- */
474
- onXRControllerRemoved?(args: NeedleXRControllerEventArgs): void;
475
-
476
-
477
- /* IPointerEventReceiver */
478
- /* @inheritdoc */
479
- onPointerEnter?(args: PointerEventData);
480
- onPointerMove?(args: PointerEventData);
481
- onPointerExit?(args: PointerEventData);
482
- onPointerDown?(args: PointerEventData);
483
- onPointerUp?(args: PointerEventData);
484
- onPointerClick?(args: PointerEventData);
485
-
486
-
487
- /** starts a coroutine (javascript generator function)
488
- * `yield` will wait for the next frame:
489
- * - Use `yield WaitForSeconds(1)` to wait for 1 second.
490
- * - Use `yield WaitForFrames(10)` to wait for 10 frames.
491
- * - Use `yield new Promise(...)` to wait for a promise to resolve.
492
- * @param routine generator function to start
493
- * @param evt event to register the coroutine for (default: FrameEvent.Update). Note that all coroutine FrameEvent callbacks are invoked after the matching regular component callbacks. For example `FrameEvent.Update` will be called after regular component `update()` methods)
494
- * @returns the generator function (use it to stop the coroutine with `stopCoroutine`)
495
- * @example
496
- * ```ts
497
- * onEnable() { this.startCoroutine(this.myCoroutine()); }
498
- * private *myCoroutine() {
499
- * while(this.activeAndEnabled) {
500
- * console.log("Hello World", this.context.time.frame);
501
- * // wait for 5 frames
502
- * for(let i = 0; i < 5; i++) yield;
503
- * }
504
- * }
505
- * ```
506
- */
507
427
  startCoroutine(routine: Generator, evt: FrameEvent = FrameEvent.Update): Generator {
508
428
  return this.context.registerCoroutineUpdate(this, routine, evt);
509
429
  }
510
- /**
511
- * Stop a coroutine that was previously started with `startCoroutine`
512
- * @param routine the routine to be stopped
513
- * @param evt the frame event to unregister the routine from (default: FrameEvent.Update)
514
- */
430
+
515
431
  stopCoroutine(routine: Generator, evt: FrameEvent = FrameEvent.Update): void {
516
432
  this.context.unregisterCoroutineUpdate(routine, evt);
517
433
  }
518
434
 
519
- /** @returns true if this component was destroyed (`this.destroy()`) or the whole object this component was part of */
520
435
  public get destroyed(): boolean {
521
436
  return this.__destroyed;
522
437
  }
523
438
 
524
- /**
525
- * Destroys this component (and removes it from the object)
526
- */
527
439
  public destroy() {
528
440
  if (this.__destroyed) return;
529
441
  this.__internalDestroy();
@@ -552,11 +464,7 @@
552
464
 
553
465
  /** @internal */
554
466
  constructor() {
555
- this.__didAwake = false;
556
- this.__didStart = false;
557
- this.__didEnable = false;
558
- this.__isEnabled = undefined;
559
- this.__destroyed = false;
467
+ this.__internalNewInstanceCreated();
560
468
  }
561
469
 
562
470
 
@@ -758,6 +666,5 @@
758
666
  }
759
667
  }
760
668
 
761
- // For legacy reasons we need to export this as well
762
- // (and we don't use extend to inherit the component docs)
763
- export { Component as Behaviour };
669
+ export class Behaviour extends Component {
670
+ }
src/engine-components/codegen/components.ts CHANGED
@@ -1,4 +1,3 @@
1
- /* eslint-disable */
2
1
  // Export types
3
2
  export class __Ignore {}
4
3
  export { ActionBuilder } from "../export/usdz/extensions/behavior/BehavioursBuilder.js";
@@ -12,11 +11,11 @@
12
11
  export { Animator } from "../Animator.js";
13
12
  export { AnimatorController } from "../AnimatorController.js";
14
13
  export { Antialiasing } from "../postprocessing/Effects/Antialiasing.js";
14
+ export { AttachedObject } from "../webxr/WebXRController.js";
15
15
  export { AudioExtension } from "../export/usdz/extensions/behavior/AudioExtension.js";
16
16
  export { AudioListener } from "../AudioListener.js";
17
17
  export { AudioSource } from "../AudioSource.js";
18
18
  export { AudioTrackHandler } from "../timeline/TimelineTracks.js";
19
- export { Avatar } from "../webxr/Avatar.js";
20
19
  export { Avatar_Brain_LookAt } from "../avatar/Avatar_Brain_LookAt.js";
21
20
  export { Avatar_MouthShapes } from "../avatar/Avatar_MouthShapes.js";
22
21
  export { Avatar_MustacheShake } from "../avatar/Avatar_MustacheShake.js";
@@ -31,6 +30,7 @@
31
30
  export { BasicIKConstraint } from "../BasicIKConstraint.js";
32
31
  export { BehaviorExtension } from "../export/usdz/extensions/behavior/Behaviour.js";
33
32
  export { BehaviorModel } from "../export/usdz/extensions/behavior/BehavioursBuilder.js";
33
+ export { Behaviour } from "../Component.js";
34
34
  export { Bloom } from "../postprocessing/Effects/Bloom.js";
35
35
  export { BoxCollider } from "../Collider.js";
36
36
  export { BoxGizmo } from "../Gizmos.js";
@@ -51,6 +51,7 @@
51
51
  export { ColorAdjustments } from "../postprocessing/Effects/ColorAdjustments.js";
52
52
  export { ColorBySpeedModule } from "../ParticleSystemModules.js";
53
53
  export { ColorOverLifetimeModule } from "../ParticleSystemModules.js";
54
+ export { Component } from "../Component.js";
54
55
  export { ContactShadows } from "../ContactShadows.js";
55
56
  export { ControlTrackHandler } from "../timeline/TimelineTracks.js";
56
57
  export { CustomBranding } from "../export/usdz/USDZExporter.js";
@@ -87,6 +88,7 @@
87
88
  export { Image } from "../ui/Image.js";
88
89
  export { InheritVelocityModule } from "../ParticleSystemModules.js";
89
90
  export { InputField } from "../ui/InputField.js";
91
+ export { Interactable } from "../Interactable.js";
90
92
  export { Light } from "../Light.js";
91
93
  export { LimitVelocityOverLifetimeModule } from "../ParticleSystemModules.js";
92
94
  export { LODGroup } from "../LODGroup.js";
@@ -100,7 +102,6 @@
100
102
  export { MeshRenderer } from "../Renderer.js";
101
103
  export { MinMaxCurve } from "../ParticleSystemModules.js";
102
104
  export { MinMaxGradient } from "../ParticleSystemModules.js";
103
- export { NeedleWebXRHtmlElement } from "../webxr/WebXRButtons.js";
104
105
  export { NestedGltf } from "../NestedGltf.js";
105
106
  export { Networking } from "../Networking.js";
106
107
  export { NoiseModule } from "../ParticleSystemModules.js";
@@ -124,6 +125,7 @@
124
125
  export { PreliminaryAction } from "../export/usdz/extensions/behavior/BehaviourComponents.js";
125
126
  export { PreliminaryTrigger } from "../export/usdz/extensions/behavior/BehaviourComponents.js";
126
127
  export { RawImage } from "../ui/Image.js";
128
+ export { Raycaster } from "../ui/Raycaster.js";
127
129
  export { Rect } from "../ui/RectTransform.js";
128
130
  export { RectTransform } from "../ui/RectTransform.js";
129
131
  export { ReflectionProbe } from "../ReflectionProbe.js";
@@ -151,7 +153,6 @@
151
153
  export { SizeOverLifetimeModule } from "../ParticleSystemModules.js";
152
154
  export { SkinnedMeshRenderer } from "../Renderer.js";
153
155
  export { SmoothFollow } from "../SmoothFollow.js";
154
- export { SpatialGrabRaycaster } from "../ui/Raycaster.js";
155
156
  export { SpatialHtml } from "../ui/SpatialHtml.js";
156
157
  export { SpatialTrigger } from "../SpatialTrigger.js";
157
158
  export { SpatialTriggerReceiver } from "../SpatialTrigger.js";
@@ -166,7 +167,7 @@
166
167
  export { SyncedRoom } from "../SyncedRoom.js";
167
168
  export { SyncedTransform } from "../SyncedTransform.js";
168
169
  export { TapGestureTrigger } from "../export/usdz/extensions/behavior/BehaviourComponents.js";
169
- export { TeleportTarget } from "../webxr/TeleportTarget.js";
170
+ export { TeleportTarget } from "../webxr/WebXRController.js";
170
171
  export { TestRunner } from "../TestRunner.js";
171
172
  export { TestSimulateUserData } from "../TestRunner.js";
172
173
  export { Text } from "../ui/Text.js";
@@ -196,16 +197,20 @@
196
197
  export { Volume } from "../postprocessing/Volume.js";
197
198
  export { VolumeParameter } from "../postprocessing/VolumeParameter.js";
198
199
  export { VolumeProfile } from "../postprocessing/VolumeProfile.js";
200
+ export { VRUserState } from "../webxr/WebXRSync.js";
201
+ export { WebAR } from "../webxr/WebXR.js";
199
202
  export { WebARCameraBackground } from "../webxr/WebARCameraBackground.js";
200
203
  export { WebARSessionRoot } from "../webxr/WebARSessionRoot.js";
201
204
  export { WebXR } from "../webxr/WebXR.js";
205
+ export { WebXRAvatar } from "../webxr/WebXRAvatar.js";
206
+ export { WebXRController } from "../webxr/WebXRController.js";
202
207
  export { WebXRImageTracking } from "../webxr/WebXRImageTracking.js";
203
208
  export { WebXRImageTrackingModel } from "../webxr/WebXRImageTracking.js";
204
209
  export { WebXRPlaneTracking } from "../webxr/WebXRPlaneTracking.js";
210
+ export { WebXRSync } from "../webxr/WebXRSync.js";
205
211
  export { WebXRTrackedImage } from "../webxr/WebXRImageTracking.js";
206
- export { XRControllerFollow } from "../webxr/controllers/XRControllerFollow.js";
207
- export { XRControllerModel } from "../webxr/controllers/XRControllerModel.js";
208
- export { XRControllerMovement } from "../webxr/controllers/XRControllerMovement.js";
209
- export { XRFlag } from "../webxr/XRFlag.js";
212
+ export { XRFlag } from "../XRFlag.js";
213
+ export { XRGrabModel } from "../webxr/WebXRGrabRendering.js";
214
+ export { XRGrabRendering } from "../webxr/WebXRGrabRendering.js";
210
215
  export { XRRig } from "../webxr/WebXRRig.js";
211
- export { XRState } from "../webxr/XRFlag.js";
216
+ export { XRState } from "../XRFlag.js";
src/engine-components/ContactShadows.ts CHANGED
@@ -1,11 +1,11 @@
1
+ import { Behaviour } from "./Component.js";
2
+ import { serializable } from "../engine/engine_serialization_decorator.js";
3
+
1
4
  import { CustomBlending, DoubleSide, Group, Matrix4, MaxEquation, Mesh, MeshBasicMaterial, MeshDepthMaterial, MinEquation, OrthographicCamera, PlaneGeometry, ShaderMaterial, WebGLRenderTarget } from "three";
2
5
  import { HorizontalBlurShader } from 'three/examples/jsm/shaders/HorizontalBlurShader.js';
3
6
  import { VerticalBlurShader } from 'three/examples/jsm/shaders/VerticalBlurShader.js';
4
-
5
- import { serializable } from "../engine/engine_serialization_decorator.js";
6
7
  import { getParam } from "../engine/engine_utils.js"
7
8
  import { setCustomVisibility } from "../engine/js-extensions/Layers.js";
8
- import { Behaviour } from "./Component.js";
9
9
 
10
10
  const debug = getParam("debugcontactshadows");
11
11
 
src/engine/debug/debug_console.ts CHANGED
@@ -1,7 +1,6 @@
1
+ import { getErrorCount } from "./debug_overlay.js";
2
+ import { getParam, isMobileDevice } from "../engine_utils.js";
1
3
  import { isLocalNetwork } from "../engine_networking_utils.js";
2
- import { getParam, isMobileDevice, isQuest } from "../engine_utils.js";
3
- import { isDevEnvironment } from "./debug.js";
4
- import { getErrorCount, makeErrorsVisibleForDevelopment } from "./debug_overlay.js";
5
4
 
6
5
  let consoleInstance: any = null;
7
6
  let consoleHtmlElement: HTMLElement | null = null;
@@ -23,11 +22,8 @@
23
22
  currentUrl.searchParams.set("console", "1");
24
23
  console.log("🌵 Tip: You can add the \"?console\" query parameter to the url to show the debug console (on mobile it will automatically open in the bottom right corner when your get errors during development)", "\nOpen this page console: " + currentUrl.toString());
25
24
  }
26
- const isMobile = isMobileDevice() || (isQuest() && isDevEnvironment());
25
+ const isMobile = isMobileDevice();
27
26
  if (isMobile) {
28
- // we need to invoke this here - otherwise we will miss errors that happen after the console is loaded
29
- // and calling the method from the root needle-engine.ts import is evaluated later (if we import the method from the toplevel file and then invoke it)
30
- makeErrorsVisibleForDevelopment();
31
27
  beginWatchingLogs();
32
28
  createConsole(true);
33
29
  if (isMobile) {
@@ -195,7 +191,7 @@
195
191
  }
196
192
  `;
197
193
  consoleHtmlElement?.prepend(styles);
198
- if (startHidden === true && getErrorCount() <= 0)
194
+ if (startHidden === true)
199
195
  hideDebugConsole();
200
196
  console.log("🌵 Debug console has loaded");
201
197
  }
src/engine/debug/debug_overlay.ts CHANGED
@@ -1,6 +1,6 @@
1
+ import { getParam } from "../engine_utils.js";
2
+ import { isLocalNetwork } from "../engine_networking_utils.js";
1
3
  import { ContextRegistry } from "../engine_context_registry.js";
2
- import { isLocalNetwork } from "../engine_networking_utils.js";
3
- import { getParam } from "../engine_utils.js";
4
4
 
5
5
  const debug = getParam("debugdebug");
6
6
  let hide = false;
@@ -15,7 +15,7 @@
15
15
  }
16
16
 
17
17
  export function getErrorCount() {
18
- return _errorCount;
18
+ return errorCount;
19
19
  }
20
20
 
21
21
  const originalConsoleError = console.error;
@@ -37,10 +37,9 @@
37
37
  if (hide) return;
38
38
  const isLocal = isLocalNetwork();
39
39
  if (debug) console.log("Is this a local network?", isLocal);
40
- if (isLocal)
41
- {
40
+ if (isLocal) {
42
41
  if (debug)
43
- console.warn("Patch console", window.location.hostname);
42
+ console.log(window.location.hostname);
44
43
  console.error = patchedConsoleError;
45
44
  window.addEventListener("error", (event) => {
46
45
  if (hide) return;
@@ -67,10 +66,10 @@
67
66
  }
68
67
 
69
68
 
70
- let _errorCount = 0;
69
+ let errorCount = 0;
71
70
 
72
71
  function onReceivedError() {
73
- _errorCount += 1;
72
+ errorCount += 1;
74
73
  }
75
74
 
76
75
  function onParseError(args: Array<any>) {
src/engine/debug/debug.ts CHANGED
@@ -1,6 +1,6 @@
1
+ import { addLog, LogType, setAllowOverlayMessages } from "./debug_overlay.js";
2
+ import { showDebugConsole } from "./debug_console.js";
1
3
  import { isLocalNetwork } from "../engine_networking_utils.js";
2
- import { showDebugConsole } from "./debug_console.js";
3
- import { addLog, LogType, setAllowOverlayMessages } from "./debug_overlay.js";
4
4
 
5
5
  export { showDebugConsole }
6
6
  export { LogType, setAllowOverlayMessages };
src/engine-components/DeleteBox.ts CHANGED
@@ -1,6 +1,5 @@
1
1
 
2
2
  import * as THREE from "three";
3
-
4
3
  import { syncDestroy } from "../engine/engine_networking_instantiate.js";
5
4
  import { getParam } from "../engine/engine_utils.js";
6
5
  import { BoxHelperComponent } from "./BoxHelperComponent.js";
src/engine-components/postprocessing/Effects/DepthOfField.ts CHANGED
@@ -1,8 +1,7 @@
1
1
  import { DepthOfFieldEffect } from "postprocessing";
2
-
2
+ import { serializable } from "../../../engine/engine_serialization.js";
3
3
  import { Mathf } from "../../../engine/engine_math.js";
4
- import { serializable } from "../../../engine/engine_serialization.js";
5
- import { isMobileDevice } from "../../../engine/engine_utils.js";
4
+ import { getParam, isMobileDevice } from "../../../engine/engine_utils.js";
6
5
  import { PostProcessingEffect } from "../PostProcessingEffect.js";
7
6
  import { VolumeParameter } from "../VolumeParameter.js";
8
7
  import { registerCustomEffectType } from "../VolumeProfile.js";
@@ -13,6 +12,8 @@
13
12
  Bokeh = 2,
14
13
  }
15
14
 
15
+ const debug = getParam("debugpost");
16
+
16
17
  export class DepthOfField extends PostProcessingEffect {
17
18
 
18
19
  get typeName() {
@@ -41,6 +42,8 @@
41
42
  bokehScale?: VolumeParameter;
42
43
 
43
44
  init() {
45
+ if (debug) console.log("DOF: INIT");
46
+
44
47
  this.focalLength.valueProcessor = v => {
45
48
  const t = v / 300;
46
49
  const max = 2;// this.context.mainCameraComponent?.farClipPlane ?? 10;
@@ -55,7 +58,10 @@
55
58
  }
56
59
 
57
60
  onCreateEffect() {
58
- if (this.mode === DepthOfFieldMode.Off) return undefined;
61
+ if (this.mode === DepthOfFieldMode.Off) {
62
+ if (debug) console.warn("DepthOfField: Mode is set to Off");
63
+ return undefined;
64
+ }
59
65
 
60
66
  const factor = 1 / window.devicePixelRatio;
61
67
 
@@ -78,9 +84,9 @@
78
84
  });
79
85
 
80
86
  this.focusDistance.onValueChanged = v => {
81
- dof.circleOfConfusionMaterial.worldFocusDistance = v;
87
+ dof.cocMaterial.worldFocusDistance = v;
82
88
  }
83
- this.focalLength.onValueChanged = v => dof.circleOfConfusionMaterial.worldFocusRange = v;
89
+ this.focalLength.onValueChanged = v => dof.cocMaterial.worldFocusRange = v;
84
90
  this.aperture.onValueChanged = v => dof.bokehScale = v;
85
91
 
86
92
  if (this.resolutionScale) this.resolutionScale.onValueChanged = v => dof.resolution.scale = v;
src/engine-components/DeviceFlag.ts CHANGED
@@ -1,6 +1,6 @@
1
1
 
2
+ import { isMobileDevice } from "../engine/engine_utils.js";
2
3
  import { serializable } from "../engine/engine_serialization_decorator.js";
3
- import { isMobileDevice } from "../engine/engine_utils.js";
4
4
  import { Behaviour, GameObject } from "./Component.js";
5
5
 
6
6
 
src/engine-components/DragControls.ts CHANGED
@@ -1,126 +1,104 @@
1
- import { AxesHelper, Box3, BufferGeometry, Camera, Color, Event, Line, LineBasicMaterial, Matrix3, Matrix4, Mesh, MeshBasicMaterial, Object3D, Plane, PlaneHelper, Quaternion, Ray, Raycaster, SphereGeometry, Vector3 } from "three";
2
-
3
- import { Gizmos } from "../engine/engine_gizmos.js";
4
- import { InstancingUtil } from "../engine/engine_instancing.js";
5
- import { Mathf } from "../engine/engine_math.js";
1
+ import { GameObject } from "./Component.js";
2
+ import { SyncedTransform } from "./SyncedTransform.js";
3
+ import type { IPointerDownHandler, IPointerEnterHandler, IPointerEventHandler, IPointerExitHandler, IPointerUpHandler, PointerEventData } from "./ui/PointerEvents.js";
4
+ import { Context } from "../engine/engine_setup.js";
5
+ import { Interactable, UsageMarker } from "./Interactable.js";
6
+ import { Rigidbody } from "./RigidBody.js";
7
+ import { WebXR } from "./webxr/WebXR.js";
8
+ import { Avatar_POI } from "./avatar/Avatar_Brain_LookAt.js";
6
9
  import { RaycastOptions } from "../engine/engine_physics.js";
7
- import { serializable } from "../engine/engine_serialization_decorator.js";
8
- import { Context } from "../engine/engine_setup.js";
9
10
  import { getWorldPosition, setWorldPosition } from "../engine/engine_three_utils.js";
10
- import { IGameObject } from "../engine/engine_types.js";
11
- import { getParam } from "../engine/engine_utils.js";
12
- import { NeedleXRSession } from "../engine/engine_xr.js";
13
- import { Avatar_POI } from "./avatar/Avatar_Brain_LookAt.js";
14
- import { Behaviour, GameObject } from "./Component.js";
15
- import { UsageMarker } from "./Interactable.js";
11
+ import type { KeyCode } from "../engine/engine_input.js";
12
+ import { nameofFactory } from "../engine/engine_utils.js";
13
+ import { InstancingUtil } from "../engine/engine_instancing.js";
16
14
  import { OrbitControls } from "./OrbitControls.js";
17
- import { Rigidbody } from "./RigidBody.js";
18
- import { SyncedTransform } from "./SyncedTransform.js";
19
- import type { IPointerEventHandler, PointerEventData } from "./ui/PointerEvents.js";
15
+ import { BufferGeometry, Camera, Color, Line, LineBasicMaterial, Matrix4, Mesh, MeshBasicMaterial, Object3D, Plane, Ray, Raycaster, SphereGeometry, Vector2, Vector3 } from "three";
20
16
  import { ObjectRaycaster } from "./ui/Raycaster.js";
17
+ import { serializable } from "../engine/engine_serialization_decorator.js";
21
18
 
22
- const debug = getParam("debugdrag");
19
+ const debug = false;
23
20
 
24
- export enum DragMode {
25
- /** Object stays at the same horizontal plane as it started. Commonly used for objects on the floor */
26
- XZPlane = 0,
27
- /** Object is dragged as if it was attached to the pointer. In 2D, that means it's dragged along the camera screen plane. In XR, it's dragged by the controller/hand. */
28
- Attached = 1,
29
- /** Object is dragged along the initial raycast hit normal. */
30
- HitNormal = 2,
31
- /** Combination of XZ and Screen based on the viewing angle. Low angles result in Screen dragging and higher angles in XZ dragging. */
32
- DynamicViewAngle = 3,
33
- /** The drag plane is adjusted dynamically while dragging. */
34
- SnapToSurfaces = 4,
21
+ export enum DragEvents {
22
+ SelectStart = "selectstart",
23
+ SelectEnd = "selectend",
35
24
  }
36
25
 
37
- export class DragControls extends Behaviour implements IPointerEventHandler {
26
+ interface SelectArgs {
27
+ selected: Object3D;
28
+ attached: Object3D | GameObject | null;
29
+ }
38
30
 
39
- // dragPlane (floor, object, view)
40
- // snap to surface (snap orientation?)
41
- // two-handed drag (scale, rotate, move)
42
- // keep upright (no tilt)
43
31
 
44
- /** How and where the object is dragged along. */
45
- @serializable()
46
- public dragMode: DragMode = DragMode.DynamicViewAngle;
32
+ export interface IDragEventListener {
33
+ onDragStart?();
34
+ onDragEnd?();
35
+ }
47
36
 
48
- /** Snap dragged objects to a XYZ grid – 0 means: no snapping. */
49
- @serializable()
50
- public snapGridResolution: number = 0.0;
51
-
52
- /** Keep the original rotation of the dragged object. */
53
- @serializable()
54
- public keepRotation: boolean = true;
55
-
56
- /** How and where the object is dragged along while dragging in XR. */
57
- @serializable()
58
- public xrDragMode: DragMode = DragMode.Attached;
37
+ export class DragControls extends Interactable implements IPointerEventHandler {
59
38
 
60
- /** Keep the original rotation of the dragged object while dragging in XR. */
61
- @serializable()
62
- public xrKeepRotation: boolean = false;
39
+ private static _active: number = 0;
40
+ public static get HasAnySelected(): boolean { return this._active > 0; }
63
41
 
64
- /** Accelerate dragging objects closer / further away when in XR */
42
+ /** Show's drag gizmos when enabled */
65
43
  @serializable()
66
- public xrDistanceDragFactor: number = 1;
44
+ public showGizmo: boolean = true;
67
45
 
68
- /** When enabled, draws a line from the dragged object downwards to the next raycast hit. */
46
+ /** When enabled DragControls will drag vertically when the object is viewed from a low angle */
69
47
  @serializable()
70
- public showGizmo: boolean = false;
48
+ public useViewAngle: boolean = true;
71
49
 
72
- // future:
73
- // constraints?
50
+ public transformSelf: boolean = true;
51
+ // public transformGroup: boolean = true;
52
+ // public targets: Object3D[] | null = null;
74
53
 
75
- public static get HasAnySelected(): boolean { return this._active > 0; }
76
- private static _active: number = 0;
77
-
78
- /** The object to be dragged – we pass this to handlers when they are created */
79
- private targetObject: GameObject | null = null;
54
+ // private controls: Control | null = null;
80
55
  private orbit: OrbitControls | null = null;
81
- private _dragHelper: LegacyDragVisualsHelper | null = null;
82
- private static lastHovered: Object3D;
83
- private _draggingRigidbodies: Rigidbody[] = [];
84
- private _potentialDragStartEvt: PointerEventData | null = null;
85
- private _dragHandlers: Map<Object3D, IDragHandler> = new Map();
86
- private _totalMovement: Vector3 = new Vector3();
87
- /** A marker is attached to components that are currently interacted with, to e.g. prevent them from being deleted. */
88
- private _marker: UsageMarker | null = null;
89
- private _isDragging: boolean = false;
90
- private _didDrag: boolean = false;
91
56
 
92
- setTargetObject(obj: Object3D | null) {
93
- this.targetObject = obj as GameObject;
94
- for (const handler of this._dragHandlers.values()) {
95
- handler.setTargetObject(obj);
96
- }
57
+ private selectStartEventListener: ((controls: DragControls, args: SelectArgs) => void)[] = [];
58
+ private selectEndEventListener: Array<Function> = [];
59
+ private _dragHelper: DragHelper | null = null;
60
+
61
+ constructor() {
62
+ super();
63
+ this.selectStartEventListener = [];
64
+ this.selectEndEventListener = [];
65
+ this._dragDelta = new Vector2();
97
66
  }
98
67
 
99
- awake() {
100
- // initialize all data that may be cloned incorrectly otherwise
101
- this._potentialDragStartEvt = null;
102
- this._dragHandlers = new Map();
103
- this._totalMovement = new Vector3();
104
- this._marker = null;
105
- this._isDragging = false;
106
- this._didDrag = false;
107
- this._dragHelper = null;
108
- this._draggingRigidbodies = [];
68
+
69
+ // TODO: Update DragEventListener code
70
+ addDragEventListener(type: DragEvents, cb: (ctrls: DragControls, args: SelectArgs) => void | Function) {
71
+ switch (type) {
72
+ case DragEvents.SelectStart:
73
+ this.selectStartEventListener.push(cb);
74
+ break;
75
+ case DragEvents.SelectEnd:
76
+ this.selectEndEventListener.push(cb);
77
+ break;
78
+ }
109
79
  }
110
80
 
81
+
82
+
111
83
  start() {
112
84
  this.orbit = GameObject.findObjectOfType(OrbitControls, this.context);
113
- if (!this.gameObject.getComponentInParent(ObjectRaycaster))
85
+ if (!this.gameObject.getComponentInParent(ObjectRaycaster)) {
114
86
  this.gameObject.addNewComponent(ObjectRaycaster);
87
+ }
115
88
  }
116
89
 
90
+ private static lastHovered: Object3D;
91
+ private _draggingRigidbodies: Rigidbody[] = [];
92
+
117
93
  private allowEdit(_obj: Object3D | null = null) {
118
94
  return this.context.connection.allowEditing;
119
95
  }
120
96
 
121
97
  onPointerEnter(evt: PointerEventData) {
122
98
  if (!this.allowEdit(this.gameObject)) return;
123
- if (evt.mode !== "screen") return;
99
+ if (WebXR.IsInWebXR) return;
100
+ // const interactable = GameObject.getComponentInParent(evt.object, Interactable);
101
+ // if (!interactable) return;
124
102
  const dc = GameObject.getComponentInParent(evt.object, DragControls);
125
103
  if (!dc || dc !== this) return;
126
104
  DragControls.lastHovered = evt.object;
@@ -129,121 +107,83 @@
129
107
 
130
108
  onPointerExit(evt: PointerEventData) {
131
109
  if (!this.allowEdit(this.gameObject)) return;
132
- if (evt.mode !== "screen") return;
110
+ if (WebXR.IsInWebXR) return;
133
111
  if (DragControls.lastHovered !== evt.object) return;
112
+ // const interactable = GameObject.getComponentInParent(evt.object, Interactable);
113
+ // if (!interactable) return;
134
114
  this.context.domElement.style.cursor = 'auto';
135
115
  }
136
116
 
117
+ private _waitingForDragStart: PointerEventData | null = null;
118
+
137
119
  onPointerDown(args: PointerEventData) {
138
120
  if (!this.allowEdit(this.gameObject)) return;
139
- if (args.used) return;
140
- DragControls.lastHovered = args.object;
141
-
142
- if (args.button === 0) {
143
- if (this._dragHandlers.size === 0) {
144
- this._didDrag = false;
145
- this._totalMovement.set(0, 0, 0);
146
- this._potentialDragStartEvt = args;
147
- }
148
-
149
- DragControls._active += 1;
150
-
151
- const newDragHandler = new DragPointerHandler(this, this.targetObject || this.gameObject);
152
- this._dragHandlers.set(args.event.space, newDragHandler);
153
-
154
- // We need to turn off OrbitControls immediately, otherwise they still get data for a short moment
155
- // and they don't properly handle being disabled while already processing data (smoothing happens when enabling again)
156
- if (this.orbit) this.orbit.enabled = false;
157
-
158
- newDragHandler.onDragStart(args);
159
-
160
- if (this._dragHandlers.size === 2) {
161
- const iterator = this._dragHandlers.values();
162
- const a = iterator.next().value;
163
- const b = iterator.next().value;
164
- const mtHandler = new MultiTouchDragHandler(this, this.targetObject || this.gameObject, a, b);
165
- this._dragHandlers.set(this.gameObject, mtHandler);
166
-
167
- mtHandler.onDragStart(args);
168
- }
169
-
170
- args.use();
171
- }
121
+ if (WebXR.IsInWebXR) return;
122
+ DragControls._active += 1;
123
+ this._dragDelta.set(0, 0);
124
+ this._didDrag = false;
125
+ // Clone to not modify the original event (and this event is used in the actual onDragStart method)
126
+ this._waitingForDragStart = args.clone();
127
+ args.stopPropagation();
128
+ // disabling pointer controls here already, otherwise we get a few frames of movement event in orbit controls and this will rotate the camera sligthly AFTER drag controls dragging ends.
129
+ if (this.orbit) this.orbit.enabled = false;
172
130
  }
173
131
 
174
132
  onPointerMove(args: PointerEventData) {
175
- if (this._isDragging || this._potentialDragStartEvt !== null) args.use();
133
+ if(this._isDragging || this._waitingForDragStart !== null) args.use();
176
134
  }
177
135
 
178
136
  onPointerUp(args: PointerEventData) {
179
-
180
- if(debug) Gizmos.DrawLabel(args.point ?? this.gameObject.worldPosition, "POINTERUP:" + args.pointerId + ", " + args.button, .03, 3);
181
-
137
+ this._waitingForDragStart = null;
182
138
  if (!this.allowEdit(this.gameObject)) return;
183
- if (args.button !== 0) return;
184
- this._potentialDragStartEvt = null;
139
+ if (DragControls._active > 0)
140
+ DragControls._active -= 1;
141
+ if (WebXR.IsInWebXR) return;
142
+ this.onDragEnd(args);
143
+ args.stopPropagation();
144
+ if (this.orbit) this.orbit.enabled = true;
145
+ }
185
146
 
186
- const handler = this._dragHandlers.get(args.event.space);
187
- const mtHandler = this._dragHandlers.get(this.gameObject) as MultiTouchDragHandler;
188
- if (mtHandler && (mtHandler.handlerA === handler || mtHandler.handlerB === handler)) {
189
- // any of the two handlers has been released, so we can remove the multi-touch handler
190
- this._dragHandlers.delete(this.gameObject);
191
- mtHandler.onDragEnd(args);
192
- }
193
147
 
194
- if (handler) {
195
- if (DragControls._active > 0)
196
- DragControls._active -= 1;
197
-
198
- if (handler.onDragEnd) handler.onDragEnd(args);
199
- this._dragHandlers.delete(args.event.space);
200
-
201
- if (this._dragHandlers.size === 0) {
202
- this.onLastDragEnd(args);
203
- }
204
- args.use();
205
- }
206
-
207
- if (DragControls._active === 0) {
208
- if (this.orbit) this.orbit.enabled = true;
209
- }
210
- }
211
-
212
148
  update(): void {
149
+ if (WebXR.IsInWebXR) return;
213
150
 
214
- for (const handler of this._dragHandlers.values()) {
215
- if (handler.collectMovementInfo) handler.collectMovementInfo();
216
- // TODO this doesn't make sense, we should instead just use the max here
217
- // or even better, each handler can decide on their own how to handle this
218
- if (handler.getTotalMovement) this._totalMovement.add(handler.getTotalMovement());
219
- }
220
-
221
151
  // drag start only after having dragged for some pixels
222
- if (this._potentialDragStartEvt) {
152
+ if (this._waitingForDragStart) {
223
153
  if (!this._didDrag) {
224
- // this is so we can e.g. process clicks without having a drag change the position, e.g. a click to call a method.
225
- // TODO probably needs to be treated differently for spatial (3D motion) and screen (2D pixel motion) drags
226
- if (this._totalMovement.length() > 0.0003)
154
+ // this is so we can e.g. process clicks without having a drag change the position
155
+ // e.g. a click to rotate the object
156
+ const delta = this.context.input.getPointerPositionDelta(0);
157
+ if (delta)
158
+ this._dragDelta.add(delta);
159
+ if (this._dragDelta.length() > 2)
227
160
  this._didDrag = true;
228
161
  else return;
229
162
  }
230
- const args = this._potentialDragStartEvt;
231
- this._potentialDragStartEvt = null;
232
- this.onFirstDragStart(args);
163
+ const args = this._waitingForDragStart;
164
+ this._waitingForDragStart = null;
165
+ this.onDragStart(args);
233
166
  }
234
167
 
235
- for (const handler of this._dragHandlers.values())
236
- if (handler.onDragUpdate) handler.onDragUpdate(this._dragHandlers.size);
237
-
238
- if (this._dragHelper && this._dragHelper.hasSelected)
239
- this.onAnyDragUpdate();
168
+ if (this._dragHelper && this._dragHelper.hasSelected) {
169
+ this.onUpdateDrag();
170
+ }
171
+
172
+ if (this._dragHelper?.hasSelected === false || (this._activePointerId !== undefined && this.context.input.getPointerPressed(this._activePointerId) === false)) {
173
+ this.onDragEnd(null);
174
+ }
240
175
  }
241
176
 
242
- /** Called when the first pointer starts dragging on this object. Not called for subsequent pointers on the same object. */
243
- private onFirstDragStart(evt: PointerEventData) {
177
+ private _isDragging: boolean = false;
178
+ private _marker: UsageMarker | null = null;
179
+ private _dragDelta!: Vector2;
180
+ private _didDrag: boolean = false;
181
+ private _activePointerId?: number;
182
+
183
+ private onDragStart(evt: PointerEventData) {
244
184
  if (!this._dragHelper) {
245
185
  if (this.context.mainCamera)
246
- this._dragHelper = new LegacyDragVisualsHelper(this.context.mainCamera);
186
+ this._dragHelper = new DragHelper(this.context.mainCamera);
247
187
  else
248
188
  return;
249
189
  }
@@ -252,17 +192,46 @@
252
192
  const dc = GameObject.getComponentInParent(evt.object, DragControls);
253
193
  if (!dc || dc !== this) return;
254
194
 
255
- const object = this.targetObject || this.gameObject;
256
195
 
257
- if (!object) return;
196
+ let object: Object3D = evt.object;
258
197
 
198
+ if (this.transformSelf) {
199
+ object = this.gameObject;
200
+ }
201
+
202
+ // raise event
203
+ const args: { selected: Object3D, attached: Object3D | null } = { selected: object, attached: object };
204
+ for (const listener of this.selectStartEventListener) {
205
+ listener(this, args);
206
+ }
207
+
208
+ this._activePointerId = evt.pointerId;
209
+
210
+ if (!args.attached) return;
211
+ if (args.attached !== object) {
212
+ // // if duplicatable changes the object being dragged
213
+ // // should it also change the active drag controls (e.g. if it has a own one)
214
+ // const drag = GameObject.getComponentInParent(args.attached, DragControls);
215
+ // if (drag && drag !== this) {
216
+ // // incredibly ugly code to pass the drag controls event to another drag controls instance
217
+ // // This is necessary since we dont call the onPointerUp events anymore for all objects
218
+ // // that have previously received the onPointerDown event.
219
+ // // NOTE: added the EventSystem.raisedPointerDownEvents array again because of this uglyness here. The code was originally removed in 757fc5e5bafd02aa13d6cd35dd5e8729c841465a and now we're adding it in 8ce886d8344d1abd5ebb89ae3e1fb8d6d47293da
220
+ // this.onDragEnd(null);
221
+ // drag.onPointerDown(evt);
222
+ // evt.object = args.attached;
223
+ // drag.onDragStart(evt);
224
+ // return;
225
+ // }
226
+ }
227
+ object = args.attached;
259
228
  this._isDragging = true;
260
229
  this._dragHelper.setSelected(object, this.context);
261
230
  if (this.orbit) this.orbit.enabled = false;
262
231
 
263
232
  const sync = GameObject.getComponentInChildren(object, SyncedTransform);
264
- if (debug) console.log("DRAG START", sync, object);
265
-
233
+ if (debug)
234
+ console.log("DRAG START", sync, object);
266
235
  if (sync) {
267
236
  sync.fastMode = true;
268
237
  sync?.requestOwnership();
@@ -270,31 +239,30 @@
270
239
 
271
240
  this._marker = GameObject.addNewComponent(object, UsageMarker);
272
241
 
242
+ // console.log(object, this._marker);
243
+
273
244
  this._draggingRigidbodies.length = 0;
274
245
  const rbs = GameObject.getComponentsInChildren(object, Rigidbody);
275
246
  if (rbs)
276
247
  this._draggingRigidbodies.push(...rbs);
248
+
249
+ const l = nameofFactory<IDragEventListener>();
250
+ GameObject.invokeOnChildren(this._dragHelper.selected, l("onDragStart"));
277
251
  }
278
252
 
279
- /** Called each frame as long as any pointer is dragging this object. */
280
- private onAnyDragUpdate() {
253
+ private onUpdateDrag() {
281
254
  if (!this._dragHelper) return;
282
255
  this._dragHelper.showGizmo = this.showGizmo;
256
+ this._dragHelper.useViewAngle = this.useViewAngle;
283
257
 
284
258
  this._dragHelper.onUpdate(this.context);
285
259
  for (const rb of this._draggingRigidbodies) {
286
260
  rb.wakeUp();
287
261
  rb.resetVelocities();
288
- rb.resetForcesAndTorques();
289
262
  }
290
-
291
- const object = this.targetObject || this.gameObject;
292
-
293
- InstancingUtil.markDirty(object);
294
263
  }
295
264
 
296
- /** Called when the last pointer has been removed from this object. */
297
- private onLastDragEnd(evt: PointerEventData | null) {
265
+ private onDragEnd(evt: PointerEventData | null) {
298
266
  if (!this || !this._isDragging) return;
299
267
  this._isDragging = false;
300
268
  if (!this._dragHelper) return;
@@ -303,7 +271,8 @@
303
271
  }
304
272
  this._draggingRigidbodies.length = 0;
305
273
  const selected = this._dragHelper.selected;
306
- if (debug) console.log("DRAG END", selected, selected?.visible)
274
+ if (debug)
275
+ console.log("DRAG END", selected, selected?.visible)
307
276
  this._dragHelper.setSelected(null, this.context);
308
277
  if (this.orbit) this.orbit.enabled = true;
309
278
  if (evt?.object) {
@@ -313,751 +282,23 @@
313
282
  // sync?.requestOwnership();
314
283
  }
315
284
  }
316
- if (this._marker)
285
+ if (this._marker) {
317
286
  this._marker.destroy();
318
- }
319
- }
320
-
321
- /** Handles two touch points affecting one object. Allows movement, scale and rotation of objects. */
322
- class MultiTouchDragHandler implements IDragHandler {
323
-
324
- handlerA: DragPointerHandler;
325
- handlerB: DragPointerHandler;
326
-
327
- private context: Context;
328
- private settings: DragControls;
329
- private gameObject: GameObject;
330
- private _handlerAAttachmentPoint: Vector3 = new Vector3();
331
- private _handlerBAttachmentPoint: Vector3 = new Vector3();
332
-
333
- private _followObject: GameObject;
334
- private _manipulatorObject: GameObject;
335
- private _deviceMode!: XRTargetRayMode;
336
- private _followObjectStartWorldQuaternion: Quaternion = new Quaternion();
337
-
338
- constructor(dragControls: DragControls, gameObject: GameObject, pointerA: DragPointerHandler, pointerB: DragPointerHandler) {
339
- this.context = dragControls.context;
340
- this.settings = dragControls;
341
- this.gameObject = gameObject;
342
- this.handlerA = pointerA;
343
- this.handlerB = pointerB;
344
-
345
- this._followObject = new Object3D() as GameObject;
346
- this._manipulatorObject = new Object3D() as GameObject;
347
-
348
- this.context.scene.add(this._manipulatorObject);
349
-
350
- const rig = NeedleXRSession.active?.rig?.gameObject;
351
-
352
- if (!this.handlerA || !this.handlerB || !this.handlerA.hitPointInLocalSpace || !this.handlerB.hitPointInLocalSpace) {
353
- console.error("Invalid: MultiTouchDragHandler needs two valid DragPointerHandlers with hitPointInLocalSpace set.");
354
- return;
355
287
  }
356
-
357
- this._tempVec1.copy(this.handlerA.hitPointInLocalSpace);
358
- this._tempVec2.copy(this.handlerB.hitPointInLocalSpace);
359
- this.gameObject.localToWorld(this._tempVec1);
360
- this.gameObject.localToWorld(this._tempVec2);
361
- if (rig) {
362
- rig.worldToLocal(this._tempVec1);
363
- rig.worldToLocal(this._tempVec2);
288
+ // raise event
289
+ for (const listener of this.selectEndEventListener) {
290
+ listener(this);
364
291
  }
365
- this._initialDistance = this._tempVec1.distanceTo(this._tempVec2);
366
-
367
- if (this._initialDistance < 0.02) {
368
- if (debug) {
369
- console.log("Finding alternative drag attachment points since initial distance is too low: " + this._initialDistance.toFixed(2));
370
- }
371
- // We want two reasonable pointer attachment points here.
372
- // But if the hitPointInLocalSpace are very close to each other, we instead fall back to controller positions.
373
- this.handlerA.followObject.parent!.getWorldPosition(this._tempVec1);
374
- this.handlerB.followObject.parent!.getWorldPosition(this._tempVec2);
375
- this._handlerAAttachmentPoint.copy(this._tempVec1);
376
- this._handlerBAttachmentPoint.copy(this._tempVec2);
377
- this.gameObject.worldToLocal(this._handlerAAttachmentPoint);
378
- this.gameObject.worldToLocal(this._handlerBAttachmentPoint);
379
- this._initialDistance = this._tempVec1.distanceTo(this._tempVec2);
380
292
 
381
- if (this._initialDistance < 0.001) {
382
- console.warn("Not supported right now – controller drag points for multitouch are too close!");
383
- this._initialDistance = 1;
384
- }
385
- }
386
- else {
387
- this._handlerAAttachmentPoint.copy(this.handlerA.hitPointInLocalSpace);
388
- this._handlerBAttachmentPoint.copy(this.handlerB.hitPointInLocalSpace);
389
- }
390
-
391
- this._tempVec3.lerpVectors(this._tempVec1, this._tempVec2, 0.5);
392
- this._initialScale.copy(gameObject.scale);
393
-
394
- if (debug) {
395
- this._followObject.add(new AxesHelper(2));
396
- this._manipulatorObject.add(new AxesHelper(5));
397
-
398
- const formatVec = (v: Vector3) => `${v.x.toFixed(2)}, ${v.y.toFixed(2)}, ${v.z.toFixed(2)}`;
399
- Gizmos.DrawLine(this._tempVec1, this._tempVec2, 0x00ffff, 0, false);
400
- Gizmos.DrawLabel(this._tempVec3, "A:B " + this._initialDistance.toFixed(2) + "\n" + formatVec(this._tempVec1) + "\n" + formatVec(this._tempVec2), 0.03, 5);
401
- }
293
+ const l = nameofFactory<IDragEventListener>();
294
+ GameObject.invokeOnChildren(selected, l("onDragEnd"));
402
295
  }
403
-
404
- onDragStart(_args: PointerEventData): void {
405
- // align _followObject with the object we want to drag
406
- this.gameObject.add(this._followObject);
407
- this._followObject.matrixAutoUpdate = false;
408
- this._followObject.matrix.identity();
409
- this._deviceMode = _args.mode;
410
- this._followObjectStartWorldQuaternion.copy(this._followObject.worldQuaternion);
411
-
412
- // align _manipulatorObject in the same way it would if this was a drag update
413
- this.alignManipulator();
414
-
415
- // and then parent it to the space object so it follows along.
416
- this._manipulatorObject.attach(this._followObject);
417
-
418
- // store offsets in local space
419
- this._manipulatorPosOffset.copy(this._followObject.position);
420
- this._manipulatorRotOffset.copy(this._followObject.quaternion);
421
- this._manipulatorScaleOffset.copy(this._followObject.scale);
422
- }
423
-
424
- onDragEnd(_args: PointerEventData): void {
425
- if (!this.handlerA || !this.handlerB) {
426
- console.error("onDragEnd called on MultiTouchDragHandler without valid handlers. This is likely a bug.");
427
- return;
428
- }
429
-
430
- // we want to initialize the drag points for these handlers again.
431
- // one of them will be removed, but we don't know here which one
432
- this.handlerA.recenter();
433
- this.handlerB.recenter();
434
-
435
- // destroy helper objects
436
- this._manipulatorObject.removeFromParent();
437
- this._followObject.removeFromParent();
438
- this._manipulatorObject.destroy();
439
- this._followObject.destroy();
440
- }
441
-
442
- private _manipulatorPosOffset: Vector3 = new Vector3();
443
- private _manipulatorRotOffset: Quaternion = new Quaternion();
444
- private _manipulatorScaleOffset: Vector3 = new Vector3();
445
-
446
- private _tempVec1: Vector3 = new Vector3();
447
- private _tempVec2: Vector3 = new Vector3();
448
- private _tempVec3: Vector3 = new Vector3();
449
- private tempLookMatrix: Matrix4 = new Matrix4();
450
- private _initialScale: Vector3 = new Vector3();
451
- private _initialDistance: number = 0;
452
-
453
- private alignManipulator() {
454
- this._tempVec1.copy(this._handlerAAttachmentPoint);
455
- this._tempVec2.copy(this._handlerBAttachmentPoint);
456
- this.handlerA.followObject.localToWorld(this._tempVec1);
457
- this.handlerB.followObject.localToWorld(this._tempVec2);
458
- this._tempVec3.lerpVectors(this._tempVec1, this._tempVec2, 0.5);
459
-
460
- this._manipulatorObject.position.copy(this._tempVec3);
461
-
462
- // - lookAt the second point on handlerB
463
- const camera = this.context.mainCamera;
464
- this.tempLookMatrix.lookAt(this._tempVec3, this._tempVec2, (camera as any as IGameObject).worldUp);
465
- this._manipulatorObject.quaternion.setFromRotationMatrix(this.tempLookMatrix);
466
-
467
- // - scale based on the distance between the two points
468
- const dist = this._tempVec1.distanceTo(this._tempVec2);
469
- this._manipulatorObject.scale.copy(this._initialScale).multiplyScalar(dist / this._initialDistance);
470
-
471
- this._manipulatorObject.updateMatrix();
472
- this._manipulatorObject.updateMatrixWorld(true);
473
-
474
- if (debug) {
475
- Gizmos.DrawLabel(this._tempVec3.clone().add(new Vector3(0,0.2,0)), "A:B " + dist.toFixed(2), 0.03);
476
- Gizmos.DrawLine(this._tempVec1, this._tempVec2, 0x00ff00, 0, false);
477
-
478
- // const wp = this._manipulatorObject.worldPosition;
479
- // Gizmos.DrawWireSphere(wp, this._initialScale.length() * dist / this._initialDistance, 0x00ff00, 0, false);
480
- }
481
- }
482
-
483
- onDragUpdate() {
484
- // At this point we've run both the other handlers, but their effects have been suppressed because they can't handle
485
- // two events at the same time. They're basically providing us with two Object3D's and we can combine these here
486
- // into a reasonable two-handed translation/rotation/scale.
487
- // One approach:
488
- // - position our control object on the center between the two pointer control objects
489
-
490
- // TODO close grab needs to be handled differently because there we don't have a hit point -
491
- // Hit point is just the center of the object
492
- // So probably we should fix that close grab has a better hit point approximation (point on bounds?)
493
-
494
- this.alignManipulator();
495
-
496
- // apply (smoothed) to the gameObject
497
- const lerpStrength = 30;
498
- const lerpFactor = 1.0;
499
-
500
- this._followObject.position.copy(this._manipulatorPosOffset);
501
- this._followObject.quaternion.copy(this._manipulatorRotOffset);
502
- this._followObject.scale.copy(this._manipulatorScaleOffset);
503
-
504
- const draggedObject = this.gameObject;
505
- const targetObject = this._followObject;
506
-
507
- targetObject.updateMatrix();
508
- targetObject.updateMatrixWorld(true);
509
-
510
- const isSpatialInput = this._deviceMode === "tracked-pointer";
511
- const keepRotation = isSpatialInput ? this.settings.xrKeepRotation : this.settings.keepRotation;
512
-
513
- // TODO refactor to a common place
514
- // apply constraints (position grid snap, rotation, ...)
515
- if (this.settings.snapGridResolution > 0) {
516
- const wp = this._followObject.worldPosition;
517
- const snap = this.settings.snapGridResolution;
518
- wp.x = Math.round(wp.x / snap) * snap;
519
- wp.y = Math.round(wp.y / snap) * snap;
520
- wp.z = Math.round(wp.z / snap) * snap;
521
- this._followObject.worldPosition = wp;
522
- this._followObject.updateMatrix();
523
- }
524
- if (keepRotation) {
525
- this._followObject.worldQuaternion = this._followObjectStartWorldQuaternion;
526
- this._followObject.updateMatrix();
527
- }
528
-
529
- // TODO refactor to a common place
530
- // TODO should use unscaled time here // some test for lerp speed depending on distance
531
- const t = Mathf.clamp01(this.context.time.deltaTime * lerpStrength * lerpFactor);// / (currentDist - 1 + 0.01));
532
-
533
- const wp = draggedObject.worldPosition;
534
- wp.lerp(targetObject.worldPosition, t);
535
- draggedObject.worldPosition = wp;
536
-
537
- const rot = draggedObject.worldQuaternion;
538
- rot.slerp(targetObject.worldQuaternion, t);
539
- draggedObject.worldQuaternion = rot;
540
-
541
- const scl = draggedObject.worldScale;
542
- scl.lerp(targetObject.worldScale, t);
543
- draggedObject.worldScale = scl;
544
- }
545
-
546
- setTargetObject(obj: Object3D | null): void {
547
- this.gameObject = obj as GameObject;
548
- }
549
296
  }
550
297
 
551
- /** Common interface for pointer handlers (single touch and multi touch) */
552
- interface IDragHandler {
553
- /** Used to determine if a drag has happened for this handler */
554
- getTotalMovement?(): Vector3;
555
- /** Target object can change mid-flight (e.g. in Duplicatable), handlers should react properly to that */
556
- setTargetObject(obj: Object3D | null): void;
557
-
558
- /** Prewarms the drag – can already move internal points around here but should not move the object itself */
559
- collectMovementInfo?(): void;
560
- onDragStart?(args: PointerEventData): void;
561
- onDragEnd?(args: PointerEventData): void;
562
- /** The target object is moved around */
563
- onDragUpdate?(numberOfPointers: number): void;
564
- }
565
298
 
566
- /** Handles a single pointer on an object. DragPointerHandlers are created on pointer enter,
567
- * help with determining if there's actually a drag, and then perform operations based on spatial pointer data.
568
- */
569
- class DragPointerHandler implements IDragHandler {
570
299
 
571
- /** Absolute movement of the pointer. Used for determining if a motion/drag is happening.
572
- * This is in world units, so very small for screens (near-plane space change) */
573
- getTotalMovement(): Vector3 { return this._totalMovement; }
574
- get followObject(): GameObject { return this._followObject; }
575
- get hitPointInLocalSpace(): Vector3 { return this._hitPointInLocalSpace; }
300
+ class DragHelper {
576
301
 
577
- private context: Context;
578
- private gameObject: GameObject;
579
- private settings: DragControls;
580
- private _lastRig: IGameObject | undefined = undefined;
581
-
582
- /** This object is placed at the pivot of the dragged object, and parented to the control space. */
583
- private _followObject: GameObject;
584
- private _totalMovement: Vector3 = new Vector3();
585
- /** Motion along the pointer ray. On screens this doesn't change. In XR it can be used to determine how much
586
- * effort someone is putting into moving an object closer or further away. */
587
- private _totalMovementAlongRayDirection: number = 0;
588
- /** Distance between _followObject and its parent at grab start, in local space */
589
- private _grabStartDistance: number = 0;
590
- private _deviceMode!: XRTargetRayMode;
591
- private _followObjectStartPosition: Vector3 = new Vector3();
592
- private _followObjectStartQuaternion: Quaternion = new Quaternion();
593
- private _followObjectStartWorldQuaternion: Quaternion = new Quaternion();
594
- private _lastDragPosRigSpace: Vector3 | undefined;
595
- private _tempVec: Vector3 = new Vector3();
596
- private _tempMat: Matrix4 = new Matrix4();
597
-
598
- private _hitPointInLocalSpace: Vector3 = new Vector3();
599
- private _hitNormalInLocalSpace: Vector3 = new Vector3();
600
- private _bottomCenter = new Vector3();
601
- private _backCenter = new Vector3();
602
- private _backBottomCenter = new Vector3();
603
- private _bounds = new Box3();
604
- private _dragPlane = new Plane(new Vector3(0, 1, 0));
605
- private _draggedOverObject: Object3D | null = null;
606
- private _draggedOverObjectLastSetUp: Object3D | null = null;
607
- private _draggedOverObjectLastNormal: Vector3 = new Vector3();
608
- private _draggedOverObjectDuration: number = 0;
609
-
610
- /** Allows overriding which object is dragged while a drag is already ongoing. Used for example by Duplicatable */
611
- setTargetObject(obj: Object3D | null) {
612
- this.gameObject = obj as GameObject;
613
- }
614
-
615
- constructor(dragControls: DragControls, gameObject: GameObject) {
616
- this.settings = dragControls;
617
- this.context = dragControls.context;
618
- this.gameObject = gameObject;
619
- this._followObject = new Object3D() as GameObject;
620
- }
621
-
622
- recenter() {
623
- if (!this._followObject.parent) {
624
- console.warn("Error: space follow object doesn't have parent but recenter() is called. This is likely a bug");
625
- return;
626
- }
627
-
628
- const p = this._followObject.parent as GameObject;
629
-
630
- this.gameObject.add(this._followObject);
631
- this._followObject.matrixAutoUpdate = false;
632
-
633
- this._followObject.position.set(0, 0, 0);
634
- this._followObject.quaternion.set(0, 0, 0, 1);
635
- this._followObject.scale.set(1, 1, 1);
636
-
637
- this._followObject.updateMatrix();
638
- this._followObject.updateMatrixWorld(true);
639
-
640
- p.attach(this._followObject);
641
-
642
- this._followObjectStartPosition.copy(this._followObject.position);
643
- this._followObjectStartQuaternion.copy(this._followObject.quaternion);
644
- this._followObjectStartWorldQuaternion.copy(this._followObject.worldQuaternion);
645
-
646
- this._followObject.updateMatrix();
647
- this._followObject.updateMatrixWorld(true);
648
-
649
- const hitPointWP = this._hitPointInLocalSpace.clone();
650
- this.gameObject.localToWorld(hitPointWP);
651
- this._grabStartDistance = hitPointWP.distanceTo(p.worldPosition);
652
- const rig = NeedleXRSession.active?.rig?.gameObject;
653
- const rigScale = rig?.worldScale.x || 1;
654
- this._grabStartDistance /= rigScale;
655
-
656
- this._totalMovementAlongRayDirection = 0;
657
- this._lastDragPosRigSpace = undefined;
658
-
659
- if (debug)
660
- {
661
- Gizmos.DrawLine(hitPointWP, p.worldPosition, 0x00ff00, 0.5, false);
662
- Gizmos.DrawLabel(p.worldPosition.add(new Vector3(0,0.1,0)), this._grabStartDistance.toFixed(2), 0.03, 0.5);
663
- }
664
- }
665
-
666
- onDragStart(args: PointerEventData) {
667
-
668
- args.event.space.add(this._followObject);
669
-
670
- // prepare for drag, we will start dragging after an object has been dragged for a few centimeters
671
- this._lastDragPosRigSpace = undefined;
672
-
673
- if (args.point && args.normal) {
674
- this._hitPointInLocalSpace.copy(args.point);
675
- this.gameObject.worldToLocal(this._hitPointInLocalSpace);
676
- this._hitNormalInLocalSpace.copy(args.normal);
677
- }
678
- else if (args) {
679
- // can happen for e.g. close grabs; we can assume/guess a good hit point and normal based on the object's bounds or so
680
- // convert controller world position to local space instead and use that as hit point
681
- const controller = args.event.space as GameObject;
682
- const controllerWp = controller.worldPosition;
683
- this.gameObject.worldToLocal(controllerWp);
684
- this._hitPointInLocalSpace.copy(controllerWp);
685
-
686
- const controllerUp = controller.worldUp;
687
- this._tempMat.copy(this.gameObject.matrixWorld).invert();
688
- controllerUp.transformDirection(this._tempMat);
689
- this._hitNormalInLocalSpace.copy(controllerUp);
690
- }
691
-
692
- this.recenter();
693
-
694
- this._totalMovement.set(0, 0, 0);
695
- this._deviceMode = args.mode;
696
-
697
-
698
- const dragSource = this._followObject.parent as IGameObject;
699
- const rayDirection = dragSource.worldForward;
700
-
701
- const isSpatialInput = this._deviceMode === "tracked-pointer";
702
- const dragMode = isSpatialInput ? this.settings.xrDragMode : this.settings.dragMode;
703
-
704
- // set up drag plane; we don't really know the normal yet but we can already set the point
705
- const hitWP = this._hitPointInLocalSpace.clone();
706
- this.gameObject.localToWorld(hitWP);
707
-
708
- switch (dragMode) {
709
- case DragMode.XZPlane:
710
- const up = new Vector3(0,1,0);
711
- if (this.gameObject.parent) {
712
- // TODO in this case _dragPlane should be in parent space, not world space,
713
- // otherwise dragging the parent and this object at the same time doesn't keep the plane constrained
714
- up.transformDirection(this.gameObject.parent.matrixWorld.clone().invert());
715
- }
716
- this._dragPlane.setFromNormalAndCoplanarPoint(up, hitWP);
717
- break;
718
- case DragMode.HitNormal:
719
- const hitNormal = this._hitNormalInLocalSpace.clone();
720
- hitNormal.transformDirection(this.gameObject.matrixWorld);
721
- this._dragPlane.setFromNormalAndCoplanarPoint(hitNormal, hitWP);
722
- break;
723
- case DragMode.Attached:
724
- this._dragPlane.setFromNormalAndCoplanarPoint(rayDirection, hitWP);
725
- break;
726
- case DragMode.DynamicViewAngle:
727
- const v0 = new Vector3(0, 1, 0);
728
- const v1 = rayDirection;
729
- const angle = v0.angleTo(v1);
730
- const angleThreshold = 0.5;
731
- if (angle > Math.PI / 2 + angleThreshold || angle < Math.PI / 2 - angleThreshold)
732
- this._dragPlane.setFromNormalAndCoplanarPoint(new Vector3(0, 1, 0), hitWP);
733
- else
734
- this._dragPlane.setFromNormalAndCoplanarPoint(rayDirection, hitWP);
735
- break;
736
- }
737
-
738
- // calculate bounding box and snapping points. We want to either snap the "back" point or the "bottom" point.
739
- const bbox = new Box3();
740
- const p = this.gameObject.parent;
741
- const localP = this.gameObject.position.clone();
742
- const localQ = this.gameObject.quaternion.clone();
743
- const localS = this.gameObject.scale.clone();
744
- if (p) p.remove(this.gameObject);
745
- this.gameObject.position.set(0, 0, 0);
746
- this.gameObject.quaternion.set(0, 0, 0, 1);
747
- this.gameObject.scale.set(1, 1, 1);
748
- bbox.setFromObject(this.gameObject);
749
-
750
- // get front center point of the bbox. basically (0, 0, 1) in local space
751
- const bboxCenter = new Vector3();
752
- bbox.getCenter(bboxCenter);
753
- const bboxSize = new Vector3();
754
- bbox.getSize(bboxSize);
755
-
756
- // attachment points for dragging
757
- this._bottomCenter.copy(bboxCenter.clone().add(new Vector3(0, -bboxSize.y / 2, 0)));
758
- this._backCenter.copy(bboxCenter.clone().add(new Vector3(0, 0, bboxSize.z / 2)));
759
- this._backBottomCenter.copy(bboxCenter.clone().add(new Vector3(0, -bboxSize.y / 2, bboxSize.z / 2)));
760
-
761
- this._bounds.copy(bbox);
762
-
763
- // restore original transform
764
- if (p) p.add(this.gameObject);
765
- this.gameObject.position.copy(localP);
766
- this.gameObject.quaternion.copy(localQ);
767
- this.gameObject.scale.copy(localS);
768
-
769
- // surface snapping
770
- this._draggedOverObject = null;
771
- this._draggedOverObjectLastSetUp = null;
772
- this._draggedOverObjectLastNormal.set(0, 1, 0);
773
- this._draggedOverObjectDuration = 0;
774
- }
775
-
776
- collectMovementInfo() {
777
- // we're dragging - there is a controlling object
778
- if (!this._followObject.parent) return;
779
-
780
- // TODO This should all be handled properly per-pointer
781
- // and we want to have a chance to react to multiple pointers being on the same object.
782
- // some common stuff (calculating of movement offsets, etc) could be done by default
783
- // and then the main thing to override is the actual movement of the object based on N _followObjects
784
-
785
- const dragSource = this._followObject.parent as IGameObject;
786
-
787
- // modify _followObject with constraints, e.g.
788
- // - dragging on a plane, e.g. the floor (keeping the distance to the floor plane constant)
789
- /* TODO fix jump on drag start
790
- const p0 = this._followObject.parent as GameObject;
791
- const ray = new Ray(p0.worldPosition, p0.worldForward.multiplyScalar(-1));
792
- const p = new Vector3();
793
- const t0 = ray.intersectPlane(new Plane(new Vector3(0, 1, 0)), p);
794
- if (t0 !== null)
795
- this._followObject.worldPosition = t0;
796
- */
797
-
798
- this._followObject.updateMatrix();
799
- const dragPosRigSpace = dragSource.worldPosition;
800
- const rig = NeedleXRSession.active?.rig?.gameObject;
801
- if (rig)
802
- rig.worldToLocal(dragPosRigSpace);
803
-
804
- // sum up delta
805
- // TODO We need to do all/most of these calculations in Rig Space instead of world space
806
- // moving the rig while holding an object should not affect _rayDelta / _dragDelta
807
- if (this._lastDragPosRigSpace === undefined || rig != this._lastRig) {
808
- this._lastDragPosRigSpace = dragPosRigSpace.clone();
809
- this._lastRig = rig;
810
- }
811
- this._tempVec.copy(dragPosRigSpace).sub(this._lastDragPosRigSpace);
812
-
813
- const rayDirectionRigSpace = dragSource.worldForward;
814
- if (rig) {
815
- this._tempMat.copy(rig.matrixWorld).invert();
816
- rayDirectionRigSpace.transformDirection(this._tempMat);
817
- }
818
- // sum up delta movement along ray
819
- this._totalMovementAlongRayDirection += rayDirectionRigSpace.dot(this._tempVec);
820
- this._tempVec.x = Math.abs(this._tempVec.x);
821
- this._tempVec.y = Math.abs(this._tempVec.y);
822
- this._tempVec.z = Math.abs(this._tempVec.z);
823
-
824
- // sum up absolute total movement
825
- this._totalMovement.add(this._tempVec);
826
- this._lastDragPosRigSpace.copy(dragPosRigSpace);
827
-
828
- if (debug) {
829
- let wp = dragPosRigSpace;
830
- // ray direction of the input source object
831
- if (rig) {
832
- wp = wp.clone();
833
- wp.transformDirection(rig.matrixWorld);
834
- }
835
- Gizmos.DrawRay(wp, rayDirectionRigSpace, 0x0000ff);
836
- }
837
- }
838
-
839
- onDragUpdate(numberOfPointers: number) {
840
-
841
- // can only handle a single pointer
842
- // if there's more, we defer to multi-touch drag handlers
843
- if (numberOfPointers > 1) return;
844
-
845
- const draggedObject = this.gameObject as IGameObject;
846
- const dragSource = this._followObject.parent as IGameObject;
847
- this._followObject.updateMatrix();
848
- const dragSourceWP = dragSource.worldPosition;
849
- const rayDirection = dragSource.worldForward;
850
-
851
-
852
- // Actually move and rotate draggedObject
853
- const isSpatialInput = this._deviceMode === "tracked-pointer";
854
- const keepRotation = isSpatialInput ? this.settings.xrKeepRotation : this.settings.keepRotation;
855
- const dragMode = isSpatialInput ? this.settings.xrDragMode : this.settings.dragMode;
856
-
857
- const lerpStrength = 10;
858
- // - keeping rotation constant during dragging
859
- if (keepRotation) this._followObject.worldQuaternion = this._followObjectStartWorldQuaternion;
860
- this._followObject.updateMatrix();
861
- this._followObject.updateMatrixWorld(true);
862
-
863
- // Acceleration for moving the object - move followObject along the ray distance by _totalMovementAlongRayDirection
864
- let currentDist = 1.0;
865
- let lerpFactor = 1.0;
866
- if (this._deviceMode === "tracked-pointer" && this._grabStartDistance > 0.5) // hands and controllers, but not touches
867
- {
868
- const factor = 1 + this._totalMovementAlongRayDirection * (2 * this.settings.xrDistanceDragFactor);
869
- currentDist = Math.max(0.0, factor);
870
- currentDist = currentDist * currentDist * currentDist;
871
- }
872
- else if (this._grabStartDistance <= 0.5)
873
- {
874
- // TODO there's still a frame delay between dragged objects and the hand models
875
- lerpFactor = 3.0;
876
- }
877
-
878
- // reset _followObject to its original position and rotation
879
- this._followObject.position.copy(this._followObjectStartPosition);
880
- if (!keepRotation)
881
- this._followObject.quaternion.copy(this._followObjectStartQuaternion);
882
-
883
- // TODO restore previous functionality:
884
- // When distance dragging, the HIT POINT should move along the ray until it reaches the controller;
885
- // NOT the pivot point of the dragged object. E.g. grabbing a large cube and pulling towards you should at most
886
- // move the grabbed point to your head and not slap the cube in your head.
887
- this._followObject.position.multiplyScalar(currentDist);
888
- this._followObject.updateMatrix();
889
-
890
- const ray = new Ray(dragSourceWP, rayDirection);
891
-
892
- // Surface snapping.
893
- // Feels quite weird in VR right now!
894
- if (dragMode == DragMode.SnapToSurfaces) {
895
- // Idea: Do a sphere cast if we're still in the proximity of the current draggedObject.
896
- // This would allow dragging slightly out of the object's bounds and still continue snapping to it.
897
- // Do a regular raycast (without the dragged object) to determine if we should change what is dragged onto.
898
- const opts = new RaycastOptions();
899
- opts.ignore = [draggedObject];
900
- const hits = this.context.physics.raycastFromRay(ray, opts);
901
-
902
- if (hits.length > 0) {
903
- const hit = hits[0];
904
- // if we're above the same surface for a specified time, adjust drag options:
905
- // - set that surface as the drag "plane". We will follow that object's surface instead now (raycast onto only that)
906
- // - if the drag plane is an object, we also want to
907
- // - calculate an initial rotation offset matching what surface/face the user originally started the drag on
908
- // - rotate the dragged object to match the surface normal
909
- if (this._draggedOverObject === hit.object)
910
- this._draggedOverObjectDuration += this.context.time.deltaTime;
911
- else {
912
- this._draggedOverObject = hit.object;
913
- this._draggedOverObjectDuration = 0;
914
- }
915
-
916
- if (hit.face) {
917
- // Adjust drag plane if we're dragging over a different object (for a certain amount of time)
918
- // or if the surface normal changed
919
- if (this._draggedOverObjectDuration > 0.15 &&
920
- (this._draggedOverObjectLastSetUp !== this._draggedOverObject ||
921
- this._draggedOverObjectLastNormal.dot(hit.face.normal) < 0.999999)
922
- ) {
923
- this._draggedOverObjectLastSetUp = this._draggedOverObject;
924
- this._draggedOverObjectLastNormal.copy(hit.face.normal);
925
-
926
- const center = new Vector3();
927
- const size = new Vector3();
928
-
929
- this._bounds.getCenter(center);
930
- this._bounds.getSize(size);
931
- center.sub(size.multiplyScalar(0.5).multiply(hit.face.normal));
932
- this._hitPointInLocalSpace.copy(center);
933
- this._hitNormalInLocalSpace.copy(hit.face.normal);
934
-
935
- // ensure plane is far enough up that we don't drag into the surface
936
- // Which offset we use here depends on the face normal direction we hit
937
- // If we hit the bottom, we want to use the top, and vice versa
938
- // To do this dynamically, we can find the intersection between our local bounds and the hit face normal (which is already in local space)
939
- this._bounds.getCenter(center);
940
- this._bounds.getSize(size);
941
- center.add(size.multiplyScalar(0.5).multiply(hit.face.normal));
942
-
943
- const offset = this._hitPointInLocalSpace.clone().add(center);
944
- this._followObject.localToWorld(offset);
945
- const offsetWP = this._followObject.worldPosition.sub(offset);
946
-
947
- this._dragPlane.setFromNormalAndCoplanarPoint(hit.face.normal, hit.point.sub(offsetWP));
948
- }
949
- }
950
- }
951
- }
952
-
953
- // Objects could also serve as "slots" for dragging other objects into. In that case, we don't want to snap to the surface,
954
- // we want to snap to the pivot of that object. These dragged-over objects could also need to be invisible (a "slot")
955
- // Raycast along the ray to the drag plane and move _followObject so that the grabbed point stays at the hit point
956
- if (dragMode !== DragMode.Attached && ray.intersectPlane(this._dragPlane, this._tempVec)) {
957
-
958
- this._followObject.worldPosition = this._tempVec;
959
- this._followObject.updateMatrix();
960
- this._followObject.updateMatrixWorld(true);
961
-
962
- const newWP = this._hitPointInLocalSpace.clone();
963
- this._followObject.localToWorld(newWP);
964
-
965
- if (debug) {
966
- Gizmos.DrawLine(newWP, this._tempVec, 0x00ffff, 0, false);
967
- }
968
-
969
- this._followObject.worldPosition = this._tempVec.multiplyScalar(2).sub(newWP);
970
- this._followObject.updateMatrix();
971
-
972
- /*
973
- // TODO figure out nicer look rotation here
974
- const normal = this._dragPlane.normal;
975
- const lookPoint = normal.clone().multiplyScalar(1000).add(this._tempVec);
976
- if (lookPoint) {
977
- this._followObject.lookAt(lookPoint);
978
- this._followObject.rotateX(Math.PI / 2);
979
- }
980
- */
981
- this._followObject.updateMatrix();
982
- }
983
-
984
- // TODO refactor to a common place
985
- // apply constraints (position grid snap, rotation, ...)
986
- if (this.settings.snapGridResolution > 0) {
987
- const wp = this._followObject.worldPosition;
988
- const snap = this.settings.snapGridResolution;
989
- wp.x = Math.round(wp.x / snap) * snap;
990
- wp.y = Math.round(wp.y / snap) * snap;
991
- wp.z = Math.round(wp.z / snap) * snap;
992
- this._followObject.worldPosition = wp;
993
- this._followObject.updateMatrix();
994
- }
995
- if (keepRotation) {
996
- this._followObject.worldQuaternion = this._followObjectStartWorldQuaternion;
997
- this._followObject.updateMatrix();
998
- }
999
-
1000
- // TODO refactor to a common place
1001
- // TODO should use unscaled time here // some test for lerp speed depending on distance
1002
- const t = Mathf.clamp01(this.context.time.deltaTime * lerpStrength * lerpFactor);// / (currentDist - 1 + 0.01));
1003
-
1004
- const wp = draggedObject.worldPosition;
1005
- wp.lerp(this._followObject.worldPosition, t);
1006
- draggedObject.worldPosition = wp;
1007
-
1008
- const rot = draggedObject.worldQuaternion;
1009
- rot.slerp(this._followObject.worldQuaternion, t);
1010
- draggedObject.worldQuaternion = rot;
1011
-
1012
-
1013
- if (debug)
1014
- {
1015
- const hitPointWP = this._hitPointInLocalSpace.clone();
1016
- draggedObject.localToWorld(hitPointWP);
1017
- // draw grab attachment point and normal. They are in grabbed object space
1018
- Gizmos.DrawSphere(hitPointWP, 0.02, 0xff0000);
1019
- const hitNormalWP = this._hitNormalInLocalSpace.clone();
1020
- hitNormalWP.applyQuaternion(rot);
1021
- Gizmos.DrawRay(hitPointWP, hitNormalWP, 0xff0000);
1022
-
1023
- // debug info
1024
- Gizmos.DrawLabel(wp.add(new Vector3(0, 0.25, 0)),
1025
- `Distance: ${this._totalMovement.length().toFixed(2)}\n
1026
- Along Ray: ${this._totalMovementAlongRayDirection.toFixed(2)}\n
1027
- Session: ${!!NeedleXRSession.active}\n
1028
- Device: ${this._deviceMode}\n
1029
- `,
1030
- 0.03
1031
- );
1032
-
1033
- // draw bottom/back snap points
1034
- const bottomCenter = this._bottomCenter.clone();
1035
- const backCenter = this._backCenter.clone();
1036
- const backBottomCenter = this._backBottomCenter.clone();
1037
- draggedObject.localToWorld(bottomCenter);
1038
- draggedObject.localToWorld(backCenter);
1039
- draggedObject.localToWorld(backBottomCenter);
1040
- Gizmos.DrawSphere(bottomCenter, 0.01, 0x00ff00, 0, false);
1041
- Gizmos.DrawSphere(backCenter, 0.01, 0x0000ff, 0, false);
1042
- Gizmos.DrawSphere(backBottomCenter, 0.01, 0xff00ff, 0, false);
1043
- Gizmos.DrawLine(bottomCenter, backBottomCenter, 0x00ffff, 0, false);
1044
- Gizmos.DrawLine(backBottomCenter, backCenter, 0x00ffff, 0, false);
1045
- }
1046
- }
1047
-
1048
- onDragEnd(args: PointerEventData) {
1049
- console.assert(this._followObject.parent === args.event.space, "Drag end: _followObject is not parented to the space object");
1050
- this._followObject.removeFromParent();
1051
- this._followObject.destroy();
1052
- this._lastDragPosRigSpace = undefined;
1053
- }
1054
- }
1055
-
1056
- /** Currently does _only_ provide visuals support for DragControls operations.
1057
- * Previously it also provided the actual drag functionality, but that has been moved to DragControls for now.
1058
- */
1059
- class LegacyDragVisualsHelper {
1060
-
1061
302
  showGizmo: boolean = true;
1062
303
  useViewAngle: boolean = true;
1063
304
 
@@ -1095,12 +336,13 @@
1095
336
  constructor(camera: Camera) {
1096
337
  this._camera = camera;
1097
338
 
1098
- const line = new Line(LegacyDragVisualsHelper.geometry);
339
+ const line = new Line(DragHelper.geometry);
1099
340
  const mat = line.material as LineBasicMaterial;
1100
341
  mat.color = new Color(.4, .4, .4);
1101
342
  line.layers.set(2);
1102
343
  line.name = 'line';
1103
344
  line.scale.y = 1;
345
+ // line.matrixAutoUpdate = false;
1104
346
  this._groundLine = line;
1105
347
 
1106
348
  const geometry = new SphereGeometry(.5, 22, 22);
@@ -1115,12 +357,13 @@
1115
357
  if (this._selected && context) {
1116
358
  for (const rb of this._rbs) {
1117
359
  rb.wakeUp();
360
+ // if (!rb.smoothedVelocity) continue;
1118
361
  rb.setVelocity(0, 0, 0);
1119
362
  }
1120
363
  }
1121
364
 
1122
365
  if (this._selected) {
1123
- // TODO move somewhere else
366
+
1124
367
  Avatar_POI.Remove(context, this._selected);
1125
368
  }
1126
369
 
@@ -1142,8 +385,6 @@
1142
385
  console.error("DragHelper: no context");
1143
386
  return;
1144
387
  }
1145
-
1146
- // TODO move somewhere else
1147
388
  Avatar_POI.Add(context, this._selected, null);
1148
389
 
1149
390
  this._groundOffsetFactor = 0;
@@ -1151,6 +392,7 @@
1151
392
  this._groundOffset.set(0, 0, 0);
1152
393
  this._requireUpdateGroundPlane = true;
1153
394
 
395
+ // this._rbs = GameObject.getComponentsInChildren(this._selected, Rigidbody);
1154
396
  this.onUpdateScreenSpacePlane();
1155
397
  }
1156
398
  }
@@ -1160,16 +402,6 @@
1160
402
  private _didDragOnGroundPlaneLastFrame: boolean = false;
1161
403
 
1162
404
  onUpdate(_context: Context) {
1163
-
1164
- if (!this._selected) return;
1165
-
1166
- const wp = getWorldPosition(this._selected);
1167
- this.onUpdateWorldPosition(wp, this._groundPlanePoint, false);
1168
- this.onUpdateGroundPlane();
1169
- this._didDragOnGroundPlaneLastFrame = true;
1170
- this._hasGroundPlane = true;
1171
-
1172
- /*
1173
405
  if (!this._context) return;
1174
406
 
1175
407
  const mainKey: KeyCode = "Space";
@@ -1256,7 +488,6 @@
1256
488
  this.onDidUpdate();
1257
489
  }
1258
490
  }
1259
- */
1260
491
  }
1261
492
 
1262
493
  private onUpdateWorldPosition(wp: Vector3, pointOnPlane: Vector3 | null, heightOnly: boolean) {
@@ -1318,6 +549,18 @@
1318
549
  this._groundOffset.copy(this._intersection).sub(wp);
1319
550
  }
1320
551
 
552
+ private onDidUpdate() {
553
+ // todo: when using instancing we need to mark the matrix to update
554
+ InstancingUtil.markDirty(this._selected);
555
+
556
+ for (const rb of this._rbs) {
557
+ rb.wakeUp();
558
+ rb.resetForcesAndTorques();
559
+ // rb.setBodyFromGameObject({ x: 0, y: 0, z: 0 });
560
+ rb.setAngularVelocity(0, 0, 0);
561
+ }
562
+ }
563
+
1321
564
  private contains(obj: Object3D, toSearch: Object3D): boolean {
1322
565
  if (obj === toSearch) return true;
1323
566
  if (obj.children) {
src/engine-components/DropListener.ts CHANGED
@@ -1,11 +1,10 @@
1
- import type { GLTF } from "three/examples/jsm/loaders/GLTFLoader.js";
2
-
1
+ import { Behaviour, GameObject } from "./Component.js";
2
+ import { RaycastOptions } from "../engine/engine_physics.js";
3
3
  import * as files from "../engine/engine_networking_files.js";
4
- import { RaycastOptions } from "../engine/engine_physics.js";
5
4
  import { serializable } from "../engine/engine_serialization_decorator.js";
5
+ import { Networking } from "../engine-components/Networking.js";
6
+ import type { GLTF } from "three/examples/jsm/loaders/GLTFLoader.js";
6
7
  import { getParam } from "../engine/engine_utils.js";
7
- import { Networking } from "../engine-components/Networking.js";
8
- import { Behaviour, GameObject } from "./Component.js";
9
8
 
10
9
  const debug = getParam("debugdroplistener");
11
10
 
src/engine-components/Duplicatable.ts CHANGED
@@ -1,26 +1,22 @@
1
- import { Object3D,Quaternion, Vector3 } from "three";
2
-
1
+ import { Behaviour, GameObject } from "./Component.js";
2
+ import { WebXRController, ControllerEvents } from "./webxr/WebXRController.js";
3
+ import { DragControls, DragEvents } from "./DragControls.js";
4
+ import { Interactable } from "./Interactable.js";
5
+ import { Animation } from "./Animation.js";
6
+ import { Vector3, Quaternion, Object3D } from "three";
7
+ import { serializable } from "../engine/engine_serialization_decorator.js";
3
8
  import { InstantiateOptions } from "../engine/engine_gameobject.js";
4
- import { serializable } from "../engine/engine_serialization_decorator.js";
5
- import { Behaviour, GameObject } from "./Component.js";
6
- import { DragControls } from "./DragControls.js";
7
- import { IPointerEventHandler, PointerEventData } from "./ui/PointerEvents.js";
8
- import { ObjectRaycaster } from "./ui/Raycaster.js";
9
9
 
10
- export class Duplicatable extends Behaviour implements IPointerEventHandler {
10
+ export class Duplicatable extends Interactable {
11
11
 
12
- /** Duplicates will be parented into the set object. If not defined, this GameObject will be used as parent. */
13
12
  @serializable(Object3D)
14
13
  parent: GameObject | null = null;
15
-
16
- /** The object to be duplicated */
17
14
  @serializable(Object3D)
18
15
  object: GameObject | null = null;
19
16
 
20
17
  // limit max object spawn count per interval
21
18
  @serializable()
22
19
  limitCount = 10;
23
-
24
20
  @serializable()
25
21
  limitInterval = 60;
26
22
 
@@ -28,7 +24,17 @@
28
24
  private _startPosition: THREE.Vector3 | null = null;
29
25
  private _startQuaternion: THREE.Quaternion | null = null;
30
26
 
31
- start(): void {
27
+ awake(): void {
28
+ // TODO: add support to not having to assign a object to clone
29
+ // if(!this.object){
30
+ // const opts = new InstantiateOptions();
31
+ // opts.parent = this.gameObject;
32
+ // opts.idProvider = InstantiateIdProvider.createFromString(this.guid);
33
+ // const clone = GameObject.instantiate(this.gameObject, opts);
34
+ // const duplicatable =
35
+ // this.object = clone;
36
+ // }
37
+ // console.log(this, this.object);
32
38
  if (this.object) {
33
39
  if (this.object as any === this.gameObject) {
34
40
  console.error("Can not duplicate self");
@@ -42,43 +48,32 @@
42
48
  this._startQuaternion = this.object.quaternion?.clone() ?? new Quaternion(0, 0, 0, 1);
43
49
  }
44
50
 
45
- // legacy – DragControls was required for duplication and so often the component is still there; we work around that by disabling it here
46
- const dragControls = this.gameObject.getComponent(DragControls);
47
- if (dragControls) {
48
- console.warn("Please remove DragControls from object with Duplicatable component, it's not needed anymore.");
49
- dragControls.enabled = false;
51
+ const drag = GameObject.getComponentInParent(this.gameObject, DragControls);
52
+ if (drag) {
53
+ drag.addDragEventListener(DragEvents.SelectStart, (_ctrls, args) => {
54
+ if (this._currentCount >= this.limitCount) {
55
+ args.attached = null;
56
+ return;
57
+ }
58
+ const res = this.handleDuplication(args.selected);
59
+ if (res) {
60
+ console.assert(res !== args.selected, "Duplicated object is original");
61
+ args.attached = res;
62
+ }
63
+ });
50
64
  }
51
-
52
- if (!this.gameObject.getComponentInParent(ObjectRaycaster))
53
- this.gameObject.addNewComponent(ObjectRaycaster);
65
+ else console.warn("Could no find drag controls in parent", this.name);
54
66
 
55
- this.cloneLimitIntervalFn();
56
- }
57
-
58
- private _forwardPointerEvents: Map<Object3D, DragControls> = new Map();
59
-
60
- onPointerDown(args: PointerEventData) {
61
- if (!this.object) return;
62
- if (!this.context.connection.allowEditing) return;
63
- if (args.button !== 0) return;
64
-
65
- const res = this.handleDuplication();
66
- if (res) {
67
- const dragControls = GameObject.getComponent(res, DragControls);
68
- if (!dragControls) console.warn("Duplicated object does not have DragControls");
69
- else {
70
- dragControls.onPointerDown(args);
71
- this._forwardPointerEvents.set(args.event.space, dragControls);
67
+ WebXRController.addEventListener(ControllerEvents.SelectStart, (_controller: WebXRController, args: { selected: THREE.Object3D, grab: THREE.Object3D | GameObject | null }) => {
68
+ if (this._currentCount >= this.limitCount) {
69
+ args.grab = null;
70
+ return;
72
71
  }
73
- }
74
- }
72
+ const res = this.handleDuplication(args.selected);
73
+ if (res) args.grab = res;
74
+ });
75
75
 
76
- onPointerUp(args: PointerEventData) {
77
- const dragControls = this._forwardPointerEvents.get(args.event.space);
78
- if (dragControls) {
79
- dragControls.onPointerUp(args);
80
- this._forwardPointerEvents.delete(args.event.space);
81
- }
76
+ this.cloneLimitIntervalFn();
82
77
  }
83
78
 
84
79
  private cloneLimitIntervalFn() {
@@ -91,39 +86,62 @@
91
86
  }, (this.limitInterval / this.limitCount) * 1000);
92
87
  }
93
88
 
94
- private handleDuplication(): THREE.Object3D | null {
89
+ private handleDuplication(selected: THREE.Object3D): THREE.Object3D | null {
90
+ if (this._currentCount >= this.limitCount) return null;
95
91
  if (!this.object) return null;
96
- if (this._currentCount >= this.limitCount) return null;
97
- if (this.object as any === this.gameObject) return null;
92
+ if (selected === this.gameObject || this.handleMultiObject(selected)) {
98
93
 
99
- this.object.visible = true;
94
+ if (this.object as any === this.gameObject) return null;
95
+ this.object.visible = true;
100
96
 
101
- if (this._startPosition)
102
- this.object.position.copy(this._startPosition);
103
- if (this._startQuaternion)
104
- this.object.quaternion.copy(this._startQuaternion);
97
+ if (this._startPosition)
98
+ this.object.position.copy(this._startPosition);
99
+ if (this._startQuaternion)
100
+ this.object.quaternion.copy(this._startQuaternion);
105
101
 
106
- const opts = new InstantiateOptions();
107
- if (!this.parent) this.parent = this.gameObject.parent as GameObject;
108
- if (this.parent) {
109
- opts.parent = this.parent.guid ?? this.parent.userData?.guid;
110
- opts.keepWorldPosition = true;
102
+ const opts = new InstantiateOptions();
103
+ if (!this.parent) this.parent = this.gameObject.parent as GameObject;
104
+ if (this.parent) {
105
+ opts.parent = this.parent.guid ?? this.parent.userData?.guid;
106
+ opts.keepWorldPosition = true;
107
+ }
108
+ opts.position = this.worldPosition;
109
+ opts.rotation = this.worldQuaternion;
110
+ opts.context = this.context;
111
+ this._currentCount += 1;
112
+
113
+ const newInstance = GameObject.instantiateSynced(this.object as GameObject, opts) as GameObject;
114
+ console.assert(newInstance !== this.object, "Duplicated object is original");
115
+ this.object.visible = false;
116
+
117
+ // see if this fixes object being offset when duplicated and dragged - it looks like three clone has shared position/quaternion objects?
118
+ if (this._startPosition)
119
+ this.object.position.clone().copy(this._startPosition);
120
+ if (this._startQuaternion)
121
+ this.object.quaternion.clone().copy(this._startQuaternion);
122
+
123
+ return newInstance;
111
124
  }
112
- opts.position = this.worldPosition;
113
- opts.rotation = this.worldQuaternion;
114
- opts.context = this.context;
115
- this._currentCount += 1;
125
+ return null;
126
+ }
116
127
 
117
- const newInstance = GameObject.instantiateSynced(this.object as GameObject, opts) as GameObject;
118
- console.assert(newInstance !== this.object, "Duplicated object is original");
119
- this.object.visible = false;
128
+ private handleMultiObject(selected: THREE.Object3D): boolean {
129
+ const shouldSearchInChildren = this.gameObject.type === "Group" || this.gameObject.type === "Object3D";
130
+ if (!shouldSearchInChildren) return false;
131
+ return this.isInChildren(this.gameObject, selected);
132
+ }
120
133
 
121
- // see if this fixes object being offset when duplicated and dragged - it looks like three clone has shared position/quaternion objects?
122
- if (this._startPosition)
123
- this.object.position.clone().copy(this._startPosition);
124
- if (this._startQuaternion)
125
- this.object.quaternion.clone().copy(this._startQuaternion);
134
+ private isInChildren(current: THREE.Object3D, search: THREE.Object3D): boolean {
135
+ if (!current) return false;
136
+ if (current === search) return true;
137
+ if (current.children) {
138
+ for (const child of current.children) {
139
+ if (this.isInChildren(child, search)) {
140
+ return true;
141
+ }
142
+ }
143
+ }
144
+ return false;
145
+ }
126
146
 
127
- return newInstance;
128
- }
129
147
  }
src/engine/engine_addressables.ts CHANGED
@@ -1,14 +1,13 @@
1
+ import { deepClone, getParam, resolveUrl } from "../engine/engine_utils.js";
2
+ import { SerializationContext, TypeSerializer, assign } from "./engine_serialization_core.js";
3
+ import { Context } from "./engine_setup.js";
1
4
  import { Group, Object3D, Texture, TextureLoader } from "three";
2
-
3
- import { deepClone, getParam, resolveUrl } from "../engine/engine_utils.js";
4
- import { destroy, IInstantiateOptions, instantiate, InstantiateOptions, isDestroyed } from "./engine_gameobject.js";
5
- import { getLoader } from "./engine_gltf.js";
6
5
  import { processNewScripts } from "./engine_mainloop_utils.js";
7
6
  import { registerPrefabProvider, syncInstantiate } from "./engine_networking_instantiate.js";
8
- import { assign,SerializationContext, TypeSerializer } from "./engine_serialization_core.js";
9
- import { Context } from "./engine_setup.js";
7
+ import { download } from "./engine_web_api.js";
8
+ import { getLoader } from "./engine_gltf.js";
10
9
  import type { IComponent, IGameObject, SourceIdentifier } from "./engine_types.js";
11
- import { download } from "./engine_web_api.js";
10
+ import { destroy, IInstantiateOptions, instantiate, InstantiateOptions, isDestroyed } from "./engine_gameobject.js";
12
11
 
13
12
  const debug = getParam("debugaddressables");
14
13
 
src/engine/engine_assetdatabase.ts CHANGED
@@ -1,8 +1,7 @@
1
+ import { InternalUsageTrackerPlugin } from "./extensions/usage_tracker.js";
1
2
  import { Bone, BufferAttribute, BufferGeometry, InterleavedBuffer, InterleavedBufferAttribute, Material, Mesh, NeverCompare, Object3D, Scene, Skeleton, SkinnedMesh, Source, Texture, Uniform, WebGLRenderer } from "three";
2
-
3
3
  import { addPatch } from "./engine_patcher.js";
4
4
  import { getParam } from "./engine_utils.js";
5
- import { InternalUsageTrackerPlugin } from "./extensions/usage_tracker.js";
6
5
 
7
6
 
8
7
  export class AssetDatabase {
src/engine/engine_camera.ts CHANGED
@@ -1,8 +1,7 @@
1
+ import type { ICameraController } from "./engine_types.js";
1
2
  import { Camera, Object3D } from "three";
2
3
 
3
- import type { ICameraController } from "./engine_types.js";
4
4
 
5
-
6
5
  const $cameraController = Symbol("cameraController");
7
6
 
8
7
  export function getCameraController(cam: Camera): ICameraController | null {
src/engine/engine_components.ts CHANGED
@@ -1,13 +1,12 @@
1
1
  import { Object3D, Scene } from "three";
2
-
2
+ import type { Constructor, ConstructorConcrete, IComponent, IGameObject } from "./engine_types.js";
3
+ import { Context, registerComponent } from "./engine_setup.js";
4
+ import { getParam } from "./engine_utils.js";
5
+ import { removeScriptFromContext, updateActiveInHierarchyWithoutEventCall } from "./engine_mainloop_utils.js";
6
+ import { activeInHierarchyFieldName } from "./engine_constants.js";
3
7
  import { apply } from "../engine-components/js-extensions/Object3D.js";
8
+ import { InstantiateIdProvider } from "./engine_networking_instantiate.js";
4
9
  import { ComponentEvents, ComponentLifecycleEvents } from "./engine_components_internal.js";
5
- import { activeInHierarchyFieldName } from "./engine_constants.js";
6
- import { removeScriptFromContext, updateActiveInHierarchyWithoutEventCall } from "./engine_mainloop_utils.js";
7
- import { InstantiateIdProvider } from "./engine_networking_instantiate.js";
8
- import { Context, registerComponent } from "./engine_setup.js";
9
- import type { Constructor, ConstructorConcrete, IComponent, IGameObject } from "./engine_types.js";
10
- import { getParam } from "./engine_utils.js";
11
10
 
12
11
  const debug = getParam("debuggetcomponent");
13
12
 
@@ -147,7 +146,7 @@
147
146
  const component = obj.userData.components[i];
148
147
  if (componentType === null || component.constructor.name === componentType["name"] || component.constructor.name === componentType) {
149
148
  if (debug)
150
- console.log("MATCH BY NAME", component)
149
+ console.log("MATCH BY NAME\n", component, "\n\ncomponentType:", componentType, "\n\ncomponentType[name]:", component.constructor.name, "==", componentType["name"], "\n\nconstructor.name:", component.constructor.name, "==", componentType);
151
150
  if (arr) arr.push(component);
152
151
  else return component;
153
152
  }
src/engine/engine_context.ts CHANGED
@@ -1,36 +1,41 @@
1
- import { EffectComposer, RenderPass } from "postprocessing";
2
1
  import {
3
2
  BufferGeometry, Cache, Camera, Clock, Color, DepthTexture, Group,
4
3
  Material, NearestFilter, NoToneMapping, Object3D, PCFSoftShadowMap,
5
4
  PerspectiveCamera, RGBAFormat, Scene, SRGBColorSpace,
6
5
  Texture, WebGLRenderer, type WebGLRendererParameters, WebGLRenderTarget, type WebXRArrayCamera
7
6
  } from 'three';
7
+
8
+ import { Input } from './engine_input.js';
9
+ import { Physics } from './engine_physics.js';
10
+ import { Time } from './engine_time.js';
11
+ import { NetworkConnection } from './engine_networking.js';
12
+
13
+ import * as looputils from './engine_mainloop_utils.js';
14
+ import * as utils from "./engine_utils.js";
15
+
16
+ import { EffectComposer, RenderPass } from "postprocessing";
17
+
18
+ import { AssetDatabase } from './engine_assetdatabase.js';
19
+
20
+ import { logHierarchy } from './engine_three_utils.js';
21
+
8
22
  import * as Stats from 'three/examples/jsm/libs/stats.module.js';
9
-
10
- import { isDevEnvironment, LogType, showBalloonError, showBalloonMessage, showBalloonWarning } from './debug/index.js';
23
+ import { RendererData as SceneLighting } from './engine_scenelighting.js';
11
24
  import { Addressables } from './engine_addressables.js';
12
25
  import { Application } from './engine_application.js';
13
- import { AssetDatabase } from './engine_assetdatabase.js';
26
+ import { LightDataRegistry, type ILightDataRegistry } from './engine_lightdata.js';
27
+ import { PlayerViewManager } from './engine_playerview.js';
28
+
29
+ import { type CoroutineData, type GLTF, type ICamera, type IComponent, type IContext, type ILight, type LoadedGLTF } from "./engine_types.js";
30
+ import { destroy, foreachComponent } from './engine_gameobject.js';
31
+ import { ContextEvent, ContextRegistry } from './engine_context_registry.js';
32
+ import { delay, getParam } from './engine_utils.js';
14
33
  import { VERSION } from './engine_constants.js';
15
- import { ContextEvent, ContextRegistry } from './engine_context_registry.js';
34
+ import { isDevEnvironment, LogType, showBalloonError, showBalloonMessage, showBalloonWarning } from './debug/index.js';
35
+ import { getLoader } from './engine_gltf.js';
36
+ import { isLocalNetwork } from './engine_networking_utils.js';
16
37
  import { WaitForPromise } from './engine_coroutine.js';
17
- import { destroy, foreachComponent } from './engine_gameobject.js';
18
- import { getLoader } from './engine_gltf.js';
19
- import { Input } from './engine_input.js';
20
38
  import { invokeLifecycleFunctions } from './engine_lifecycle_functions_internal.js';
21
- import { type ILightDataRegistry, LightDataRegistry } from './engine_lightdata.js';
22
- import * as looputils from './engine_mainloop_utils.js';
23
- import { NetworkConnection } from './engine_networking.js';
24
- import { isLocalNetwork } from './engine_networking_utils.js';
25
- import { Physics } from './engine_physics.js';
26
- import { PlayerViewManager } from './engine_playerview.js';
27
- import { RendererData as SceneLighting } from './engine_scenelighting.js';
28
- import { logHierarchy } from './engine_three_utils.js';
29
- import { Time } from './engine_time.js';
30
- import { type CoroutineData, type GLTF, type ICamera, type IComponent, type IContext, type ILight, INeedleXRSession, type LoadedGLTF } from "./engine_types.js";
31
- import * as utils from "./engine_utils.js";
32
- import { delay, getParam } from './engine_utils.js';
33
- import type { INeedleXRSessionEventReceiver, NeedleXRSession } from './engine_xr.js';
34
39
 
35
40
 
36
41
  const debug = utils.getParam("debugcontext");
@@ -96,6 +101,11 @@
96
101
  Undefined = -1,
97
102
  }
98
103
 
104
+ export enum XRSessionMode {
105
+ ImmersiveVR = "immersive-vr",
106
+ ImmersiveAR = "immersive-ar",
107
+ }
108
+
99
109
  /** threejs callback event signature */
100
110
  export declare type OnRenderCallback = (renderer: WebGLRenderer, scene: Scene, camera: Camera, geometry: BufferGeometry, material: Material, group: Group) => void
101
111
 
@@ -203,7 +213,6 @@
203
213
  private _boundingClientRectFrame: number = -1;
204
214
  private _boundingClientRect: DOMRect | null = null;
205
215
  private _domX; private _domY;
206
- /** update bounding rects + domX, domY */
207
216
  private calculateBoundingClientRect() {
208
217
  // workaround for mozilla webXR viewer
209
218
  if (this.isInAR) {
@@ -218,46 +227,30 @@
218
227
  this._domY = this._boundingClientRect.y;
219
228
  }
220
229
 
221
- /** The width of the `<needle-engine>` element on the website */
222
230
  get domWidth(): number {
223
231
  // for mozilla XR
224
232
  if (this.isInAR) return window.innerWidth;
225
233
  return this.domElement.clientWidth;
226
234
  }
227
- /** The height of the `<needle-engine>` element on the website */
228
235
  get domHeight(): number {
229
236
  // for mozilla XR
230
237
  if (this.isInAR) return window.innerHeight;
231
238
  return this.domElement.clientHeight;
232
239
  }
233
- /** the X position of the Needle Engine element on the website */
234
240
  get domX(): number {
235
241
  this.calculateBoundingClientRect();
236
242
  return this._domX;
237
243
  }
238
- /** the Y position of the Needlee Engine element on the website */
239
244
  get domY(): number {
240
245
  this.calculateBoundingClientRect();
241
246
  return this._domY;
242
247
  }
243
248
  get isInXR() { return this.renderer?.xr?.isPresenting || false; }
244
- /** shorthand for `NeedleXRSession.active`
245
- * Automatically set by NeedleXRSession when a XR session is active
246
- * @returns the active XR session or null if no session is active
247
- * */
248
- xr: NeedleXRSession | null = null;
249
- get xrSessionMode() { return this.xr?.mode; }
250
- get isInVR() { return this.xrSessionMode === "immersive-vr"; }
251
- get isInAR() { return this.xrSessionMode === "immersive-ar"; }
252
- /** If a XR session is active and in pass through mode (immersive-ar on e.g. Quest) */
253
- get isInPassThrough() { return this.xr ? this.xr.isPassThrough : false; }
254
- /** access the raw `XRSession` object (shorthand for `context.renderer.xr.getSession()`). For more control use `NeedleXRSession.active` */
249
+ xrSessionMode: XRSessionMode | undefined = undefined;
250
+ get isInVR() { return this.xrSessionMode === XRSessionMode.ImmersiveVR; }
251
+ get isInAR() { return this.xrSessionMode === XRSessionMode.ImmersiveAR; }
255
252
  get xrSession() { return this.renderer?.xr?.getSession(); }
256
- /** @returns the latest XRFrame (if a XRSession is currently active)
257
- * @link https://developer.mozilla.org/en-US/docs/Web/API/XRFrame
258
- */
259
253
  get xrFrame() { return this._xrFrame }
260
- /** @returns the current WebXR camera (shorthand for `context.renderer.xr.getCamera()`) */
261
254
  get xrCamera(): WebXRArrayCamera | undefined { return this.renderer?.xr?.getCamera(); }
262
255
  private _xrFrame: XRFrame | null = null;
263
256
  get arOverlayElement(): HTMLElement {
@@ -277,37 +270,17 @@
277
270
  composer: EffectComposer | null = null;
278
271
 
279
272
  // all scripts
280
- readonly scripts: IComponent[] = [];
281
- readonly scripts_pausedChanged: IComponent[] = [];
273
+ scripts: IComponent[] = [];
274
+ scripts_pausedChanged: IComponent[] = [];
282
275
  // scripts with update event
283
- readonly scripts_earlyUpdate: IComponent[] = [];
284
- readonly scripts_update: IComponent[] = [];
285
- readonly scripts_lateUpdate: IComponent[] = [];
286
- readonly scripts_onBeforeRender: IComponent[] = [];
287
- readonly scripts_onAfterRender: IComponent[] = [];
288
- readonly scripts_WithCorroutines: IComponent[] = [];
289
- readonly scripts_immersive_vr: INeedleXRSessionEventReceiver[] = [];
290
- readonly scripts_immersive_ar: INeedleXRSessionEventReceiver[] = [];
291
- readonly coroutines: { [FrameEvent: number]: Array<CoroutineData> } = {}
276
+ scripts_earlyUpdate: IComponent[] = [];
277
+ scripts_update: IComponent[] = [];
278
+ scripts_lateUpdate: IComponent[] = [];
279
+ scripts_onBeforeRender: IComponent[] = [];
280
+ scripts_onAfterRender: IComponent[] = [];
281
+ scripts_WithCorroutines: IComponent[] = [];
282
+ coroutines: { [FrameEvent: number]: Array<CoroutineData> } = {}
292
283
 
293
- /** callbacks called once after the context has been created */
294
- readonly post_setup_callbacks: Function[] = [];
295
- /** called every frame at the beginning of the frame (after component start events and before earlyUpdate) */
296
- readonly pre_update_callbacks: Function[] = [];
297
- /** called every frame before rendering (after all component events) */
298
- readonly pre_render_callbacks: Array<(frame: XRFrame | null) => void> = [];
299
- /** called every frame after rendering (after all component events) */
300
- readonly post_render_callbacks: Function[] = [];
301
-
302
- /** called every frame befroe update (this list is emptied every frame) */
303
- readonly pre_update_oneshot_callbacks: Function[] = [];
304
-
305
- readonly new_scripts: IComponent[] = [];
306
- readonly new_script_start: IComponent[] = [];
307
- readonly new_scripts_pre_setup_callbacks: Function[] = [];
308
- readonly new_scripts_post_setup_callbacks: Function[] = [];
309
- readonly new_scripts_xr: INeedleXRSessionEventReceiver[] = [];
310
-
311
284
  mainCameraComponent: ICamera | undefined;
312
285
 
313
286
  private _camera: Camera | null = null;
@@ -327,13 +300,20 @@
327
300
  this._camera = cam;
328
301
  }
329
302
 
303
+ post_setup_callbacks: Function[] = [];
304
+ pre_update_callbacks: Function[] = [];
305
+ pre_render_callbacks: Function[] = [];
306
+ post_render_callbacks: Function[] = [];
307
+
308
+ new_scripts: IComponent[] = [];
309
+ new_script_start: IComponent[] = [];
310
+ new_scripts_pre_setup_callbacks: Function[] = [];
311
+ new_scripts_post_setup_callbacks: Function[] = [];
312
+
330
313
  application: Application;
331
- /** access timings (current frame number, deltaTime, timeScale, ...) */
332
314
  time: Time;
333
315
  input: Input;
334
- /** access physics related methods (e.g. raycasting). To access the phyiscs engine use `context.physics.engine` */
335
316
  physics: Physics;
336
- /** access networking methods (use it to send or listen to messages or join a networking backend) */
337
317
  connection: NetworkConnection;
338
318
  /**
339
319
  * @deprecated AssetDataBase is deprecated
@@ -413,7 +393,7 @@
413
393
  }
414
394
  }
415
395
  }
416
- if (debug) console.log("Using Renderer Parameters:", params, this.domElement)
396
+ if(debug) console.log("Using Renderer Parameters:", params, this.domElement)
417
397
 
418
398
  this.renderer = new WebGLRenderer(params);
419
399
 
@@ -432,8 +412,6 @@
432
412
  this.renderer.outputColorSpace = SRGBColorSpace;
433
413
  // https://github.com/mrdoob/three.js/pull/25556
434
414
  this.renderer.useLegacyLights = false;
435
-
436
- this.input.bindEvents();
437
415
  }
438
416
 
439
417
 
@@ -445,13 +423,10 @@
445
423
 
446
424
  private _disposeCallbacks: Function[] = [];
447
425
 
426
+ // private _requestSizeUpdate : boolean = false;
448
427
 
449
- /** will request a renderer size update the next render call (will call updateSize the next update) */
450
- requestSizeUpdate() { this._sizeChanged = true; }
451
-
452
- /** update the renderer and canvas size */
453
- updateSize(force: boolean = false) {
454
- if (force || (!this.isManagedExternally && this.renderer.xr?.isPresenting === false)) {
428
+ updateSize() {
429
+ if (!this.isManagedExternally && this.renderer.xr?.isPresenting === false) {
455
430
  this._sizeChanged = false;
456
431
  const scaleFactor = this.resolutionScaleFactor;
457
432
  const width = this.domWidth * scaleFactor;
@@ -502,7 +477,7 @@
502
477
  async create(opts?: ContextCreateArgs) {
503
478
  try {
504
479
  this._isCreating = true;
505
- if (opts !== this._originalCreationArgs)
480
+ if(opts !== this._originalCreationArgs)
506
481
  this._originalCreationArgs = utils.deepClone(opts);
507
482
  window.addEventListener("unhandledrejection", this.onUnhandledRejection)
508
483
  const res = await this.internalOnCreate(opts);
@@ -555,11 +530,11 @@
555
530
  if (this.renderer) {
556
531
  this.renderer.setClearAlpha(0);
557
532
  this.renderer.clear();
558
- if (!this.isManagedExternally) {
559
- if (debug) console.log("Disposing renderer");
560
- this.renderer.dispose();
561
- }
562
533
  }
534
+ if (!this.isManagedExternally) {
535
+ if(debug) console.log("Disposing renderer");
536
+ this.renderer.dispose();
537
+ }
563
538
  this.scene = null!;
564
539
  this.renderer = null!;
565
540
  this.input.dispose();
@@ -577,10 +552,6 @@
577
552
  this._isCreated = false;
578
553
  ContextRegistry.dispatchCallback(ContextEvent.ContextDestroyed, this);
579
554
  ContextRegistry.unregister(this);
580
- if (Context.Current === this) {
581
- //@ts-ignore
582
- Context.Current = null;
583
- }
584
555
  }
585
556
 
586
557
  registerCoroutineUpdate(script: IComponent, coroutine: Generator, evt: FrameEvent): Generator {
@@ -732,7 +703,7 @@
732
703
  private async internalOnCreate(opts?: ContextCreateArgs) {
733
704
  const createId = ++this._createId;
734
705
 
735
- if (debug) console.log("Creating context", this.name, opts);
706
+ if(debug) console.log("Creating context", this.name, opts);
736
707
 
737
708
  this.clear();
738
709
  // stop the animation loop if its running during creation
@@ -839,8 +810,6 @@
839
810
  }
840
811
  }
841
812
 
842
- this.input.bindEvents();
843
-
844
813
  Context.Current = this;
845
814
  looputils.processNewScripts(this);
846
815
 
@@ -883,7 +852,7 @@
883
852
  this._dispatchReadyAfterFrame = true;
884
853
  const res = ContextRegistry.dispatchCallback(ContextEvent.ContextCreated, this, { files: loadedFiles });
885
854
  if (res) {
886
- if ("internalSetLoadingMessage" in this.domElement && typeof this.domElement.internalSetLoadingMessage === "function")
855
+ if("internalSetLoadingMessage" in this.domElement && typeof this.domElement.internalSetLoadingMessage === "function")
887
856
  this.domElement?.internalSetLoadingMessage("finish loading");
888
857
  await res;
889
858
  }
@@ -927,7 +896,7 @@
927
896
  }
928
897
 
929
898
  args?.onLoadingStart?.call(this, i, file);
930
- if (debug) console.log("Context Load " + file);
899
+ if(debug) console.log("Context Load " + file);
931
900
  const res = await loader.loadSync(this, file, file, loadingHash, prog => {
932
901
  progressArg.name = file;
933
902
  progressArg.progress = prog;
@@ -1003,7 +972,7 @@
1003
972
  catch (err) {
1004
973
  this._renderlooperrors += 1;
1005
974
  if ((isDevEnvironment() || debug) && (err instanceof Error || err instanceof TypeError))
1006
- showBalloonMessage(`Caught unhandled exception during render-loop - see console for details.`, LogType.Error);
975
+ showBalloonMessage("Caught unhandled exception during render-loop.<br/>Stopping renderloop...<br/>See console for details.", LogType.Error);
1007
976
  console.error(err);
1008
977
  if (this._renderlooperrors > 10) {
1009
978
  console.warn("Stopping render loop due to error")
@@ -1038,11 +1007,7 @@
1038
1007
 
1039
1008
  private internalOnBeforeRender(timestamp: DOMHighResTimeStamp, frame: XRFrame | null) {
1040
1009
 
1041
- const sessionStarted = frame !== null && this._xrFrame === null;
1042
1010
  this._xrFrame = frame;
1043
- if (sessionStarted) {
1044
- this.domElement.dispatchEvent(new CustomEvent("xr-session-started", { detail: { context: this, session: this.xrSession, frame: frame } }));
1045
- }
1046
1011
 
1047
1012
  this._currentFrameEvent = FrameEvent.Undefined;
1048
1013
 
@@ -1081,13 +1046,6 @@
1081
1046
  this.setCurrentCamera(last);
1082
1047
  }
1083
1048
 
1084
- if (this.pre_update_oneshot_callbacks) {
1085
- for (const i in this.pre_update_oneshot_callbacks) {
1086
- this.pre_update_oneshot_callbacks[i]();
1087
- }
1088
- this.pre_update_oneshot_callbacks.length = 0;
1089
- }
1090
-
1091
1049
  if (this.pre_update_callbacks) {
1092
1050
  for (const i in this.pre_update_callbacks) {
1093
1051
  this.pre_update_callbacks[i]();
@@ -1170,7 +1128,7 @@
1170
1128
 
1171
1129
  if (this.pre_render_callbacks) {
1172
1130
  for (const i in this.pre_render_callbacks) {
1173
- this.pre_render_callbacks[i](frame);
1131
+ this.pre_render_callbacks[i]();
1174
1132
  }
1175
1133
  }
1176
1134
 
@@ -1248,8 +1206,8 @@
1248
1206
  }
1249
1207
  this._isRendering = true;
1250
1208
  this.renderRequiredTextures();
1209
+
1251
1210
 
1252
-
1253
1211
  if (this.composer && !this.isInXR) {
1254
1212
  this.composer.render(this.time.deltaTime);
1255
1213
  }
src/engine/engine_create_objects.ts CHANGED
@@ -1,7 +1,5 @@
1
- import { BoxGeometry, ColorRepresentation,DoubleSide, Material, Mesh, MeshBasicMaterial, MeshStandardMaterial, PlaneGeometry, SphereGeometry } from "three"
1
+ import { PlaneGeometry, MeshBasicMaterial, DoubleSide, Mesh, Material, MeshStandardMaterial, BoxGeometry, SphereGeometry, ColorRepresentation } from "three"
2
2
 
3
- import { Vec3 } from "./engine_types.js";
4
-
5
3
  export enum PrimitiveType {
6
4
  Quad = 0,
7
5
  Cube = 1,
@@ -11,10 +9,6 @@
11
9
  export type ObjectOptions = {
12
10
  name?: string,
13
11
  material?: Material,
14
- position?: Vec3,
15
- /** euler */
16
- rotation?: Vec3,
17
- scale?: Vec3,
18
12
  }
19
13
 
20
14
  export class ObjectUtils {
@@ -41,12 +35,6 @@
41
35
  }
42
36
  if (opts?.name)
43
37
  obj.name = opts.name;
44
- if (opts?.position)
45
- obj.position.set(opts.position.x, opts.position.y, opts.position.z);
46
- if (opts?.rotation)
47
- obj.rotation.set(opts.rotation.x, opts.rotation.y, opts.rotation.z);
48
- if (opts?.scale)
49
- obj.scale.set(opts.scale.x, opts.scale.y, opts.scale.z);
50
38
  return obj;
51
39
  }
52
40
  }
src/engine/engine_element_loading.ts CHANGED
@@ -1,9 +1,9 @@
1
- import { logoSVG } from "./assets/index.js"
2
1
  import { showBalloonWarning } from "./debug/index.js";
3
- import { hasCommercialLicense, hasProLicense, runtimeLicenseCheckPromise } from "./engine_license.js";
4
2
  import { Mathf } from "./engine_math.js";
5
3
  import { LoadingProgressArgs } from "./engine_setup.js";
6
4
  import { getParam } from "./engine_utils.js";
5
+ import { logoSVG } from "./assets/index.js"
6
+ import { hasCommercialLicense, hasProLicense, runtimeLicenseCheckPromise } from "./engine_license.js";
7
7
 
8
8
  const debug = getParam("debugloading");
9
9
  const debugRendering = getParam("debugloadingrendering");
@@ -233,7 +233,7 @@
233
233
  const maxWidth = 30;
234
234
  loadingBarContainer.style.display = "flex";
235
235
  loadingBarContainer.style.width = maxWidth + "%";
236
- loadingBarContainer.style.height = "3px";
236
+ loadingBarContainer.style.height = "2px";
237
237
  if (loadingStyle === "light")
238
238
  loadingBarContainer.style.backgroundColor = "rgba(0,0,0,.2)"
239
239
  else
@@ -247,15 +247,6 @@
247
247
  logo.style.marginBottom = "20px";
248
248
  logo.style.userSelect = "none";
249
249
  logo.style.objectFit = "contain";
250
- if (!hasCommercialLicense()) {
251
- logo.style.transition = "transform 1s ease-in-out, opacity 1s ease-in-out";
252
- logo.style.transform = "translateY(10px)";
253
- logo.style.opacity = "1";
254
- setTimeout(() => {
255
- logo.style.transform = "translateY(0px)";
256
- logo.style.opacity = "1";
257
- }, 1);
258
- }
259
250
  logo.src = logoSVG;
260
251
  let isUsingCustomLogo = false;
261
252
  if (hasLicense && this._element) {
@@ -332,16 +323,6 @@
332
323
  // if it's the case then we don't need to perform a runtime check
333
324
  if (commercialLicense) return;
334
325
 
335
- // If we don't have a commercial license, then we need to display our message
336
- if (debugLicense) console.log("Loading UI has commercial license?", commercialLicense);
337
- const nonCommercialContainer = document.createElement("div");
338
- nonCommercialContainer.style.paddingTop = ".6em";
339
- nonCommercialContainer.style.fontSize = ".8em";
340
- nonCommercialContainer.style.textTransform = "uppercase";
341
- nonCommercialContainer.innerText = "non commercial";
342
- nonCommercialContainer.style.opacity = "0";
343
- loadingElement.appendChild(nonCommercialContainer);
344
-
345
326
  // Use the runtime license check
346
327
  if (runtimeLicenseCheckPromise) {
347
328
  if (debugLicense) console.log("Waiting for runtime license check");
@@ -349,7 +330,13 @@
349
330
  commercialLicense = hasCommercialLicense();
350
331
  }
351
332
  if (commercialLicense) return;
352
- nonCommercialContainer.style.transition = "opacity .5s ease-in-out";
353
- nonCommercialContainer.style.opacity = "1";
333
+
334
+ // If we don't have a commercial license, then we need to display our message
335
+ if (debugLicense) console.log("Loading UI has commercial license?", commercialLicense);
336
+ const nonCommercialContainer = document.createElement("div");
337
+ nonCommercialContainer.style.paddingTop = ".6em";
338
+ nonCommercialContainer.style.fontSize = ".8em";
339
+ nonCommercialContainer.innerText = "NON COMMERCIAL";
340
+ loadingElement.appendChild(nonCommercialContainer);
354
341
  }
355
342
  }
src/engine/engine_element_overlay.ts CHANGED
@@ -16,7 +16,6 @@
16
16
  private _createdAROnlyElements: Array<any> = [];
17
17
  private _reparentedObjects: Array<{ el: Element, previousParent: HTMLElement | null }> = [];
18
18
  private contentElement: HTMLElement | null = null;
19
- private originalDomOverlayParent: ParentNode | null = null;
20
19
 
21
20
  requestEndAR = () => {
22
21
  this.onRequestedEndAR();
@@ -35,22 +34,6 @@
35
34
  this._reparentedObjects.push({ el: el, previousParent: el.parentElement });
36
35
  this.arContainer?.appendChild(el);
37
36
  }
38
-
39
- if(overlayContainer) {
40
- this.originalDomOverlayParent = overlayContainer.parentNode;
41
- if (this.originalDomOverlayParent)
42
- {
43
- console.log("Reparent DOM Overlay to body", overlayContainer, overlayContainer.style.display);
44
- // mozilla webxr does hide elements on session start
45
- // this is only necessary if we generated the overlay element
46
- overlayContainer.style.display = "";
47
- overlayContainer.style.visibility = "";
48
- document.body.appendChild(overlayContainer);
49
- }
50
- }
51
- else {
52
- console.warn("WebXRViewer: No DOM Overlay found");
53
- }
54
37
  }
55
38
  this.ensureQuitARButton(this.arContainer);
56
39
  }
src/engine/engine_element.ts CHANGED
@@ -1,15 +1,15 @@
1
- import { getLoader, registerLoader } from "../engine/engine_gltf.js";
1
+ import { Context, ContextCreateArgs, LoadingProgressArgs } from "./engine_setup.js";
2
+ import { AROverlayHandler, arContainerClassName } from "./engine_element_overlay.js";
2
3
  import { GameObject } from "../engine-components/Component.js";
3
- import { isDevEnvironment, showBalloonWarning } from "./debug/index.js";
4
- import { VERSION } from "./engine_constants.js";
5
4
  import { calculateProgress01, EngineLoadingView, type ILoadingViewHandler } from "./engine_element_loading.js";
6
- import { arContainerClassName,AROverlayHandler } from "./engine_element_overlay.js";
7
- import { hasCommercialLicense } from "./engine_license.js";
5
+ import { getParam } from "./engine_utils.js";
8
6
  import { setDracoDecoderPath, setDracoDecoderType, setKtx2TranscoderPath } from "./engine_loaders.js";
7
+ import { getLoader, registerLoader } from "../engine/engine_gltf.js";
9
8
  import { NeedleGltfLoader } from "./engine_scenetools.js";
10
- import { Context, ContextCreateArgs, LoadingProgressArgs } from "./engine_setup.js";
11
9
  import { type INeedleEngineComponent, type LoadedGLTF } from "./engine_types.js";
12
- import { getParam } from "./engine_utils.js";
10
+ import { isDevEnvironment, showBalloonWarning } from "./debug/index.js";
11
+ import { hasCommercialLicense } from "./engine_license.js";
12
+ import { VERSION } from "./engine_constants.js";
13
13
 
14
14
  //
15
15
  // registering loader here too to make sure it's imported when using engine via vanilla js
@@ -143,15 +143,12 @@
143
143
  }
144
144
  :host .quit-ar-button {
145
145
  position: absolute;
146
- // top: env(titlebar-area-y); /** this doesnt work **/
147
- top: 60px; /** camera access needs a bit more space **/
146
+ top: 40px;
148
147
  right: 20px;
149
148
  z-index: 9999;
150
149
  }
151
150
  </style>
152
- <div> <!-- this wrapper is necessary for WebXR https://github.com/meta-quest/immersive-web-emulator/issues/55 -->
153
- <canvas></canvas>
154
- </div>
151
+ <canvas></canvas>
155
152
  <div class="content">
156
153
  <slot class="overlay-content"></slot>
157
154
  </div>
@@ -170,7 +167,6 @@
170
167
  console.log("<needle-engine> connected");
171
168
  }
172
169
 
173
- this.addEventListener("xr-session-started", this.onXRSessionStarted);
174
170
  this.onSetupDesktop();
175
171
 
176
172
  if (!this.getAttribute("src")) {
@@ -200,8 +196,6 @@
200
196
  }
201
197
 
202
198
  disconnectedCallback() {
203
- this.removeEventListener("xr-session-started", this.onXRSessionStarted);
204
-
205
199
  this._didFullyLoad = false;
206
200
  const keepAlive = this.getAttribute("keep-alive");
207
201
  const dispose = keepAlive == undefined || (keepAlive?.length > 0 && keepAlive !== "true" && keepAlive !== "1");
@@ -390,23 +384,6 @@
390
384
  }));
391
385
  }
392
386
 
393
- private onXRSessionStarted = () => {
394
- const xrSessionMode = this.context.xrSessionMode;
395
- if (xrSessionMode === "immersive-ar")
396
- this.onEnterAR(this.context.xrSession!);
397
- else if (xrSessionMode === "immersive-vr")
398
- this.onEnterVR(this.context.xrSession!);
399
-
400
- // handle session end:
401
- this.context.xrSession?.addEventListener("end", () => {
402
- this.dispatchEvent(new CustomEvent("xr-session-ended", { detail: { session: this.context.xrSession, context: this._context, sessionMode: xrSessionMode } }));
403
- if (xrSessionMode === "immersive-ar")
404
- this.onExitAR(this.context.xrSession!);
405
- else if (xrSessionMode === "immersive-vr")
406
- this.onExitVR(this.context.xrSession!);
407
- });
408
- };
409
-
410
387
  /** called by the context when the first frame has been rendered */
411
388
  private onReady = () => this._loadingView?.onLoadingFinished();
412
389
  private onError = () => this._loadingView?.setMessage("Loading failed!");
@@ -497,10 +474,8 @@
497
474
  return null;
498
475
  }
499
476
 
500
- onEnterAR(session: XRSession) {
477
+ onEnterAR(session: XRSession, overlayContainer: HTMLElement) {
501
478
  this.onSetupAR();
502
- const overlayContainer = this.getAROverlayContainer();
503
- console.log("onEnterAR", session, overlayContainer);
504
479
  this._overlay_ar.onBegin(this._context!, overlayContainer, session);
505
480
  this.dispatchEvent(new CustomEvent("enter-ar", { detail: { session: session, context: this._context, htmlContainer: this._overlay_ar?.ARContainer } }));
506
481
  }
src/engine/engine_gameobject.ts CHANGED
@@ -1,18 +1,17 @@
1
1
  import { Bone, Object3D, Quaternion, SkinnedMesh, Vector3 } from "three";
2
-
3
- import { apply } from "../engine-components/js-extensions/Object3D.js";
4
- import { __internalNotifyObjectDestroyed as __internalRemoveReferences,disposeObjectResources } from "./engine_assetdatabase.js";
5
- import { ComponentEvents,ComponentLifecycleEvents } from "./engine_components_internal.js";
6
- import { activeInHierarchyFieldName } from "./engine_constants.js";
7
- import { editorGuidKeyName } from "./engine_constants.js";
8
- import { $isUsingInstancing, InstancingUtil } from "./engine_instancing.js";
9
2
  import { processNewScripts } from "./engine_mainloop_utils.js";
10
3
  import { InstantiateIdProvider } from "./engine_networking_instantiate.js";
11
- import { assign } from "./engine_serialization_core.js";
12
4
  import { Context, registerComponent } from "./engine_setup.js";
13
5
  import { logHierarchy, setWorldPosition, setWorldQuaternion } from "./engine_three_utils.js";
14
- import { type Constructor,type GuidsMap, type IComponent as Component, type IComponent, type IGameObject as GameObject, type UIDProvider } from "./engine_types.js";
6
+ import { type GuidsMap, type IComponent as Component, type IComponent, type IGameObject as GameObject, type UIDProvider, type Constructor } from "./engine_types.js";
15
7
  import { getParam, tryFindObject } from "./engine_utils.js";
8
+ import { apply } from "../engine-components/js-extensions/Object3D.js";
9
+ import { $isUsingInstancing, InstancingUtil } from "./engine_instancing.js";
10
+ import { activeInHierarchyFieldName } from "./engine_constants.js";
11
+ import { assign } from "./engine_serialization_core.js";
12
+ import { disposeObjectResources, __internalNotifyObjectDestroyed as __internalRemoveReferences } from "./engine_assetdatabase.js";
13
+ import { editorGuidKeyName } from "./engine_constants.js";
14
+ import { ComponentLifecycleEvents, ComponentEvents } from "./engine_components_internal.js";
16
15
 
17
16
  const debug = getParam("debuggetcomponent");
18
17
  const debugInstantiate = getParam("debuginstantiate");
@@ -286,6 +285,7 @@
286
285
  // }
287
286
  }
288
287
  }
288
+ console.log(options?.position)
289
289
 
290
290
  let context = Context.Current;
291
291
  if (options?.context) context = options.context;
src/engine/engine_gizmos.ts CHANGED
@@ -1,13 +1,11 @@
1
- import { AxesHelper,Box3, BoxGeometry, BufferAttribute, Color, type ColorRepresentation, CylinderGeometry, EdgesGeometry, Line, LineBasicMaterial, LineSegments, Mesh, Object3D, Quaternion, SphereGeometry, Vector3 } from 'three';
2
- import ThreeMeshUI, { Inline, Text } from "three-mesh-ui"
3
- import { type Options } from 'three-mesh-ui/build/types/core/elements/MeshUIBaseElement.js';
4
-
5
- import { isDestroyed } from './engine_gameobject.js';
1
+ import { BufferAttribute, Line, BoxGeometry, EdgesGeometry, Color, LineSegments, LineBasicMaterial, Object3D, Mesh, SphereGeometry, type ColorRepresentation, Vector3, Box3, Quaternion, CylinderGeometry, AxesHelper } from 'three';
6
2
  import { Context } from './engine_setup.js';
7
3
  import { getWorldPosition, lookAtObject, setWorldPositionXYZ } from './engine_three_utils.js';
8
4
  import type { Vec3, Vec4 } from './engine_types.js';
5
+ import ThreeMeshUI, { Inline, Text } from "three-mesh-ui"
9
6
  import { getParam } from './engine_utils.js';
10
- import { NeedleXRSession } from './engine_xr.js';
7
+ import { type Options } from 'three-mesh-ui/build/types/core/elements/MeshUIBaseElement.js';
8
+ import { isDestroyed } from './engine_gameobject.js';
11
9
 
12
10
  const _tmp = new Vector3();
13
11
  const _tmp2 = new Vector3();
@@ -23,15 +21,6 @@
23
21
 
24
22
  export class Gizmos {
25
23
 
26
- /**
27
- * Allow creating gizmos
28
- * If disabled then no gizmos will be added to the scene anymore
29
- */
30
- static enabled = true;
31
-
32
- /**
33
- * Returns true if a given object is a gizmo
34
- */
35
24
  static isGizmo(obj: Object3D) {
36
25
  return obj[$cacheSymbol] !== undefined;
37
26
  }
@@ -40,12 +29,10 @@
40
29
  * Draw a label in the scene or attached to an object (if a parent is provided)
41
30
  * @returns a handle to the label that can be used to change the text
42
31
  */
43
- static DrawLabel(position: Vec3, text: string, size: number = .05, duration: number = 0, color?: ColorRepresentation, backgroundColor?: ColorRepresentation | ColorWithAlpha, parent?: Object3D,) {
44
- if (!Gizmos.enabled) return null;
32
+ static DrawLabel(position: Vec3, text: string, size: number = .1, duration: number = 9999, color?: ColorRepresentation, backgroundColor?: ColorRepresentation | ColorWithAlpha, parent?: Object3D,) {
45
33
  if (!color) color = defaultColor;
46
- const rigScale = NeedleXRSession.active?.rigScale ?? 1;
47
- const element = Internal.getTextLabel(duration, text, size * rigScale, color, backgroundColor);
48
- if (parent instanceof Object3D) parent.add(element as any);
34
+ const element = Internal.getTextLabel(duration, text, size, color, backgroundColor);
35
+ if (parent instanceof Object3D) parent.add(element);
49
36
  element.position.x = position.x;
50
37
  element.position.y = position.y;
51
38
  element.position.z = position.z;
@@ -53,7 +40,6 @@
53
40
  }
54
41
 
55
42
  static DrawRay(origin: Vec3, dir: Vec3, color: ColorRepresentation = defaultColor, duration: number = 0, depthTest: boolean = true) {
56
- if (!Gizmos.enabled) return;
57
43
  const obj = Internal.getLine(duration);
58
44
  const positions = obj.geometry.getAttribute("position");
59
45
  positions.setXYZ(0, origin.x, origin.y, origin.z);
@@ -66,7 +52,6 @@
66
52
  }
67
53
 
68
54
  static DrawDirection(pt: Vec3, direction: Vec3 | Vec4, color: ColorRepresentation = defaultColor, duration: number = 0, depthTest: boolean = true, lengthFactor: number = 1) {
69
- if (!Gizmos.enabled) return;
70
55
  const obj = Internal.getLine(duration);
71
56
  const positions = obj.geometry.getAttribute("position");
72
57
  positions.setXYZ(0, pt.x, pt.y, pt.z);
@@ -88,8 +73,8 @@
88
73
  }
89
74
 
90
75
  static DrawLine(pt0: Vec3, pt1: Vec3, color: ColorRepresentation = defaultColor, duration: number = 0, depthTest: boolean = true) {
91
- if (!Gizmos.enabled) return;
92
76
  const obj = Internal.getLine(duration);
77
+
93
78
  const positions = obj.geometry.getAttribute("position");
94
79
  positions.setXYZ(0, pt0.x, pt0.y, pt0.z);
95
80
  positions.setXYZ(1, pt1.x, pt1.y, pt1.z);
@@ -100,7 +85,6 @@
100
85
  }
101
86
 
102
87
  static DrawWireSphere(center: Vec3, radius: number, color: ColorRepresentation = defaultColor, duration: number = 0, depthTest: boolean = true) {
103
- if (!Gizmos.enabled) return;
104
88
  const obj = Internal.getSphere(radius, duration, true);
105
89
  setWorldPositionXYZ(obj, center.x, center.y, center.z);
106
90
  obj.material["color"].set(color);
@@ -109,7 +93,6 @@
109
93
  }
110
94
 
111
95
  static DrawSphere(center: Vec3, radius: number, color: ColorRepresentation = defaultColor, duration: number = 0, depthTest: boolean = true) {
112
- if (!Gizmos.enabled) return;
113
96
  const obj = Internal.getSphere(radius, duration, false);
114
97
  setWorldPositionXYZ(obj, center.x, center.y, center.z);
115
98
  obj.material["color"].set(color);
@@ -118,7 +101,6 @@
118
101
  }
119
102
 
120
103
  static DrawWireBox(center: Vec3, size: Vec3, color: ColorRepresentation = defaultColor, duration: number = 0, depthTest: boolean = true) {
121
- if (!Gizmos.enabled) return;
122
104
  const obj = Internal.getBox(duration);
123
105
  obj.position.set(center.x, center.y, center.z);
124
106
  obj.scale.set(size.x, size.y, size.z);
@@ -129,7 +111,6 @@
129
111
  }
130
112
 
131
113
  static DrawWireBox3(box: Box3, color: ColorRepresentation = defaultColor, duration: number = 0, depthTest: boolean = true) {
132
- if (!Gizmos.enabled) return;
133
114
  const obj = Internal.getBox(duration);
134
115
  obj.position.copy(box.getCenter(_tmp));
135
116
  obj.scale.copy(box.getSize(_tmp));
@@ -141,7 +122,6 @@
141
122
 
142
123
  private static _up = new Vector3(0, 1, 0);
143
124
  static DrawArrow(pt0: Vec3, pt1: Vec3, color: ColorRepresentation = defaultColor, duration: number = 0, depthTest: boolean = true, wireframe: boolean = false) {
144
- if (!Gizmos.enabled) return;
145
125
  const obj = Internal.getArrowHead(duration);
146
126
  obj.position.set(pt1.x, pt1.y, pt1.z);
147
127
  obj.quaternion.setFromUnitVectors(this._up.set(0, 1, 0), _tmp.set(pt1.x, pt1.y, pt1.z).sub(_tmp2.set(pt0.x, pt0.y, pt0.z)).normalize());
@@ -214,7 +194,6 @@
214
194
  textContent: text,
215
195
  borderRadius: 1 * size,
216
196
  padding: 1 * size,
217
- whiteSpace: 'pre',
218
197
  };
219
198
 
220
199
  if (!element) {
@@ -222,7 +201,7 @@
222
201
  const global = this;
223
202
  const labelHandle = element as LabelHandle & Text;
224
203
  labelHandle.setText = function (str: string) {
225
- this.set({ textContent: str });
204
+ this.set({ textContent: str, whiteSpace: 'pre' });
226
205
  global.tmuiNeedsUpdate = true;
227
206
  };
228
207
  }
@@ -232,8 +211,9 @@
232
211
  // handle.setText(text);
233
212
  }
234
213
  this.tmuiNeedsUpdate = true;
235
- element.layers.enableAll();
236
- this.registerTimedObject(Context.Current, element as any, duration, this.textLabelCache as any);
214
+ element.layers.disableAll();
215
+ element.layers.enable(2);
216
+ this.registerTimedObject(Context.Current, element, duration, this.textLabelCache);
237
217
  return element as Text & LabelHandle;
238
218
  }
239
219
 
@@ -289,41 +269,20 @@
289
269
  private static textLabelCache: Array<Text> = [];
290
270
 
291
271
  private static registerTimedObject(context: Context, object: Object3D, duration: number, cache: Array<Object3D>) {
292
- const beforeRender = this.contextBeforeRenderCallbacks.get(context);
293
- const postRender = this.contextPostRenderCallbacks.get(context);
294
-
295
- if (!beforeRender) {
296
- const cb = () => { this.onBeforeRender(context, this.timedObjectsBuffer) };
297
- this.contextBeforeRenderCallbacks.set(context, cb);
298
- context.pre_render_callbacks.push(cb);
299
- }
300
- // make sure gizmo pre render is the last one being called
301
- else if (context.pre_render_callbacks[context.pre_render_callbacks.length - 1] !== beforeRender) {
302
- const index = context.pre_render_callbacks.indexOf(beforeRender);
303
- if (index >= 0) {
304
- context.pre_render_callbacks.splice(index, 1);
305
- }
306
- context.pre_render_callbacks.push(beforeRender);
307
- }
308
-
309
- if (!postRender) {
272
+ if (!this.contextPostRenderCallbacks.get(context)) {
310
273
  const cb = () => { this.onPostRender(context, this.timedObjectsBuffer, this.timesBuffer) };
311
274
  this.contextPostRenderCallbacks.set(context, cb);
312
275
  context.post_render_callbacks.push(cb);
313
276
  }
314
- // make sure gizmo post render is the last one being called
315
- else if (context.post_render_callbacks[context.post_render_callbacks.length - 1] !== postRender) {
316
- const index = context.post_render_callbacks.indexOf(postRender);
317
- if (index >= 0) {
318
- context.post_render_callbacks.splice(index, 1);
319
- }
320
- context.post_render_callbacks.push(postRender);
277
+ if (!this.contextBeforeRenderCallbacks.get(context)) {
278
+ const cb = () => { this.onBeforeRender(context, this.timedObjectsBuffer) };
279
+ this.contextBeforeRenderCallbacks.set(context, cb);
280
+ context.pre_render_callbacks.push(cb);
321
281
  }
322
282
 
283
+ object.renderOrder = 999999;
323
284
  object.layers.disableAll();
324
285
  object.layers.enable(2);
325
-
326
- object.renderOrder = 999999;
327
286
  object[$cacheSymbol] = cache;
328
287
  this.timedObjectsBuffer.push(object);
329
288
  this.timesBuffer.push(Context.Current.time.realtimeSinceStartup + duration);
@@ -345,13 +304,13 @@
345
304
  for (let i = 0; i < objects.length; i++) {
346
305
  const obj = objects[i];
347
306
  if (ctx.mainCamera && obj instanceof ThreeMeshUI.MeshUIBaseElement) {
348
- if (isDestroyed(obj as any)) {
307
+ if (isDestroyed(obj)) {
349
308
  continue;
350
309
  }
351
310
  const isInXR = ctx.isInVR;
352
311
  const keepUp = isInXR;
353
312
  const copyRotation = !isInXR;
354
- lookAtObject(obj as any, ctx.mainCamera, keepUp, copyRotation);
313
+ lookAtObject(obj, ctx.mainCamera, keepUp, copyRotation);
355
314
  }
356
315
  }
357
316
  }
src/engine/engine_gltf_builtin_components.ts CHANGED
@@ -1,20 +1,18 @@
1
1
  import "./codegen/register_types.js";
2
-
3
- import { Object3D } from "three";
4
-
5
- import { LogType, showBalloonMessage } from "./debug/index.js";
6
- import { addNewComponent } from "./engine_components.js";
7
- import { builtinComponentKeyName,editorGuidKeyName } from "./engine_constants.js";
8
- import { debugExtension } from "./engine_default_parameters.js";
2
+ import { TypeStore } from "./engine_typestore.js";
9
3
  import { InstantiateIdProvider } from "./engine_networking_instantiate.js"
10
- import { isLocalNetwork } from "./engine_networking_utils.js";
4
+ import { Context } from "./engine_setup.js";
11
5
  import { deserializeObject, serializeObject } from "./engine_serialization.js";
12
6
  import { assign, ImplementationInformation, type ISerializable, SerializationContext } from "./engine_serialization_core.js";
13
- import { Context } from "./engine_setup.js";
7
+ import { NEEDLE_components } from "./extensions/NEEDLE_components.js";
8
+ import { debugExtension } from "./engine_default_parameters.js";
9
+ import { editorGuidKeyName, builtinComponentKeyName } from "./engine_constants.js";
14
10
  import type { GuidsMap, ICamera, IComponent, IGameObject, SourceIdentifier, UIDProvider } from "./engine_types.js";
15
- import { TypeStore } from "./engine_typestore.js";
11
+ import { addNewComponent } from "./engine_components.js";
16
12
  import { getParam } from "./engine_utils.js";
17
- import { NEEDLE_components } from "./extensions/NEEDLE_components.js";
13
+ import { LogType, showBalloonMessage } from "./debug/index.js";
14
+ import { isLocalNetwork } from "./engine_networking_utils.js";
15
+ import { Object3D } from "three";
18
16
 
19
17
 
20
18
  const debug = debugExtension;
src/engine/engine_gltf.ts CHANGED
@@ -1,9 +1,8 @@
1
- import type { GLTF } from 'three/examples/jsm/loaders/GLTFLoader.js'
2
-
3
- import { SerializationContext } from "./engine_serialization_core.js";
1
+ import type { ConstructorConcrete, SourceIdentifier, UIDProvider } from "./engine_types.js";
4
2
  import { Context } from "./engine_setup.js";
5
- import type { ConstructorConcrete, SourceIdentifier, UIDProvider } from "./engine_types.js";
6
3
  import { NEEDLE_components } from "./extensions/NEEDLE_components.js";
4
+ import { SerializationContext } from "./engine_serialization_core.js";
5
+ import type { GLTF } from 'three/examples/jsm/loaders/GLTFLoader.js'
7
6
 
8
7
 
9
8
  export interface INeedleGltfLoader {
src/engine/engine_hot_reload.ts CHANGED
@@ -1,8 +1,8 @@
1
- import { addLog, LogType } from "./debug/debug_overlay.js";
2
- import { addScriptToArrays, removeScriptFromContext } from "./engine_mainloop_utils.js"
3
1
  import type { IComponent } from "./engine_types.js";
4
2
  import { TypeStore } from "./engine_typestore.js";
3
+ import { addScriptToArrays, removeScriptFromContext } from "./engine_mainloop_utils.js"
5
4
  import { getParam } from "./engine_utils.js";
5
+ import { addLog, LogType } from "./debug/debug_overlay.js";
6
6
 
7
7
  const debug = getParam("debughotreload");
8
8
 
src/engine/engine_input.ts CHANGED
@@ -1,137 +1,22 @@
1
- import { Matrix4, Object3D, Ray, Vector2, Vector3 } from 'three';
2
-
1
+ import { Vector2 } from 'three';
3
2
  import { showBalloonMessage, showBalloonWarning } from './debug/debug.js';
4
3
  import { Context } from './engine_setup.js';
5
- import type { ButtonName, IGameObject, IInput, MouseButtonName, Vec2 } from './engine_types.js';
6
- import { EnumToPrimitiveUnion, getParam, isMozillaXR } from './engine_utils.js';
4
+ import type { IInput, Vec2 } from './engine_types.js';
5
+ import { getParam } from './engine_utils.js';
7
6
 
8
7
  const debug = getParam("debuginput");
9
8
 
10
-
11
- export const enum PointerType {
12
- Mouse = "mouse",
13
- Touch = "touch",
14
- Controller = "controller",
15
- Hand = "hand"
16
- }
17
- export type PointerTypeNames = EnumToPrimitiveUnion<PointerType>;
18
-
19
- const enum PointerEnumType {
20
- PointerDown = "pointerdown",
21
- PointerUp = "pointerup",
22
- PointerMove = "pointermove",
23
- }
24
- const enum KeyboardEnumType {
25
- KeyDown = "keydown",
26
- KeyUp = "keyup",
27
- KeyPressed = "keypress"
28
- }
29
-
30
- export const enum InputEvents {
31
- PointerDown = "pointerdown",
32
- PointerUp = "pointerup",
33
- PointerMove = "pointermove",
34
- KeyDown = "keydown",
35
- KeyUp = "keyup",
36
- KeyPressed = "keypress"
37
- }
38
- /** e.g. `pointerdown` */
39
- export type InputEventNames = EnumToPrimitiveUnion<InputEvents>;
40
-
41
-
42
-
43
- export declare type NEPointerEventInit = PointerEventInit &
44
- {
45
- origin: object;
46
- pointerId: number;
47
- /** the index of the device */
48
- deviceIndex: number;
49
- pointerType: PointerTypeNames;
50
- mode: XRTargetRayMode,
51
- ray?: Ray;
52
- /** The control object for this input. In the case of spatial devices the controller,
53
- * otherwise a generated object in screen space. The object may not be in the scene. */
54
- device: IGameObject;
55
- buttonName: ButtonName | "none";
56
- }
57
-
58
-
59
9
  export class NEPointerEvent extends PointerEvent {
60
-
61
- /** the device index: mouse and touch are always 0, otherwise e.g. index of the connected Gamepad or XRController */
62
- readonly deviceIndex: number;
63
-
64
- /** The origin of the event contains a reference to the creator of this event.
65
- * This can be the Needle Engine input system or e.g. a XR controller
66
- */
67
- readonly origin: object;
68
-
69
- /** the browser event that triggered this event (if any) */
70
10
  readonly source: Event | null;
71
11
 
72
- readonly mode: XRTargetRayMode;
73
- /** A ray in worldspace for the event.
74
- * If the ray is undefined you can also use `space.worldForward` and `space.worldPosition` */
75
- readonly ray?: Ray;
76
- /** The device space (this object is not necessarily rendered in the scene but you can access or copy the matrix)
77
- * E.g. you can access the input world space source position with `space.worldPosition` or world direction with `space.worldForward`
78
- */
79
- readonly space: IGameObject;
80
-
81
- /** true if this event is a click */
82
- isClick: boolean = false;
83
- /** true if this event is a double click */
84
- isDoubleClick: boolean = false;
85
-
86
-
87
- /** Unique identifier for this input: a combination of the deviceIndex + button to uniquely identify the exact input (e.g. LeftController:Button0 = 0, RightController:Button1 = 101) */
88
- override get pointerId(): number { return this._pointerid; }
89
- private readonly _pointerid;
90
-
91
- // this is set via the init arguments (we override it here for intellisense to show the string options)
92
- override get pointerType(): PointerTypeNames { return this._pointerType; }
93
- private readonly _pointerType: PointerTypeNames;
94
-
95
- // this is set via the init arguments (we override it here for intellisense to show the string options)
96
- /** The input that raised this event like `pointerdown` */
97
- override get type(): InputEventNames { return this._type; }
98
- private readonly _type: InputEventNames;
99
-
100
- constructor(type: InputEvents | InputEventNames, source: Event | null, init: NEPointerEventInit) {
101
- super(type, init);
102
- // apply the init arguments. Otherwise the arguments will be undefined in the bundled / published version of needle engine
103
- // so we have to be careful if we override properties - we then also need to set them in the constructor
104
- this._pointerid = init.pointerId;
105
- this._pointerType = init.pointerType;
106
- this._type = type;
107
-
108
- this.deviceIndex = init.deviceIndex;
109
- this.origin = init.origin;
12
+ constructor(type: InputEvents, source: Event | null, init: PointerEventInit) {
13
+ super(type, init)
110
14
  this.source = source;
111
- this.mode = init.mode;
112
- this.ray = init.ray;
113
- this.space = init.device;
114
15
  }
115
-
116
- private _immediatePropagationStopped = false;
117
- get immediatePropagationStopped() {
118
- return this._immediatePropagationStopped;
119
- }
120
- private _propagationStopped = false;
121
- get propagationStopped() {
122
- return this._immediatePropagationStopped || this._propagationStopped;
123
- }
124
-
125
16
  stopImmediatePropagation(): void {
126
- this._immediatePropagationStopped = true;
127
17
  super.stopImmediatePropagation();
128
18
  this.source?.stopImmediatePropagation();
129
19
  }
130
- stopPropagation(): void {
131
- this._propagationStopped = true;
132
- super.stopPropagation();
133
- this.source?.stopPropagation();
134
- }
135
20
  }
136
21
  export class NEKeyboardEvent extends KeyboardEvent {
137
22
  source?: Event
@@ -156,44 +41,22 @@
156
41
  }
157
42
  }
158
43
 
44
+ export enum InputEvents {
45
+ PointerDown = "pointerdown",
46
+ PointerUp = "pointerup",
47
+ PointerMove = "pointermove",
48
+ KeyDown = "keydown",
49
+ KeyUp = "keyup",
50
+ KeyPressed = "keypress"
51
+ }
159
52
 
53
+ export enum PointerType {
54
+ Mouse = "mouse",
55
+ Touch = "touch",
56
+ }
160
57
 
161
- declare type PointerEventListener = (evt: NEPointerEvent) => void;
162
- declare type KeyboardEventListener = (evt: NEKeyboardEvent) => void;
163
- declare type InputEventListener = PointerEventListener | KeyboardEventListener;
58
+ export class Input extends EventTarget implements IInput {
164
59
 
165
- export class Input implements IInput {
166
-
167
- private readonly _pointerEventListener: { [key: string]: PointerEventListener[] } = {};
168
-
169
- addEventListener(type: InputEvents | InputEventNames, callback: PointerEventListener): void {
170
- if (!this._pointerEventListener[type]) this._pointerEventListener[type] = [];
171
- this._pointerEventListener[type].push(callback);
172
- }
173
- removeEventListener(type: InputEvents | InputEventNames, callback: PointerEventListener): void {
174
- if (!this._pointerEventListener[type]) return;
175
- const index = this._pointerEventListener[type].indexOf(callback);
176
- if (index >= 0) this._pointerEventListener[type].splice(index, 1);
177
- }
178
- private dispatchEvent(evt: NEPointerEvent | NEKeyboardEvent) {
179
- if (evt instanceof NEKeyboardEvent) {
180
- // TODO: implement. We want typescript to be smart enough to detect the event listener by the type union (e.g. keydown | keyup === keyboard events)
181
- }
182
- else {
183
- const listeners = this._pointerEventListener[evt.type];
184
- if (listeners) {
185
- for (const l of listeners) {
186
- if (evt.immediatePropagationStopped) {
187
- if (debug) console.log("immediatePropagationStopped", evt.type);
188
- break;
189
- }
190
- l(evt);
191
- }
192
- }
193
- }
194
- }
195
-
196
-
197
60
  _doubleClickTimeThreshold = .2;
198
61
  _longPressTimeThreshold = 1;
199
62
 
@@ -380,41 +243,7 @@
380
243
  private _mouseWheelDeltaY: number[] = [0];
381
244
  private _pointerEvent: Event[] = [];
382
245
  private _pointerUsed: boolean[] = [];
383
- /** This is added/updated for pointers. screenspace pointers set this to the camera near plane */
384
- private _pointerSpace: IGameObject[] = [];
385
246
 
386
-
387
-
388
- private readonly _pressedStack = new Map<number, number[]>();
389
- private onDownButton(pointerId: number, button: number) {
390
- let stack = this._pressedStack.get(pointerId);
391
- if (!stack) {
392
- stack = [];
393
- this._pressedStack.set(pointerId, stack);
394
- }
395
- stack.push(button);
396
- }
397
- private onReleaseButton(pointerId: number, button: number) {
398
- const stack = this._pressedStack.get(pointerId);
399
- if (!stack) return;
400
- const index = stack.indexOf(button);
401
- if (index >= 0) stack.splice(index, 1);
402
- }
403
- /** the first button that was down and is currently pressed */
404
- getFirstPressedButtonForPointer(pointerId: number): number | undefined {
405
- const stack = this._pressedStack.get(pointerId);
406
- if (!stack) return undefined;
407
- return stack[0];
408
- }
409
- /** the last (most recent) button that was down and is currently pressed */
410
- getLatestPressedButtonForPointer(pointerId: number): number | undefined {
411
- const stack = this._pressedStack.get(pointerId);
412
- if (!stack) return undefined;
413
- return stack[stack.length - 1];
414
- }
415
-
416
-
417
-
418
247
  getKeyDown(): string | null {
419
248
  for (const key in this.keysPressed) {
420
249
  const k = this.keysPressed[key];
@@ -484,58 +313,39 @@
484
313
  return null;
485
314
  }
486
315
 
487
- createInputEvent(args: NEPointerEvent) {
488
- // TODO: technically we would need to check for circular invocations here!
489
- switch (args.type) {
490
- case InputEvents.PointerDown:
491
- if (debug) showBalloonMessage("Create Pointer down");
492
- this.onDownButton(args.deviceIndex, args.button);
493
- this.onDown(args);
494
- break;
495
- case InputEvents.PointerMove:
496
- if (debug) showBalloonMessage("Create Pointer move");
497
- this.onMove(args);
498
- break;
499
- case InputEvents.PointerUp:
500
- if (debug) showBalloonMessage("Create Pointer up");
501
- this.onUp(args);
502
- this.onReleaseButton(args.deviceIndex, args.button);
503
- break;
504
- }
316
+ createPointerDown(args: NEPointerEvent) {
317
+ if (debug) showBalloonMessage("Create Pointer down");
318
+ this.onDown(args);
505
319
  }
506
320
 
321
+ createPointerMove(args: NEPointerEvent) {
322
+ if (debug) showBalloonMessage("Create Pointer move");
323
+ this.onMove(args);
324
+ }
325
+
326
+ createPointerUp(args: NEPointerEvent) {
327
+ if (debug) showBalloonMessage("Create Pointer up");
328
+ this.onUp(args);
329
+ }
330
+
507
331
  convertScreenspaceToRaycastSpace(vec2: Vec2) {
508
332
  vec2.x = (vec2.x - this.context.domX) / this.context.domWidth * 2 - 1;
509
333
  vec2.y = -((vec2.y - this.context.domY) / this.context.domHeight) * 2 + 1;
510
334
  }
511
335
 
512
336
  constructor(context: Context) {
337
+ super();
513
338
  this.context = context;
514
339
  this.context.post_render_callbacks.push(this.onEndOfFrame);
515
- }
516
340
 
517
- /** this is the html element we subscribed to for events */
518
- private _htmlEventSource!: HTMLElement;
519
-
520
- bindEvents() {
521
- this.unbindEvents();
522
-
523
- // we subscribe to the canvas element because we don't want to receive events when the user is interacting with the UI
524
- // e.g. if we have slotted HTML elements in the needle engine DOM elements we don't want to receive input events for those
525
- this._htmlEventSource = this.context.renderer.domElement;
526
-
527
- window.addEventListener('contextmenu', this.onContextMenu);
528
-
529
- this._htmlEventSource.addEventListener('touchstart', this.onTouchStart, false);
530
- window.addEventListener('touchstart', this.onTouchStartWindow);
341
+ window.addEventListener('touchstart', this.onTouchStart, false);
531
342
  window.addEventListener('touchmove', this.onTouchMove, { passive: true });
532
343
  window.addEventListener('touchend', this.onTouchUp, false);
533
- window.addEventListener("touchcancel", this.onTouchCancel, false);
534
344
 
535
- this._htmlEventSource.addEventListener('mousedown', this.onMouseDown);
345
+ window.addEventListener('mousedown', this.onMouseDown, false);
536
346
  window.addEventListener('mousemove', this.onMouseMove, false);
537
347
  window.addEventListener('mouseup', this.onMouseUp, false);
538
- this._htmlEventSource.addEventListener('wheel', this.onMouseWheel, { passive: true });
348
+ window.addEventListener('wheel', this.onMouseWheel, { passive: true });
539
349
 
540
350
  window.addEventListener("keydown", this.onKeyDown, false);
541
351
  window.addEventListener("keypress", this.onKeyPressed, false);
@@ -545,19 +355,18 @@
545
355
  window.addEventListener('blur', this.onLostFocus);
546
356
  }
547
357
 
548
- unbindEvents() {
549
- window.removeEventListener('contextmenu', this.onContextMenu);
358
+ dispose() {
359
+ const index = this.context.post_render_callbacks.indexOf(this.onEndOfFrame);
360
+ if (index >= 0) this.context.post_render_callbacks.splice(index, 1);
550
361
 
551
- this._htmlEventSource?.removeEventListener('touchstart', this.onTouchStart, false);
552
- window.removeEventListener('touchstart', this.onTouchStartWindow);
362
+ window.removeEventListener('touchstart', this.onTouchStart, false);
553
363
  window.removeEventListener('touchmove', this.onTouchMove, false);
554
364
  window.removeEventListener('touchend', this.onTouchUp, false);
555
- window.removeEventListener("touchcancel", this.onTouchCancel, false);
556
365
 
557
- this._htmlEventSource?.removeEventListener('mousedown', this.onMouseDown, false);
366
+ window.removeEventListener('mousedown', this.onMouseDown, false);
558
367
  window.removeEventListener('mousemove', this.onMouseMove, false);
559
368
  window.removeEventListener('mouseup', this.onMouseUp, false);
560
- this._htmlEventSource?.removeEventListener('wheel', this.onMouseWheel, false);
369
+ window.removeEventListener('wheel', this.onMouseWheel, false);
561
370
 
562
371
  window.removeEventListener("keydown", this.onKeyDown, false);
563
372
  window.removeEventListener("keypress", this.onKeyPressed, false);
@@ -566,12 +375,6 @@
566
375
  window.removeEventListener('blur', this.onLostFocus);
567
376
  }
568
377
 
569
- dispose() {
570
- const index = this.context.post_render_callbacks.indexOf(this.onEndOfFrame);
571
- if (index >= 0) this.context.post_render_callbacks.splice(index, 1);
572
- this.unbindEvents();
573
- }
574
-
575
378
  private onLostFocus = () => {
576
379
  for (const kp in this.keysPressed) {
577
380
  this.keysPressed[kp].pressed = false;
@@ -600,37 +403,14 @@
600
403
  // if(evt.target === this.context.renderer.domElement) return true;
601
404
  // const css = window.getComputedStyle(evt.target as HTMLElement);
602
405
  // if(css.pointerEvents === "all") return false;
406
+
603
407
  // We only check the target elements here since the canvas may be overlapped by other elements
604
408
  // in which case we do not want to use the input (e.g. if a HTML element is being triggered)
605
- if (evt.target === this.context.renderer?.domElement) return true;
606
- if (evt.target === this.context.domElement) return true;
607
-
608
- // looks like in Mozilla WebXR viewer the target element is the body
609
- if (this.context.isInAR && evt.target === document.body && isMozillaXR()) return true;
610
-
409
+ if(evt.target === this.context.renderer?.domElement) return true;
410
+ if(evt.target === this.context.domElement) return true;
611
411
  return false;
612
412
  }
613
413
 
614
- private onContextMenu = (evt: Event) => {
615
- if (this.canReceiveInput(evt) === false)
616
- return;
617
- if (evt instanceof PointerEvent) {
618
- // for longpress on touch there might open a context menu
619
- // in which case we set the pointer pressed back to false (resetting the pressed pointer)
620
- // we need to emit a pointer up event here as well
621
- if (evt.pointerType === "touch") {
622
- // for (const index in this._pointerPressed) {
623
- // if (this._pointerTypes[index] === PointerType.Touch) {
624
- // // this._pointerPressed[index] = false;
625
- // // this throws orbit controls?
626
- // // const ne = this.createPointerEventFromTouch("pointerup", parseInt(index), this._pointerPositions[index].x, this._pointerPositions[index].y, 0, evt);
627
- // // this.onUp(ne);
628
- // }
629
- // }
630
- }
631
- }
632
- }
633
-
634
414
  private keysPressed: { [key: KeyCode | string]: { pressed: boolean, frame: number, startFrame: number, key: string, code: KeyCode | string } } = {};
635
415
 
636
416
  private onKeyDown = (evt: KeyboardEvent) => {
@@ -673,12 +453,6 @@
673
453
  this._mouseWheelDeltaY[0] = current + evt.deltaY;
674
454
  }
675
455
 
676
- private onTouchStartWindow = (evt: TouchEvent) => {
677
- // onTouchStart registers on the renderer canvas so that we don't receive events when clicking on UI elements slotted in Needle Engine
678
- // however in AR we need to handle these events on the window since they're not emitted otherwise (see NE-4098)
679
- if (!this.context.isInAR) return;
680
- this.onTouchStart(evt);
681
- };
682
456
  private onTouchStart = (evt: TouchEvent) => {
683
457
  if (evt.changedTouches.length <= 0) return;
684
458
  if (this.canReceiveInput(evt) === false) return;
@@ -686,8 +460,7 @@
686
460
  const touch = evt.changedTouches[i];
687
461
  const id = this.getPointerIndex(touch.identifier)
688
462
  if (debug) showBalloonMessage(`touch start #${id}, identifier:${touch.identifier}`);
689
- const space = this.getAndUpdateSpatialObjectForScreenPosition(id, touch.clientX, touch.clientY);
690
- const ne = new NEPointerEvent(InputEvents.PointerDown, evt, { origin: this, mode: "screen", deviceIndex: 0, pointerId: id, button: 0, clientX: touch.clientX, clientY: touch.clientY, pointerType: PointerType.Touch, buttonName: "unknown", device: space, pressure: touch.force });
463
+ const ne = new NEPointerEvent(InputEvents.PointerDown, evt, { button: id, clientX: touch.clientX, clientY: touch.clientY, pointerType: PointerType.Touch });
691
464
  this.onDown(ne);
692
465
  }
693
466
  }
@@ -696,9 +469,8 @@
696
469
  if (evt.changedTouches.length <= 0) return;
697
470
  for (let i = 0; i < evt.changedTouches.length; i++) {
698
471
  const touch = evt.changedTouches[i];
699
- const id = this.getPointerIndex(touch.identifier);
700
- const space = this.getAndUpdateSpatialObjectForScreenPosition(id, touch.clientX, touch.clientY);
701
- const ne = new NEPointerEvent(InputEvents.PointerMove, evt, { origin: this, mode: "screen", deviceIndex: 0, pointerId: id, button: 0, clientX: touch.clientX, clientY: touch.clientY, pointerType: PointerType.Touch, buttonName: "unknown", device: space, pressure: touch.force });
472
+ const id = this.getPointerIndex(touch.identifier)
473
+ const ne = new NEPointerEvent(InputEvents.PointerMove, evt, { button: id, clientX: touch.clientX, clientY: touch.clientY, pointerType: PointerType.Touch });
702
474
  this.onMove(ne);
703
475
  }
704
476
  }
@@ -708,96 +480,38 @@
708
480
  for (let i = 0; i < evt.changedTouches.length; i++) {
709
481
  const touch = evt.changedTouches[i];
710
482
  const id = this.getPointerIndex(touch.identifier);
483
+
711
484
  if (!this.isNewEvent(evt.timeStamp, id, this._pointerUpTimestamp)) continue;
712
- const ne = this.createPointerEventFromTouch("pointerup", touch.identifier, touch.clientX, touch.clientY, touch.force, evt);
485
+
486
+ if (debug) showBalloonMessage(`touch up #${id}, identifier:${touch.identifier}`);
487
+ const ne = new NEPointerEvent(InputEvents.PointerUp, evt, { button: id, clientX: touch.clientX, clientY: touch.clientY, pointerType: PointerType.Touch });
713
488
  this.onUp(ne);
714
489
  }
715
490
  }
716
- private createPointerEventFromTouch(type: InputEventNames, touchIdentifier: number, x: number, y: number, force: number, evt: Event): NEPointerEvent {
717
- const id = this.getPointerIndex(touchIdentifier);
718
- if (debug) showBalloonMessage(`touch up #${id}, identifier:${touchIdentifier}`);
719
- const space = this.getAndUpdateSpatialObjectForScreenPosition(id, x, y);
720
- const ne = new NEPointerEvent(type, evt, { origin: this, mode: "screen", deviceIndex: 0, pointerId: id, button: 0, clientX: x, clientY: y, pointerType: PointerType.Touch, buttonName: "unknown", device: space, pressure: force });
721
- return ne;
722
- }
723
491
 
724
- private onTouchCancel = (_evt: Event) => {
725
- };
726
-
727
492
  private onMouseDown = (evt: MouseEvent) => {
728
- this.onDownButton(0, evt.button);
729
- if (this.context.isInVR) return;
730
493
  if (evt.defaultPrevented) return;
731
494
  if (this.canReceiveInput(evt) === false) return;
732
- // TODO: if we have multiple mouse devices we need to get the deviceId
733
- const button = evt.button;
734
- let buttonName: MouseButtonName | "none" = "none";
735
- switch (button) {
736
- case 0: buttonName = "left"; break;
737
- case 1: buttonName = "middle"; break;
738
- case 2: buttonName = "right"; break;
739
- }
740
- const pointerId = 0 + button;
741
- const space = this.getAndUpdateSpatialObjectForScreenPosition(pointerId, evt.clientX, evt.clientY);
742
- const ne = new NEPointerEvent(InputEvents.PointerDown, evt, { origin: this, mode: "screen", deviceIndex: 0, pointerId, button: button, clientX: evt.clientX, clientY: evt.clientY, pointerType: PointerType.Mouse, buttonName: buttonName, device: space, pressure: 1 });
495
+ const id = evt.button;
496
+ const ne = new NEPointerEvent(InputEvents.PointerDown, evt, { button: id, clientX: evt.clientX, clientY: evt.clientY, pointerType: PointerType.Mouse });
743
497
  this.onDown(ne);
744
498
  }
745
499
 
746
500
  private onMouseMove = (evt: MouseEvent) => {
747
- if (this.context.isInVR) return;
748
501
  if (evt.defaultPrevented) return;
749
- // take the last pressed button (or should the first pressed button have priority?)
750
- const pressedButton = this.getFirstPressedButtonForPointer(0);
751
- const button = pressedButton ?? 0;
752
- const pointerId = 0 + button;
753
- const space = this.getAndUpdateSpatialObjectForScreenPosition(pointerId, evt.clientX, evt.clientY);
754
- const pressure = pressedButton !== undefined ? 1 : 0;
755
- const ne = new NEPointerEvent(InputEvents.PointerMove, evt, { origin: this, mode: "screen", deviceIndex: 0, pointerId, button: button, clientX: evt.clientX, clientY: evt.clientY, pointerType: PointerType.Mouse, buttonName: "none", device: space, pressure });
502
+ const id = evt.button;
503
+ const ne = new NEPointerEvent(InputEvents.PointerMove, evt, { button: id, clientX: evt.clientX, clientY: evt.clientY, pointerType: PointerType.Mouse });
756
504
  this.onMove(ne);
757
505
  }
758
506
 
759
507
  private onMouseUp = (evt: MouseEvent) => {
760
- this.onReleaseButton(0, evt.button);
761
- if (this.context.isInVR) return;
762
- const button = evt.button;
763
- if (!this.isNewEvent(evt.timeStamp, button, this._pointerUpTimestamp)) return;
764
- let buttonName: MouseButtonName | "none" = "none";
765
- switch (button) {
766
- case 0: buttonName = "left"; break;
767
- case 1: buttonName = "middle"; break;
768
- case 2: buttonName = "right"; break;
769
- }
770
- const pointerId = 0 + button;
771
508
  if (evt.defaultPrevented) return;
772
- const space = this.getAndUpdateSpatialObjectForScreenPosition(pointerId, evt.clientX, evt.clientY);
773
- const ne = new NEPointerEvent(InputEvents.PointerUp, evt, { origin: this, mode: "screen", deviceIndex: 0, pointerId, button: button, clientX: evt.clientX, clientY: evt.clientY, pointerType: PointerType.Mouse, buttonName: buttonName, device: space, pressure: 0 });
509
+ const id = evt.button;
510
+ if (!this.isNewEvent(evt.timeStamp, id, this._pointerUpTimestamp)) return;
511
+ const ne = new NEPointerEvent(InputEvents.PointerUp, evt, { button: id, clientX: evt.clientX, clientY: evt.clientY, pointerType: PointerType.Mouse });
774
512
  this.onUp(ne);
775
513
  }
776
514
 
777
- private readonly tempNearPlaneVector = new Vector3();
778
- private readonly tempFarPlaneVector = new Vector3();
779
- private readonly tempLookMatrix = new Matrix4();
780
- private getAndUpdateSpatialObjectForScreenPosition(id: number, screenX: number, screenY: number): IGameObject {
781
- let space = this._pointerSpace[id]
782
- if (!space) {
783
- space = new Object3D() as unknown as IGameObject;
784
- this._pointerSpace[id] = space;
785
- }
786
- this._pointerSpace[id] = space;
787
- const camera = this.context.mainCamera;
788
- if (camera) {
789
- const pointOnNearPlane = this.tempNearPlaneVector.set(screenX, screenY, -1);
790
- this.convertScreenspaceToRaycastSpace(pointOnNearPlane);
791
- const pointOnFarPlane = this.tempFarPlaneVector.set(pointOnNearPlane.x, pointOnNearPlane.y, 1);
792
- pointOnNearPlane.unproject(camera);
793
- pointOnFarPlane.unproject(camera);
794
- this.tempLookMatrix.lookAt(pointOnFarPlane, pointOnNearPlane, (camera as any as IGameObject).worldUp);
795
- space.position.set(pointOnNearPlane.x, pointOnNearPlane.y, pointOnNearPlane.z);
796
- space.quaternion.setFromRotationMatrix(this.tempLookMatrix);
797
- }
798
- return space;
799
- }
800
-
801
515
  // Prevent the same event being handled twice (e.g. on touch we get a mouseUp and touchUp evt with the same timestamp)
802
516
  private isNewEvent(timestamp: number, index: number, arr: number[]): boolean {
803
517
  while (arr.length <= index) arr.push(-1);
@@ -818,18 +532,12 @@
818
532
  }
819
533
 
820
534
  private onDown(evt: NEPointerEvent) {
821
- const index = evt.pointerId;
822
- if (this.getPointerPressed(index)) {
823
- console.warn(`pointerId is already pressed: ${index}`, debug ? evt : '');
824
- }
825
- if (debug) console.log(evt.pointerType, "DOWN", index);
535
+ if (debug) console.log(evt.pointerType, "DOWN", evt.button);
826
536
  if (!this.isInRect(evt)) return;
827
537
 
828
- // TODO: this whole pointer handling doesnt consider that in VR we have multiple pointers. It's not enough to store the button as the pointer ID anymore
829
-
830
538
  // check if we received an mouse UP event for a touch (for some reason we get a mouse.down for touch.up)
831
539
  if (evt.pointerType === PointerType.Mouse) {
832
- const upTime = this._pointerUpTimestamp[index];
540
+ const upTime = this._pointerUpTimestamp[evt.button];
833
541
  if (upTime > 0 && evt.source?.timeStamp !== undefined) {
834
542
  const diff = (evt.source.timeStamp - upTime);
835
543
  // on android touch up and mouse up have the exact same value
@@ -842,20 +550,20 @@
842
550
  }
843
551
  }
844
552
 
845
- this.setPointerState(index, this._pointerPressed, true);
846
- this.setPointerState(index, this._pointerDown, true);
847
- this.setPointerStateT(index, this._pointerEvent, evt.source);
553
+ this.setPointerState(evt.button, this._pointerPressed, true);
554
+ this.setPointerState(evt.button, this._pointerDown, true);
555
+ this.setPointerStateT(evt.button, this._pointerEvent, evt.source);
848
556
 
849
- while (index >= this._pointerTypes.length) this._pointerTypes.push(evt.pointerType);
850
- this._pointerTypes[index] = evt.pointerType;
557
+ while (evt.button >= this._pointerTypes.length) this._pointerTypes.push(evt.pointerType);
558
+ this._pointerTypes[evt.button] = evt.pointerType;
851
559
 
852
- while (index >= this._pointerPositionDown.length) this._pointerPositionDown.push(new Vector2());
853
- this._pointerPositionDown[index].set(evt.clientX, evt.clientY);
854
- while (index >= this._pointerPositions.length) this._pointerPositions.push(new Vector2());
855
- this._pointerPositions[index].set(evt.clientX, evt.clientY);
560
+ while (evt.button >= this._pointerPositionDown.length) this._pointerPositionDown.push(new Vector2());
561
+ this._pointerPositionDown[evt.button].set(evt.clientX, evt.clientY);
562
+ while (evt.button >= this._pointerPositions.length) this._pointerPositions.push(new Vector2());
563
+ this._pointerPositions[evt.button].set(evt.clientX, evt.clientY);
856
564
 
857
- if (index >= this._pointerDownTime.length) this._pointerDownTime.push(0);
858
- this._pointerDownTime[index] = this.context.time.time;
565
+ if (evt.button >= this._pointerDownTime.length) this._pointerDownTime.push(0);
566
+ this._pointerDownTime[evt.button] = this.context.time.time;
859
567
 
860
568
  this.updatePointerPosition(evt);
861
569
 
@@ -863,60 +571,63 @@
863
571
  }
864
572
  // moveEvent?: Event;
865
573
  private onMove(evt: NEPointerEvent) {
866
- const index = evt.pointerId;
867
-
574
+ const index = evt.button;
575
+
868
576
  const isDown = this.getPointerPressed(index);
869
577
  if (isDown === false && !this.isInRect(evt)) return;
870
578
  if (evt.pointerType === PointerType.Touch && !isDown) return;
871
- if (debug) console.log(evt.pointerType, "MOVE", index, "hasSpace=" + evt.space != null);
872
-
579
+ if (debug) console.log(evt.pointerType, "MOVE", index);
580
+
873
581
  this.updatePointerPosition(evt);
874
582
  this.setPointerStateT(index, this._pointerEvent, evt.source);
875
583
  this.onDispatchEvent(evt);
876
584
  }
877
585
  private onUp(evt: NEPointerEvent) {
878
- const index = evt.pointerId;
879
- const wasDown = this.getPointerPressed(index);
586
+ if (this._pointerIds?.length >= evt.button)
587
+ this._pointerIds[evt.button] = -1;
588
+ const wasDown = this._pointerPressed[evt.button];
880
589
  if (!wasDown) {
881
- if (debug) console.log(evt.pointerType, "UP", index, "was not down");
590
+ if (debug) console.log(evt.pointerType, "UP", evt.button, "was not down");
882
591
  return;
883
592
  }
884
- if (debug) console.log(evt.pointerType, "UP", index);
885
- this.setPointerState(index, this._pointerPressed, false);
886
- this.setPointerStateT(index, this._pointerEvent, evt.source);
887
- this.setPointerState(index, this._pointerUp, true);
593
+ if (debug) console.log(evt.pointerType, "UP", evt.button);
594
+ this.setPointerState(evt.button, this._pointerPressed, false);
595
+ this.setPointerStateT(evt.button, this._pointerEvent, evt.source);
888
596
 
889
- while (index >= this._pointerUsed.length) this._pointerUsed.push(false);
890
- this.setPointerState(index, this._pointerUsed, false);
597
+ // if (!this.isInRect(evt)) {
598
+ // if (debug) showBalloonWarning("Pointer out of bounds: " + evt.clientX + ", " + evt.clientY);
599
+ // return;
600
+ // }
601
+ this.setPointerState(evt.button, this._pointerUp, true);
891
602
 
603
+ while (evt.button >= this._pointerUsed.length) this._pointerUsed.push(false);
604
+ this.setPointerState(evt.button, this._pointerUsed, false);
605
+
892
606
  this.updatePointerPosition(evt);
893
607
 
894
- if (!this._pointerPositionDown[index]) {
895
- if (debug) showBalloonWarning("Received pointer up event without matching down event for button: " + index);
896
- console.warn("Received pointer up event without matching down event for button: " + index)
608
+ if (!this._pointerPositionDown[evt.button]) {
609
+ if (debug) showBalloonWarning("Received pointer up event without matching down event for button: " + evt.button);
610
+ console.warn("Received pointer up event without matching down event for button: " + evt.button)
897
611
  return;
898
612
  }
899
- const dx = evt.clientX - this._pointerPositionDown[index].x;
900
- const dy = evt.clientY - this._pointerPositionDown[index].y;
613
+ const dx = evt.clientX - this._pointerPositionDown[evt.button].x;
614
+ const dy = evt.clientY - this._pointerPositionDown[evt.button].y;
901
615
 
902
- if (index >= this._pointerUpTime.length) this._pointerUpTime.push(-99);
616
+ if (evt.button >= this._pointerUpTime.length) this._pointerUpTime.push(-99);
903
617
 
904
-
618
+ // console.log(dx, dy);
905
619
  if (Math.abs(dx) < 5 && Math.abs(dy) < 5) {
906
- if (debug) console.log("CLICK", index)
907
- this.setPointerState(index, this._pointerClick, true);
908
- evt.isClick = true;
620
+ this.setPointerState(evt.button, this._pointerClick, true);
909
621
 
910
622
  // handle double click
911
- const lastUp = this._pointerUpTime[index];
623
+ const lastUp = this._pointerUpTime[evt.button];
912
624
  const dt = this.context.time.time - lastUp;
913
625
  // console.log(dt);
914
626
  if (dt < this._doubleClickTimeThreshold && dt > 0) {
915
- this.setPointerState(index, this._pointerDoubleClick, true);
916
- evt.isDoubleClick = true;
627
+ this.setPointerState(evt.button, this._pointerDoubleClick, true);
917
628
  }
918
629
  }
919
- this._pointerUpTime[index] = this.context.time.time;
630
+ this._pointerUpTime[evt.button] = this.context.time.time;
920
631
 
921
632
  this.onDispatchEvent(evt);
922
633
  }
@@ -934,11 +645,11 @@
934
645
  let dx = evt.clientX - lf.x;
935
646
  let dy = evt.clientY - lf.y;
936
647
  // if pointer is locked, clientX and Y are not changed, but Movement is.
937
- if (evt.source instanceof MouseEvent || evt.source instanceof TouchEvent) {
648
+ if(evt.source instanceof MouseEvent || evt.source instanceof TouchEvent) {
938
649
  const source = evt.source as PointerEvent;
939
- if (dx === 0 && source.movementX !== 0)
650
+ if(dx === 0 && source.movementX !== 0)
940
651
  dx = source.movementX || 0;
941
- if (dy === 0 && source.movementY !== 0)
652
+ if(dy === 0 && source.movementY !== 0)
942
653
  dy = source.movementY || 0;
943
654
  }
944
655
  delta.x += dx;
@@ -980,16 +691,16 @@
980
691
  }
981
692
 
982
693
  private setPointerState(index: number, arr: boolean[], value: boolean) {
694
+ while (arr.length <= index) arr.push(false);
983
695
  arr[index] = value;
984
696
  }
985
697
 
986
698
  private setPointerStateT<T>(index: number, arr: T[], value: T) {
987
- // while (arr.length <= index) arr.push(null as any);
699
+ while (arr.length <= index) arr.push(null as any);
988
700
  arr[index] = value;
989
- return value;
990
701
  }
991
702
 
992
- private onDispatchEvent(evt: NEPointerEvent | NEKeyboardEvent) {
703
+ private onDispatchEvent(evt: NEPointerEvent | KeyboardEvent) {
993
704
  const prevContext = Context.Current;
994
705
  try {
995
706
  Context.Current = this.context;
@@ -1089,81 +800,81 @@
1089
800
  | "F11"
1090
801
  | "F12";
1091
802
 
1092
- // KEY_1 = 49,
1093
- // KEY_2 = 50,
1094
- // KEY_3 = 51,
1095
- // KEY_4 = 52,
1096
- // KEY_5 = 53,
1097
- // KEY_6 = 54,
1098
- // KEY_7 = 55,
1099
- // KEY_8 = 56,
1100
- // KEY_9 = 57,
1101
- // KEY_A = 65,
1102
- // KEY_B = 66,
1103
- // KEY_C = 67,
1104
- // KEY_D = "d",
1105
- // KEY_E = 69,
1106
- // KEY_F = 70,
1107
- // KEY_G = 71,
1108
- // KEY_H = 72,
1109
- // KEY_I = 73,
1110
- // KEY_J = 74,
1111
- // KEY_K = 75,
1112
- // KEY_L = 76,
1113
- // KEY_M = 77,
1114
- // KEY_N = 78,
1115
- // KEY_O = 79,
1116
- // KEY_P = 80,
1117
- // KEY_Q = 81,
1118
- // KEY_R = 82,
1119
- // KEY_S = 83,
1120
- // KEY_T = 84,
1121
- // KEY_U = 85,
1122
- // KEY_V = 86,
1123
- // KEY_W = 87,
1124
- // KEY_X = 88,
1125
- // KEY_Y = 89,
1126
- // KEY_Z = 90,
1127
- // LEFT_META = 91,
1128
- // RIGHT_META = 92,
1129
- // SELECT = 93,
1130
- // NUMPAD_0 = 96,
1131
- // NUMPAD_1 = 97,
1132
- // NUMPAD_2 = 98,
1133
- // NUMPAD_3 = 99,
1134
- // NUMPAD_4 = 100,
1135
- // NUMPAD_5 = 101,
1136
- // NUMPAD_6 = 102,
1137
- // NUMPAD_7 = 103,
1138
- // NUMPAD_8 = 104,
1139
- // NUMPAD_9 = 105,
1140
- // MULTIPLY = 106,
1141
- // ADD = 107,
1142
- // SUBTRACT = 109,
1143
- // DECIMAL = 110,
1144
- // DIVIDE = 111,
1145
- // F1 = 112,
1146
- // F2 = 113,
1147
- // F3 = 114,
1148
- // F4 = 115,
1149
- // F5 = 116,
1150
- // F6 = 117,
1151
- // F7 = 118,
1152
- // F8 = 119,
1153
- // F9 = 120,
1154
- // F10 = 121,
1155
- // F11 = 122,
1156
- // F12 = 123,
1157
- // NUM_LOCK = 144,
1158
- // SCROLL_LOCK = 145,
1159
- // SEMICOLON = 186,
1160
- // EQUALS = 187,
1161
- // COMMA = 188,
1162
- // DASH = 189,
1163
- // PERIOD = 190,
1164
- // FORWARD_SLASH = 191,
1165
- // GRAVE_ACCENT = 192,
1166
- // OPEN_BRACKET = 219,
1167
- // BACK_SLASH = 220,
1168
- // CLOSE_BRACKET = 221,
1169
- // SINGLE_QUOTE = 222
803
+ // KEY_1 = 49,
804
+ // KEY_2 = 50,
805
+ // KEY_3 = 51,
806
+ // KEY_4 = 52,
807
+ // KEY_5 = 53,
808
+ // KEY_6 = 54,
809
+ // KEY_7 = 55,
810
+ // KEY_8 = 56,
811
+ // KEY_9 = 57,
812
+ // KEY_A = 65,
813
+ // KEY_B = 66,
814
+ // KEY_C = 67,
815
+ // KEY_D = "d",
816
+ // KEY_E = 69,
817
+ // KEY_F = 70,
818
+ // KEY_G = 71,
819
+ // KEY_H = 72,
820
+ // KEY_I = 73,
821
+ // KEY_J = 74,
822
+ // KEY_K = 75,
823
+ // KEY_L = 76,
824
+ // KEY_M = 77,
825
+ // KEY_N = 78,
826
+ // KEY_O = 79,
827
+ // KEY_P = 80,
828
+ // KEY_Q = 81,
829
+ // KEY_R = 82,
830
+ // KEY_S = 83,
831
+ // KEY_T = 84,
832
+ // KEY_U = 85,
833
+ // KEY_V = 86,
834
+ // KEY_W = 87,
835
+ // KEY_X = 88,
836
+ // KEY_Y = 89,
837
+ // KEY_Z = 90,
838
+ // LEFT_META = 91,
839
+ // RIGHT_META = 92,
840
+ // SELECT = 93,
841
+ // NUMPAD_0 = 96,
842
+ // NUMPAD_1 = 97,
843
+ // NUMPAD_2 = 98,
844
+ // NUMPAD_3 = 99,
845
+ // NUMPAD_4 = 100,
846
+ // NUMPAD_5 = 101,
847
+ // NUMPAD_6 = 102,
848
+ // NUMPAD_7 = 103,
849
+ // NUMPAD_8 = 104,
850
+ // NUMPAD_9 = 105,
851
+ // MULTIPLY = 106,
852
+ // ADD = 107,
853
+ // SUBTRACT = 109,
854
+ // DECIMAL = 110,
855
+ // DIVIDE = 111,
856
+ // F1 = 112,
857
+ // F2 = 113,
858
+ // F3 = 114,
859
+ // F4 = 115,
860
+ // F5 = 116,
861
+ // F6 = 117,
862
+ // F7 = 118,
863
+ // F8 = 119,
864
+ // F9 = 120,
865
+ // F10 = 121,
866
+ // F11 = 122,
867
+ // F12 = 123,
868
+ // NUM_LOCK = 144,
869
+ // SCROLL_LOCK = 145,
870
+ // SEMICOLON = 186,
871
+ // EQUALS = 187,
872
+ // COMMA = 188,
873
+ // DASH = 189,
874
+ // PERIOD = 190,
875
+ // FORWARD_SLASH = 191,
876
+ // GRAVE_ACCENT = 192,
877
+ // OPEN_BRACKET = 219,
878
+ // BACK_SLASH = 220,
879
+ // CLOSE_BRACKET = 221,
880
+ // SINGLE_QUOTE = 222
src/engine/engine_license.ts CHANGED
@@ -1,8 +1,8 @@
1
+ import { getParam, isMobileDevice } from "./engine_utils.js";
2
+ import { ContextEvent, ContextRegistry } from "./engine_context_registry.js";
3
+ import type { IContext } from "./engine_types.js";
1
4
  import { logoSVG } from "./assets/index.js";
2
5
  import { GENERATOR, VERSION } from "./engine_constants.js";
3
- import { ContextEvent, ContextRegistry } from "./engine_context_registry.js";
4
- import type { IContext } from "./engine_types.js";
5
- import { getParam, isMobileDevice } from "./engine_utils.js";
6
6
 
7
7
  const debug = getParam("debuglicense");
8
8
 
@@ -50,21 +50,18 @@
50
50
  const licenseUrl = "https://engine.needle.tools/licensing/check?location=" + encodeURIComponent(window.location.href) + "&version=" + VERSION + "&generator=" + encodeURIComponent(GENERATOR);
51
51
  const res = await fetch(licenseUrl, {
52
52
  method: "GET",
53
- }).catch(_err => {
54
- if (debug) console.error("License check failed", _err);
55
- return undefined;
56
- });
53
+ }).catch();
57
54
  if (res?.status === 200) {
58
55
  applicationIsForbidden = false;
59
56
  if (debug) console.log("License check succeeded");
60
57
  NEEDLE_ENGINE_LICENSE_TYPE = "pro";
61
58
  }
62
- else if (res?.status === 403) {
59
+ else if (res.status === 403) {
63
60
  applicationIsForbidden = true;
64
61
  applicationForbiddenText = await res.text();
65
62
  }
66
63
  else {
67
- if (debug) console.log("License check failed with status " + res?.status);
64
+ if (debug) console.log("License check failed with status " + res.status);
68
65
  }
69
66
  }
70
67
  catch (err) {
src/engine/engine_lifecycle_api.ts CHANGED
@@ -1,54 +1,30 @@
1
+ import { ContextEvent } from "./engine_context_registry.js";
1
2
  import { FrameEvent } from "./engine_context.js";
2
- import { ContextEvent } from "./engine_context_registry.js";
3
3
  import { LifecycleMethod, registerFrameEventCallback } from "./engine_lifecycle_functions_internal.js";
4
4
 
5
5
 
6
6
  /**
7
7
  * Register a callback in the engine context created event.
8
8
  * This happens once per context (after the context has been created and the first content has been loaded)
9
- * ```ts
10
- * onInitialized((ctx : Context) => {
11
- * // do something
12
- * }
13
- * ```
14
- * */
9
+ */
15
10
  export function onInitialized(cb: LifecycleMethod) {
16
11
  registerFrameEventCallback(cb, ContextEvent.ContextCreated);
17
12
  }
18
13
 
19
14
  /** Register a callback in the engine start event.
20
- * This happens at the beginning of each frame
21
- * ```ts
22
- * onStart((ctx : Context) => {
23
- * // do something
24
- * }
25
- * ```
26
- * */
15
+ * This happens at the beginning of each frame */
27
16
  export function onStart(cb: LifecycleMethod) {
28
17
  registerFrameEventCallback(cb, FrameEvent.Start);
29
18
  }
30
19
 
31
20
 
32
21
  /** Register a callback in the engine update event
33
- * This is called every frame
34
- * ```ts
35
- * onUpdate((ctx : Context) => {
36
- * // do something
37
- * }
38
- * ```
22
+ * This is called every frame
39
23
  * */
40
24
  export function onUpdate(cb: LifecycleMethod) {
41
25
  registerFrameEventCallback(cb, FrameEvent.Update);
42
26
  }
43
27
 
44
- /** Register a callback in the engine onBeforeRender event
45
- * This is called every frame
46
- * ```ts
47
- * onBeforeRender((ctx : Context) => {
48
- * // do something
49
- * }
50
- * ```
51
- * */
52
28
  export function onBeforeRender(cb: LifecycleMethod) {
53
29
  registerFrameEventCallback(cb, FrameEvent.OnBeforeRender);
54
30
  }
src/engine/engine_lifecycle_functions_internal.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { type Context,FrameEvent } from "./engine_context.js";
1
+ import { safeInvoke } from "./engine_generic_utils.js";
2
+ import { FrameEvent, type Context } from "./engine_context.js";
2
3
  import type { ContextEvent } from "./engine_context_registry.js";
3
- import { safeInvoke } from "./engine_generic_utils.js";
4
4
 
5
5
  export declare type LifecycleMethod = (ctx: Context) => void;
6
6
  export declare type Event = ContextEvent | FrameEvent;
src/engine/engine_lightdata.ts CHANGED
@@ -1,9 +1,8 @@
1
- import { ShaderChunk, Texture, UniformsLib, Vector4 } from "three";
2
-
1
+ import { LightmapType } from "./extensions/NEEDLE_lightmaps.js";
2
+ import { Texture, ShaderChunk, UniformsLib, Vector4 } from "three";
3
3
  import { Context } from "./engine_setup.js";
4
+ import { getParam } from "./engine_utils.js";
4
5
  import type { SourceIdentifier } from "./engine_types.js";
5
- import { getParam } from "./engine_utils.js";
6
- import { LightmapType } from "./extensions/NEEDLE_lightmaps.js";
7
6
 
8
7
  const debugLightmap = getParam("debuglightmaps") ? true : false;
9
8
 
src/engine/engine_loaders.ts CHANGED
@@ -1,10 +1,10 @@
1
1
 
2
- import { MeshoptDecoder } from 'three/examples/jsm/libs/meshopt_decoder.module.js';
2
+ import { Context } from "./engine_setup.js"
3
+ import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
3
4
  import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js';
4
- import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
5
5
  import { KTX2Loader } from 'three/examples/jsm/loaders/KTX2Loader.js';
6
+ import { MeshoptDecoder } from 'three/examples/jsm/libs/meshopt_decoder.module.js';
6
7
 
7
- import { Context } from "./engine_setup.js"
8
8
  import { getParam } from "./engine_utils.js";
9
9
 
10
10
  const debug = getParam("debugdecoders");
src/engine/engine_mainloop_utils.ts CHANGED
@@ -1,13 +1,11 @@
1
+ import { safeInvoke } from "./engine_generic_utils.js";
2
+ import * as constants from "./engine_constants.js";
3
+ import { getParam } from './engine_utils.js';
1
4
  import { CubeCamera, Object3D, Scene, WebGLCubeRenderTarget } from 'three';
2
-
5
+ import type { IComponent, IContext } from './engine_types.js';
6
+ import { isActiveSelf } from './engine_gameobject.js';
7
+ import { ContextRegistry } from "./engine_context_registry.js";
3
8
  import { isDevEnvironment } from "./debug/index.js";
4
- import * as constants from "./engine_constants.js";
5
- import { ContextRegistry } from "./engine_context_registry.js";
6
- import { isActiveSelf } from './engine_gameobject.js';
7
- import { safeInvoke } from "./engine_generic_utils.js";
8
- import type { IComponent, IContext } from './engine_types.js';
9
- import { getParam } from './engine_utils.js';
10
- import type { INeedleXRSessionEventReceiver } from "./engine_xr.js";
11
9
 
12
10
  const debug = getParam("debugnewscripts");
13
11
  const debugHierarchy = getParam("debughierarchy");
@@ -210,12 +208,9 @@
210
208
  if (script.onBeforeRender) context.scripts_onBeforeRender.push(script);
211
209
  if (script.onAfterRender) context.scripts_onAfterRender.push(script);
212
210
  if (script.onPausedChanged) context.scripts_pausedChanged.push(script);
213
- if (isNeedleXRSessionEventReceiver(script, null)) context.new_scripts_xr.push(script);
214
- // do we want to check if a XR session is active before adding scripts here?
215
- if (isNeedleXRSessionEventReceiver(script, "immersive-vr")) context.scripts_immersive_vr.push(script);
216
- if (isNeedleXRSessionEventReceiver(script, "immersive-ar")) context.scripts_immersive_ar.push(script);
217
211
  }
218
212
 
213
+
219
214
  export function removeScriptFromContext(script: any, context: IContext) {
220
215
  removeFromArray(script, context.new_scripts);
221
216
  removeFromArray(script, context.new_script_start);
@@ -226,9 +221,6 @@
226
221
  removeFromArray(script, context.scripts_onBeforeRender);
227
222
  removeFromArray(script, context.scripts_onAfterRender);
228
223
  removeFromArray(script, context.scripts_pausedChanged);
229
- removeFromArray(script, context.new_scripts_xr);
230
- removeFromArray(script, context.scripts_immersive_vr);
231
- removeFromArray(script, context.scripts_immersive_ar);
232
224
  context.stopAllCoroutinesFrom(script);
233
225
  }
234
226
 
@@ -237,26 +229,7 @@
237
229
  if (index >= 0) array.splice(index, 1);
238
230
  }
239
231
 
240
- export function isNeedleXRSessionEventReceiver(script: any, mode: XRSessionMode | null): script is INeedleXRSessionEventReceiver {
241
- if (script) {
242
- const i = script as Partial<INeedleXRSessionEventReceiver>;
243
- if (i.onBeforeXR ||
244
- i.onEnterXR ||
245
- i.onUpdateXR ||
246
- i.onLeaveXR ||
247
- i.onXRControllerAdded ||
248
- i.onXRControllerRemoved
249
- ) {
250
- if (mode != null) {
251
- if (i.supportsXR?.(mode) === false) return false;
252
- }
253
- return true;
254
- }
255
- }
256
- return false;
257
- }
258
232
 
259
-
260
233
  export function updateIsActive(obj?: Object3D) {
261
234
  if (!obj) obj = ContextRegistry.Current.scene;
262
235
  if (!obj) {
src/engine/engine_networking_auto.ts CHANGED
@@ -1,6 +1,6 @@
1
+ import { getParam } from "./engine_utils.js";
1
2
  import { isDevEnvironment } from "./debug/index.js";
2
3
  import type { IComponent } from "./engine_types.js";
3
- import { getParam } from "./engine_utils.js";
4
4
 
5
5
  const debug = getParam("debugautosync");
6
6
 
src/engine/engine_networking_files_default_components.ts CHANGED
@@ -1,9 +1,8 @@
1
1
  // import { SyncedTransform } from "../engine-components/SyncedTransform.js";
2
2
  // import { DragControls } from "../engine-components/DragControls.js"
3
3
  // import { ObjectRaycaster } from "../engine-components/ui/Raycaster.js";
4
+ import type { UIDProvider } from "./engine_types.js";
4
5
  import type { GLTF } from "three/examples/jsm/loaders/GLTFLoader.js";
5
-
6
- import type { UIDProvider } from "./engine_types.js";
7
6
  // import { Animation } from "../engine-components/Animation.js";
8
7
 
9
8
 
src/engine/engine_networking_files.ts CHANGED
@@ -1,16 +1,15 @@
1
- import { BoxGeometry, BoxHelper, Mesh, MeshBasicMaterial, Object3D, Vector3 } from "three";
2
- import type { GLTF } from 'three/examples/jsm/loaders/GLTFLoader.js'
3
-
4
- import { getLoader } from "../engine/engine_gltf.js";
1
+ import { Context } from "../engine/engine_setup.js";
2
+ import * as web from "../engine/engine_web_api.js";
5
3
  import { NetworkConnection } from "../engine/engine_networking.js";
6
4
  import { generateSeed, InstantiateIdProvider } from "../engine/engine_networking_instantiate.js";
7
- import { Context } from "../engine/engine_setup.js";
8
- import * as web from "../engine/engine_web_api.js";
9
- import { ContextEvent, ContextRegistry } from "./engine_context_registry.js";
10
- import { findByGuid } from "./engine_gameobject.js";
11
5
  import * as def from "./engine_networking_files_default_components.js"
6
+ import type { GLTF } from 'three/examples/jsm/loaders/GLTFLoader.js'
7
+ import { getLoader } from "../engine/engine_gltf.js";
12
8
  import type { IModel } from "./engine_networking_types.js";
13
9
  import type { IGameObject } from "./engine_types.js";
10
+ import { findByGuid } from "./engine_gameobject.js";
11
+ import { ContextEvent, ContextRegistry } from "./engine_context_registry.js";
12
+ import { BoxGeometry, BoxHelper, Mesh, MeshBasicMaterial, Object3D, Vector3 } from "three";
14
13
 
15
14
  export enum File_Event {
16
15
  File_Spawned = "file-spawned",
src/engine/engine_networking_instantiate.ts CHANGED
@@ -1,20 +1,20 @@
1
1
  // import { IModel, NetworkConnection } from "./engine_networking.js"
2
2
  import * as THREE from "three";
3
- import { Object3D } from "three";
3
+ import { Context } from "./engine_setup.js"
4
+ import * as utils from "./engine_utils.js"
5
+ import type { INetworkConnection } from "./engine_networking_types.js";
6
+ import type { IGameObject as GameObject, IComponent as Component } from "./engine_types.js"
7
+
4
8
  // https://github.com/uuidjs/uuid
5
9
  // v5 takes string and namespace
6
10
  import { v5 } from 'uuid';
7
-
8
- import { ContextEvent, ContextRegistry } from "../engine/engine_context_registry.js";
9
- import { destroy, findByGuid, IInstantiateOptions, instantiate } from "./engine_gameobject.js";
10
- import { InstantiateOptions } from "./engine_gameobject.js";
11
- import type { INetworkConnection } from "./engine_networking_types.js";
11
+ import type { UIDProvider } from "./engine_types.js";
12
12
  import type { IModel } from "./engine_networking_types.js";
13
13
  import { SendQueue } from "./engine_networking_types.js";
14
- import { Context } from "./engine_setup.js"
15
- import type { IComponent as Component,IGameObject as GameObject } from "./engine_types.js"
16
- import type { UIDProvider } from "./engine_types.js";
17
- import * as utils from "./engine_utils.js"
14
+ import { IInstantiateOptions, destroy, findByGuid, instantiate } from "./engine_gameobject.js";
15
+ import { Object3D } from "three";
16
+ import { InstantiateOptions } from "./engine_gameobject.js";
17
+ import { ContextEvent, ContextRegistry } from "../engine/engine_context_registry.js";
18
18
 
19
19
 
20
20
 
@@ -163,7 +163,7 @@
163
163
  }
164
164
  }
165
165
 
166
- export class NewInstanceModel implements IModel {
166
+ class NewInstanceModel implements IModel {
167
167
  guid: string;
168
168
  originalGuid: string;
169
169
  seed: number | undefined;
@@ -176,9 +176,6 @@
176
176
  rotation: { x: number, y: number, z: number, w: number } | undefined;
177
177
  scale: { x: number, y: number, z: number } | undefined;
178
178
 
179
- /** Set to true to prevent this model from being instantiated */
180
- preventCreation?: boolean = undefined;
181
-
182
179
  constructor(originalGuid: string, newGuid: string) {
183
180
  this.originalGuid = originalGuid;
184
181
  this.guid = newGuid;
@@ -252,13 +249,11 @@
252
249
  export function beginListenInstantiate(context: Context) {
253
250
  context.connection.beginListen(InstantiateEvent.NewInstanceCreated, async (model: NewInstanceModel) => {
254
251
  const obj: GameObject | null = await tryResolvePrefab(model.originalGuid, context.scene) as GameObject;
255
- if (model.preventCreation === true) {
256
- return;
257
- }
258
252
  if (!obj) {
259
253
  console.warn("could not find object that was instantiated: " + model.guid);
260
254
  return;
261
255
  }
256
+ // console.log(model);
262
257
  const options = new InstantiateOptions();
263
258
  if (model.position)
264
259
  options.position = new THREE.Vector3(model.position.x, model.position.y, model.position.z);
src/engine/engine_networking_peer.ts CHANGED
@@ -1,6 +1,5 @@
1
+ import Peer, { type PeerConnectOption } from "peerjs";
1
2
  import type { DataConnection, PeerJSOption } from "peerjs";
2
- import Peer, { type PeerConnectOption } from "peerjs";
3
-
4
3
  import { type ConstructorConcrete } from "./engine_types.js";
5
4
 
6
5
  let peerOptions: PeerJSOption | undefined = undefined;
src/engine/engine_networking_streams.ts CHANGED
@@ -1,13 +1,12 @@
1
+ import { type Context } from "./engine_context.js";
1
2
  import Peer, { MediaConnection } from "peerjs"
2
- import { EventDispatcher } from "three";
3
-
4
3
  import { RoomEvents } from "../engine/engine_networking.js";
5
4
  import { UserJoinedOrLeftRoomModel } from "../engine/engine_networking.js";
5
+ import type { IModel } from "./engine_networking_types.js";
6
6
  import { getPeerjsInstance } from "../engine/engine_networking_peer.js";
7
- import { type Context } from "./engine_context.js";
8
- import type { IModel } from "./engine_networking_types.js";
7
+ import { EventDispatcher } from "three";
8
+ import { getParam } from "./engine_utils.js";
9
9
  import { type IComponent } from "./engine_types.js";
10
- import { getParam } from "./engine_utils.js";
11
10
 
12
11
 
13
12
 
@@ -57,7 +56,7 @@
57
56
  Outgoing = "outgoing",
58
57
  }
59
58
 
60
- class CallHandle extends EventDispatcher<any> {
59
+ class CallHandle extends EventDispatcher {
61
60
  readonly userId: string;
62
61
  readonly direction: CallDirection;
63
62
  readonly call: MediaConnection;
@@ -106,7 +105,7 @@
106
105
  }
107
106
  }
108
107
 
109
- export class PeerHandle extends EventDispatcher<any> {
108
+ export class PeerHandle extends EventDispatcher {
110
109
 
111
110
  private static readonly instances: Map<string, PeerHandle> = new Map();
112
111
 
@@ -306,7 +305,7 @@
306
305
  // userId: string;
307
306
  // }
308
307
 
309
- export class NetworkedStreams extends EventDispatcher<any> {
308
+ export class NetworkedStreams extends EventDispatcher {
310
309
 
311
310
  static create(comp: IComponent) {
312
311
  const peer = PeerHandle.getOrCreate(comp.context, comp.context.connection.connectionId!);
src/engine/engine_networking.ts CHANGED
@@ -1,21 +1,19 @@
1
1
  const defaultNetworkingBackendUrlProvider = "https://urls.needle.tools/default-networking-backend/index";
2
2
  let serverUrl: string | undefined = "wss://needle-tiny-starter.glitch.me/socket";
3
3
 
4
+ import { Websocket, type WebsocketBuilder } from 'websocket-ts';
5
+ // import { Networking } from '../engine-components/Networking.js';
6
+ import { Context } from './engine_setup.js';
7
+ import * as utils from "./engine_utils.js";
4
8
  import * as flatbuffers from 'flatbuffers';
5
- import { type Websocket } from 'websocket-ts';
6
-
7
9
  import * as schemes from "../engine-schemes/schemes.js";
8
- import { isDevEnvironment } from './debug/debug.js';
9
10
  import { PeerNetworking } from './engine_networking_peer.js';
10
11
  import { type IModel, type INetworkConnection, SendQueue } from './engine_networking_types.js';
11
12
  import { isHostedOnGlitch } from './engine_networking_utils.js';
12
- // import { Networking } from '../engine-components/Networking.js';
13
- import { Context } from './engine_setup.js';
14
- import * as utils from "./engine_utils.js";
13
+ import { isDevEnvironment } from './debug/debug.js';
15
14
 
16
15
  export const debugNet = utils.getParam("debugnet") ? true : false;
17
16
  export const debugOwner = debugNet || utils.getParam("debugowner") ? true : false;
18
- const debugnetBin = utils.getParam("debugnetbin");
19
17
 
20
18
  export interface INetworkingWebsocketUrlProvider {
21
19
  getWebsocketUrl(): string | null;
@@ -391,7 +389,7 @@
391
389
 
392
390
  /** Send a binary message to the server (broadcasted to all connected users) */
393
391
  public sendBinary(bin: Uint8Array) {
394
- if (debugnetBin) console.log("<< send binary", this.context.time.frame, (bin.length / 1024) + " KB");
392
+ if (debugNet) console.log("<< bin", bin.length);
395
393
  this._ws?.send(bin);
396
394
  }
397
395
 
@@ -549,11 +547,10 @@
549
547
  console.error("⊠ Websocket error", i, ev);
550
548
  resolve(false);
551
549
  })
550
+ .onMessage(this.onMessage.bind(this))
552
551
  .onRetry(() => { console.log("Retry connecting to networking websocket") })
553
552
  .build();
554
- ws.addEventListener(pkg.WebsocketEvent.message, (socket, msg) => {
555
- this.onMessage(socket, msg);
556
- });
553
+
557
554
  });
558
555
  }
559
556
 
@@ -584,7 +581,6 @@
584
581
  }
585
582
 
586
583
  private async handleIncomingBinaryMessage(blob: Blob) {
587
- if (debugnetBin) console.log("<< bin", this.context.time.frame);
588
584
  const buf = await blob.arrayBuffer();
589
585
  var data = new Uint8Array(buf);
590
586
  const bb = new flatbuffers.ByteBuffer(data);
src/engine/engine_physics_rapier.ts CHANGED
@@ -1,29 +1,29 @@
1
- import { ActiveCollisionTypes, ActiveEvents, Ball, CoefficientCombineRule, Collider, ColliderDesc, Cuboid, EventQueue, JointData, QueryFilterFlags, Ray, RigidBody, RigidBodyType, ShapeColliderTOI, ShapeType, World } from '@dimforge/rapier3d-compat';
2
1
  import { BufferAttribute, BufferGeometry, LineBasicMaterial, LineSegments, Matrix4, Mesh, Object3D, Quaternion, Vector3 } from 'three'
3
2
  import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js'
4
-
5
- import { CollisionDetectionMode, type PhysicsMaterial, PhysicsMaterialCombine } from '../engine/engine_physics.types.js';
6
- import { isDevEnvironment } from './debug/debug.js';
7
- import { ContextEvent, ContextRegistry } from './engine_context_registry.js';
8
- import { foreachComponent } from './engine_gameobject.js';
9
- import { Gizmos } from './engine_gizmos.js';
10
- import { Mathf } from './engine_math.js';
3
+ import { CircularBuffer, getParam } from "./engine_utils.js"
11
4
  import { getWorldPosition, getWorldQuaternion, getWorldScale, setWorldPositionXYZ, setWorldQuaternionXYZW } from "./engine_three_utils.js"
12
5
  import type {
13
- IBoxCollider,
6
+ IPhysicsEngine,
7
+ IComponent,
14
8
  ICollider,
15
- IComponent,
9
+ IRigidbody,
10
+ Vec3,
11
+ IGameObject,
12
+ Vec2,
16
13
  IContext,
17
- IGameObject,
18
- IPhysicsEngine,
19
- IRigidbody,
20
14
  ISphereCollider,
21
- Vec2,
22
- Vec3,
15
+ IBoxCollider,
23
16
  } from './engine_types.js';
24
- import { Collision,ContactPoint } from './engine_types.js';
17
+ import { ContactPoint, Collision } from './engine_types.js';
18
+ import { foreachComponent } from './engine_gameobject.js';
19
+
20
+ import { ActiveCollisionTypes, ActiveEvents, CoefficientCombineRule, Ball, Collider, ColliderDesc, EventQueue, JointData, QueryFilterFlags, RigidBody, RigidBodyType, ShapeColliderTOI, World, Ray, ShapeType, Cuboid } from '@dimforge/rapier3d-compat';
21
+ import { CollisionDetectionMode, type PhysicsMaterial, PhysicsMaterialCombine } from '../engine/engine_physics.types.js';
22
+ import { Gizmos } from './engine_gizmos.js';
23
+ import { Mathf } from './engine_math.js';
25
24
  import { SphereOverlapResult } from './engine_types.js';
26
- import { CircularBuffer, getParam } from "./engine_utils.js"
25
+ import { ContextEvent, ContextRegistry } from './engine_context_registry.js';
26
+ import { isDevEnvironment } from './debug/debug.js';
27
27
 
28
28
  const debugPhysics = getParam("debugphysics");
29
29
  const debugColliderPlacement = getParam("debugcolliderplacement");
@@ -166,14 +166,12 @@
166
166
  addForce(rigidbody: IRigidbody, force: Vec3, wakeup: boolean) {
167
167
  this.validate();
168
168
  const body = this.internal_getRigidbody(rigidbody);
169
- if(body) body.addForce(force, wakeup)
170
- else console.warn("Rigidbody doesn't exist: can not apply force");
169
+ body?.addForce(force, wakeup)
171
170
  }
172
171
  addImpulse(rigidbody: IRigidbody, force: Vec3, wakeup: boolean) {
173
172
  this.validate();
174
173
  const body = this.internal_getRigidbody(rigidbody);
175
- if (body) body.applyImpulse(force, wakeup);
176
- else console.warn("Rigidbody doesn't exist: can not apply impulse");
174
+ body?.applyImpulse(force, wakeup)
177
175
  }
178
176
  getLinearVelocity(comp: IRigidbody | ICollider): Vec3 | null {
179
177
  this.validate();
@@ -206,15 +204,13 @@
206
204
  applyImpulse(rb: IRigidbody, vec: Vec3, wakeup: boolean) {
207
205
  this.validate();
208
206
  const body = this.internal_getRigidbody(rb);
209
- if(body) body.applyImpulse(vec, wakeup);
210
- else console.warn("Rigidbody doesn't exist: can not apply impulse");
207
+ body?.applyImpulse(vec, wakeup);
211
208
  }
212
209
 
213
210
  wakeup(rb: IRigidbody) {
214
211
  this.validate();
215
212
  const body = this.internal_getRigidbody(rb);
216
- if(body) body.wakeUp();
217
- else console.warn("Rigidbody doesn't exist: can not wake up");
213
+ body?.wakeUp();
218
214
  }
219
215
  isSleeping(rb: IRigidbody) {
220
216
  this.validate();
@@ -224,14 +220,12 @@
224
220
  setAngularVelocity(rb: IRigidbody, vec: Vec3, wakeup: boolean) {
225
221
  this.validate();
226
222
  const body = this.internal_getRigidbody(rb);
227
- if(body) body.setAngvel(vec, wakeup);
228
- else console.warn("Rigidbody doesn't exist: can not set angular velocity");
223
+ body?.setAngvel(vec, wakeup);
229
224
  }
230
225
  setLinearVelocity(rb: IRigidbody, vec: Vec3, wakeup: boolean) {
231
226
  this.validate();
232
227
  const body = this.internal_getRigidbody(rb);
233
- if(body) body.setLinvel(vec, wakeup);
234
- else console.warn("Rigidbody doesn't exist: can not set linear velocity");
228
+ body?.setLinvel(vec, wakeup);
235
229
  }
236
230
 
237
231
  private context?: IContext;
@@ -994,22 +988,6 @@
994
988
  }
995
989
  this.world.step(this.eventQueue);
996
990
  this._isUpdatingPhysicsWorld = false;
997
- }
998
-
999
- public postStep() {
1000
- if (!this.world) return;
1001
- if (!this.enabled) return;
1002
- this._isUpdatingPhysicsWorld = true;
1003
- this.syncObjects();
1004
- this._isUpdatingPhysicsWorld = false;
1005
-
1006
- if (this.eventQueue && !this.collisionHandler) {
1007
- this.collisionHandler = new PhysicsCollisionHandler(this.world, this.eventQueue);
1008
- }
1009
- if (this.collisionHandler) {
1010
- this.collisionHandler.handleCollisionEvents();
1011
- this.collisionHandler.update();
1012
- }
1013
991
  this.updateDebugRendering(this.world);
1014
992
  }
1015
993
 
@@ -1017,7 +995,7 @@
1017
995
  if (debugPhysics || debugColliderPlacement || showColliders || this.debugRenderColliders === true) {
1018
996
  if (!this.lines) {
1019
997
  const material = new LineBasicMaterial({
1020
- color: 0x77dd77,
998
+ color: 0x227700,
1021
999
  fog: false,
1022
1000
  // vertexColors: THREE.VertexColors
1023
1001
  });
@@ -1039,6 +1017,22 @@
1039
1017
  }
1040
1018
  }
1041
1019
 
1020
+ public postStep() {
1021
+ if (!this.world) return;
1022
+ if (!this.enabled) return;
1023
+ this._isUpdatingPhysicsWorld = true;
1024
+ this.syncObjects();
1025
+ this._isUpdatingPhysicsWorld = false;
1026
+
1027
+ if (this.eventQueue && !this.collisionHandler) {
1028
+ this.collisionHandler = new PhysicsCollisionHandler(this.world, this.eventQueue);
1029
+ }
1030
+ if (this.collisionHandler) {
1031
+ this.collisionHandler.handleCollisionEvents();
1032
+ this.collisionHandler.update();
1033
+ }
1034
+ }
1035
+
1042
1036
  /** sync rendered objects with physics world (except for colliders without rigidbody) */
1043
1037
  private syncObjects() {
1044
1038
  if (debugColliderPlacement) return;
@@ -1075,8 +1069,8 @@
1075
1069
  if (center && center.isVector3) {
1076
1070
  this._tempQuaternion.set(rot.x, rot.y, rot.z, rot.w);
1077
1071
  const offset = this._tempPosition.copy(center).applyQuaternion(this._tempQuaternion);
1078
- const scale = getWorldScale(obj.gameObject);
1079
- offset.multiply(scale);
1072
+ // const scale = getWorldScale(obj.gameObject);
1073
+ // offset.multiply(scale);
1080
1074
  pos.x -= offset.x;
1081
1075
  pos.y -= offset.y;
1082
1076
  pos.z -= offset.z;
@@ -1173,14 +1167,8 @@
1173
1167
  this._tempCenterPos.z = center.z;
1174
1168
  getWorldScale(collider.gameObject, this._tempCenterVec);
1175
1169
  this._tempCenterPos.multiply(this._tempCenterVec);
1176
- if (!collider.attachedRigidbody)
1177
- {
1178
- getWorldQuaternion(collider.gameObject, this._tempCenterQuaternion);
1179
- this._tempCenterPos.applyQuaternion(this._tempCenterQuaternion);
1180
- }
1181
- else {
1182
- this._tempCenterPos.applyQuaternion(collider.gameObject.quaternion);
1183
- }
1170
+ const rot = getWorldQuaternion(collider.gameObject, this._tempCenterQuaternion);
1171
+ this._tempCenterPos.applyQuaternion(rot);
1184
1172
  targetVector.x += this._tempCenterPos.x;
1185
1173
  targetVector.y += this._tempCenterPos.y;
1186
1174
  targetVector.z += this._tempCenterPos.z;
src/engine/engine_physics.ts CHANGED
@@ -1,11 +1,10 @@
1
- import { AxesHelper, Box3, Camera, type Intersection, Layers, Line,Mesh, Object3D, Ray, Raycaster, Sphere, Vector2, Vector3 } from 'three'
2
-
3
- import { Gizmos } from './engine_gizmos.js';
1
+ import { Box3, Camera, type Intersection, Layers, Mesh, Object3D, Ray, Raycaster, Sphere, Vector2, Vector3, AxesHelper, Line } from 'three'
4
2
  import { Context } from './engine_setup.js';
3
+ import { getParam } from "./engine_utils.js"
5
4
  import { getWorldPosition } from "./engine_three_utils.js"
6
5
  import type { Vec2, Vec3, } from './engine_types.js';
7
6
  import type { IPhysicsEngine } from './engine_types.js';
8
- import { getParam } from "./engine_utils.js"
7
+ import { Gizmos } from './engine_gizmos.js';
9
8
 
10
9
  const debugPhysics = getParam("debugphysics");
11
10
  const layerMaskHelper: Layers = new Layers();
@@ -13,7 +12,7 @@
13
12
  export declare type RaycastTestObjectReturnType = void | boolean | "continue in children";
14
13
  export declare type RaycastTestObjectCallback = (obj: Object3D) => RaycastTestObjectReturnType;
15
14
 
16
- export declare interface IRaycastOptions {
15
+ declare interface IRaycastOptions {
17
16
  /** Optionally a custom raycaster can be provided. Other properties will then be set on this raycaster */
18
17
  raycaster?: Raycaster;
19
18
  /** Optional ray that can be used for raycasting
@@ -166,19 +165,17 @@
166
165
  if (obj.type === "Mesh" && obj.layers.test(mask) && !Gizmos.isGizmo(obj)) {
167
166
  const mesh = obj as Mesh;
168
167
  const geo = mesh.geometry;
169
- if (geo) {
170
- if (!geo.boundingBox)
171
- geo.computeBoundingBox();
172
- if (geo.boundingBox) {
173
- if (mesh.matrixWorldNeedsUpdate) mesh.updateMatrixWorld();
174
- const test = this.tempBoundingBox.copy(geo.boundingBox).applyMatrix4(mesh.matrixWorld);
175
- if (sp.intersectsBox(test)) {
176
- const wp = getWorldPosition(obj);
177
- const dist = wp.distanceTo(sp.center);
178
- const int = new SphereIntersection(obj, dist, wp);
179
- results.push(int);
180
- if (!traverseChildsAfterHit) return;
181
- }
168
+ if (!geo.boundingBox)
169
+ geo.computeBoundingBox();
170
+ if (geo.boundingBox) {
171
+ if (mesh.matrixWorldNeedsUpdate) mesh.updateMatrixWorld();
172
+ const test = this.tempBoundingBox.copy(geo.boundingBox).applyMatrix4(mesh.matrixWorld);
173
+ if (sp.intersectsBox(test)) {
174
+ const wp = getWorldPosition(obj);
175
+ const dist = wp.distanceTo(sp.center);
176
+ const int = new SphereIntersection(obj, dist, wp);
177
+ results.push(int);
178
+ if (!traverseChildsAfterHit) return;
182
179
  }
183
180
  }
184
181
  }
@@ -191,7 +188,7 @@
191
188
  }
192
189
  }
193
190
 
194
- public raycastFromRay(ray: Ray, options: IRaycastOptions | null = null): Array<Intersection> {
191
+ public raycastFromRay(ray: Ray, options: IRaycastOptions | RaycastOptions | null = null): Array<Intersection> {
195
192
  const opts = options ?? this.defaultRaycastOptions;
196
193
  opts.ray = ray;
197
194
  const res = this.raycast(opts);
@@ -206,7 +203,7 @@
206
203
  * Use raycastPhysics for raycasting against physic colliders only. Depending on your scenario this might be faster.
207
204
  * @param options raycast options. If null, default options will be used.
208
205
  */
209
- public raycast(options: IRaycastOptions | null = null): Array<Intersection> {
206
+ public raycast(options: IRaycastOptions | RaycastOptions | null = null): Array<Intersection> {
210
207
  if (!options) options = this.defaultRaycastOptions;
211
208
  const mp = options.screenPoint ?? this.context.input.mousePositionRC;
212
209
  const rc = options.raycaster ?? this.raycaster;
@@ -274,10 +271,8 @@
274
271
 
275
272
  private intersect(raycaster: Raycaster, objects: Object3D[], results: Intersection[], options: IRaycastOptions) {
276
273
  for (const obj of objects) {
277
- // dont raycast invisible objects
278
- if (obj.visible === false) continue;
279
-
280
274
  if (Gizmos.isGizmo(obj)) continue;
275
+
281
276
  // dont raycast object if it's a line and the line threshold is < 0
282
277
  if (options.lineThreshold !== undefined && options.lineThreshold < 0) {
283
278
  if (obj instanceof Line) {
src/engine/engine_playerview.ts CHANGED
@@ -1,7 +1,6 @@
1
+ import { getParam } from "./engine_utils.js";
1
2
  import { Object3D } from "three";
2
-
3
3
  import { Context } from "./engine_setup.js";
4
- import { getParam } from "./engine_utils.js";
5
4
 
6
5
  const debug = getParam("debugplayerview");
7
6
 
src/engine/engine_scenelighting.ts CHANGED
@@ -1,11 +1,10 @@
1
- import { EquirectangularReflectionMapping, LightProbe, SphericalHarmonics3, SRGBColorSpace,Texture, Vector4, WebGLCubeRenderTarget } from "three";
2
-
3
- import { AssetReference } from "./engine_addressables.js";
1
+ import { Vector4, EquirectangularReflectionMapping, WebGLCubeRenderTarget, Texture, LightProbe, SphericalHarmonics3, SRGBColorSpace } from "three";
4
2
  import { Context } from "./engine_setup.js";
3
+ import { SceneLightSettings } from "./extensions/NEEDLE_lighting_settings.js";
5
4
  import { createFlatTexture, createTrilightTexture } from "./engine_shaders.js";
5
+ import { getParam } from "./engine_utils.js";
6
6
  import { type SourceIdentifier } from "./engine_types.js";
7
- import { getParam } from "./engine_utils.js";
8
- import { SceneLightSettings } from "./extensions/NEEDLE_lighting_settings.js";
7
+ import { AssetReference } from "./engine_addressables.js";
9
8
  // import { LightProbeGenerator } from "three/examples/jsm/lights/LightProbeGenerator.js"
10
9
 
11
10
  const debug = getParam("debugenvlight");
src/engine/engine_scenetools.ts CHANGED
@@ -1,18 +1,17 @@
1
- import { Object3D } from "three";
1
+ import { Context } from "./engine_setup.js"
2
2
  import { type GLTF, GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
3
-
4
- import { showBalloonMessage } from "./debug/index.js";
5
- import { getLoader, type INeedleGltfLoader, registerLoader } from "./engine_gltf.js";
6
- import { createBuiltinComponents, writeBuiltinComponentData } from "./engine_gltf_builtin_components.js";
7
3
  // import * as object from "./engine_gltf_builtin_components.js";
8
4
  import * as loaders from "./engine_loaders.js"
9
- import { registerPrewarmObject } from "./engine_mainloop_utils.js";
10
- import { SerializationContext } from "./engine_serialization_core.js";
11
- import { Context } from "./engine_setup.js"
12
- import { type SourceIdentifier, type UIDProvider } from "./engine_types.js";
13
5
  import * as utils from "./engine_utils.js";
14
6
  import { registerComponentExtension, registerExtensions } from "./extensions/extensions.js";
7
+ import { getLoader, type INeedleGltfLoader, registerLoader } from "./engine_gltf.js";
8
+ import { type SourceIdentifier, type UIDProvider } from "./engine_types.js";
9
+ import { createBuiltinComponents, writeBuiltinComponentData } from "./engine_gltf_builtin_components.js";
10
+ import { SerializationContext } from "./engine_serialization_core.js";
15
11
  import { NEEDLE_components } from "./extensions/NEEDLE_components.js";
12
+ import { registerPrewarmObject } from "./engine_mainloop_utils.js";
13
+ import { Object3D } from "three";
14
+ import { showBalloonMessage } from "./debug/index.js";
16
15
 
17
16
 
18
17
  export class NeedleGltfLoader implements INeedleGltfLoader {
src/engine/engine_serialization_builtin_serializer.ts CHANGED
@@ -1,15 +1,14 @@
1
1
  import * as THREE from "three";
2
- import { Color, CompressedTexture, Object3D, Texture, WebGLRenderTarget } from "three";
3
-
4
- import { isDevEnvironment, showBalloonMessage, showBalloonWarning } from "../engine/debug/index.js";
2
+ import { RGBAColor } from "../engine-components/js-extensions/RGBAColor.js";
3
+ import { SerializationContext, TypeSerializer } from "./engine_serialization_core.js";
5
4
  import { Behaviour, Component, GameObject } from "../engine-components/Component.js";
5
+ import { debugExtension } from "./engine_default_parameters.js";
6
6
  import { CallInfo, EventList } from "../engine-components/EventList.js";
7
- import { RGBAColor } from "../engine-components/js-extensions/RGBAColor.js";
8
- import { AssetReference } from "./engine_addressables.js";
9
- import { debugExtension } from "./engine_default_parameters.js";
10
- import { SerializationContext, TypeSerializer } from "./engine_serialization_core.js";
7
+ import { Color, CompressedTexture, Object3D, Texture, WebGLRenderTarget } from "three";
11
8
  import { RenderTexture } from "./engine_texture.js";
9
+ import { isDevEnvironment, showBalloonMessage, showBalloonWarning } from "../engine/debug/index.js";
12
10
  import { resolveUrl } from "./engine_utils.js";
11
+ import { AssetReference } from "./engine_addressables.js";
13
12
 
14
13
  // export class SourcePath {
15
14
  // src?:string
src/engine/engine_serialization_core.ts CHANGED
@@ -1,14 +1,13 @@
1
+ import { type GLTF } from "three/examples/jsm/loaders/GLTFLoader.js";
2
+ import { getParam } from "./engine_utils.js";
1
3
  import { AnimationClip, Material, Mesh, Object3D, Texture } from "three";
2
- import { type GLTF } from "three/examples/jsm/loaders/GLTFLoader.js";
3
-
4
+ import { Context } from "./engine_setup.js";
5
+ import { isPersistentAsset } from "./extensions/NEEDLE_persistent_assets.js";
6
+ import { type ConstructorConcrete, type SourceIdentifier } from "./engine_types.js";
4
7
  import { debugExtension } from "../engine/engine_default_parameters.js";
5
- import { addLog,LogType } from "./debug/debug_overlay.js";
8
+ import { LogType, addLog } from "./debug/debug_overlay.js";
6
9
  import { isLocalNetwork } from "./engine_networking_utils.js";
7
- import { Context } from "./engine_setup.js";
8
- import { Constructor, type ConstructorConcrete, type SourceIdentifier } from "./engine_types.js";
9
10
  import { $BuiltInTypeFlag } from "./engine_typestore.js";
10
- import { getParam } from "./engine_utils.js";
11
- import { isPersistentAsset } from "./extensions/NEEDLE_persistent_assets.js";
12
11
 
13
12
  const debug = getParam("debugserializer");
14
13
 
@@ -125,7 +124,7 @@
125
124
  // }
126
125
  // }
127
126
 
128
- constructor(type: Constructor<any> | Constructor<any>[]) {
127
+ constructor(type: ConstructorConcrete<any> | ConstructorConcrete<any>[]) {
129
128
  if (Array.isArray(type)) {
130
129
  for (const key of type)
131
130
  helper.register(key.name, this);
src/engine/engine_serialization.ts CHANGED
@@ -1,6 +1,7 @@
1
- import { deserializeObject,serializeObject } from "./engine_serialization_core.js";
1
+ import { serializeObject, deserializeObject } from "./engine_serialization_core.js";
2
2
 
3
- export { deserializeObject,serializeObject };
3
+ export { serializeObject, deserializeObject };
4
4
 
5
- export * from "./engine_serialization_builtin_serializer.js";
6
- export { serializable, serializeable } from "./engine_serialization_decorator.js"
5
+ export { serializable, serializeable } from "./engine_serialization_decorator.js"
6
+
7
+ export * from "./engine_serialization_builtin_serializer.js";
src/engine/engine_shaders.ts CHANGED
@@ -1,10 +1,9 @@
1
1
 
2
- import { Color,DataTexture, FileLoader, RGBAFormat, Vector4 } from "three";
3
-
2
+ import * as loader from "./engine_fileloader.js"
3
+ import * as SHADERDATA from "./shaders/shaderData.js"
4
+ import { Vector4, FileLoader, DataTexture, RGBAFormat, Color } from "three";
4
5
  import { RGBAColor } from "../engine-components/js-extensions/RGBAColor.js";
5
- import * as loader from "./engine_fileloader.js"
6
6
  import { Mathf } from "./engine_math.js";
7
- import * as SHADERDATA from "./shaders/shaderData.js"
8
7
 
9
8
 
10
9
  const white = new Uint8Array(4);
src/engine/engine_texture.ts CHANGED
@@ -1,6 +1,5 @@
1
+ import { Camera, Mesh, Object3D, Texture, WebGLRenderer, WebGLRenderTarget } from "three";
1
2
  import { EffectComposer } from "postprocessing";
2
- import { Camera, Mesh, Object3D, Texture, WebGLRenderer, WebGLRenderTarget } from "three";
3
-
4
3
  import { findResourceUsers } from "./engine_assetdatabase.js";
5
4
 
6
5
 
src/engine/engine_three_utils.ts CHANGED
@@ -1,7 +1,6 @@
1
- import { AnimationAction, Euler, Mesh,Object3D, PerspectiveCamera, PlaneGeometry, Quaternion, Scene, Texture, Uniform, Vector3 } from "three";
2
- import { ShaderMaterial,WebGLRenderer } from "three";
3
-
4
1
  import { Mathf } from "./engine_math.js"
2
+ import { Vector3, Quaternion, Uniform, Texture, AnimationAction, PerspectiveCamera, Object3D, Euler, PlaneGeometry, Scene, Mesh } from "three";
3
+ import { WebGLRenderer, ShaderMaterial } from "three";
5
4
  import { CircularBuffer } from "./engine_utils.js";
6
5
 
7
6
 
@@ -48,24 +47,11 @@
48
47
 
49
48
 
50
49
  const _tempVecs = new CircularBuffer(() => new Vector3(), 100);
51
- export function getTempVector(vecOrX?: Vector3 | number | DOMPointReadOnly, y?: number, z?: number) {
50
+ export function getTempVector(value?: Vector3) {
52
51
  const vec = _tempVecs.get();
53
- if (vecOrX instanceof Vector3) vec.copy(vecOrX);
54
- else if (vecOrX instanceof DOMPointReadOnly) vec.set(vecOrX.x, vecOrX.y, vecOrX.z);
55
- else {
56
- if (typeof vecOrX === "number") vec.x = vecOrX;
57
- if (typeof y === "number") vec.y = y;
58
- if (typeof z === "number") vec.z = z;
59
- }
52
+ if(value instanceof Vector3) vec.copy(value);
60
53
  return vec;
61
54
  }
62
- const _tempQuats = new CircularBuffer(() => new Quaternion(), 100);
63
- export function getTempQuaternion(value?: Quaternion | DOMPointReadOnly) {
64
- const val = _tempQuats.get();
65
- if (value instanceof Quaternion) val.copy(value);
66
- else if (value instanceof DOMPointReadOnly) val.set(value.x, value.y, value.z, value.w);
67
- return val;
68
- }
69
55
 
70
56
 
71
57
  const _worldPositions = new CircularBuffer(() => new Vector3(), 100);
src/engine/engine_time.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  import { Clock } from 'three'
2
-
2
+ import { getParam } from './engine_utils.js';
3
3
  import { type ITime } from './engine_types.js';
4
- import { getParam } from './engine_utils.js';
5
4
 
6
5
  const timescaleUrl = getParam("timescale");
7
6
  let timeScale = 1;
src/engine/engine_types.ts CHANGED
@@ -1,12 +1,10 @@
1
- import type { Camera, Color, Material, Mesh, Object3D, Quaternion, Ray, Scene, WebGLRenderer } from "three";
1
+ import { RenderTexture } from "./engine_texture.js";
2
+ import type { Camera, Color, Material, Object3D, Quaternion, Ray, Scene, WebGLRenderer, Mesh } from "three";
2
3
  import { Vector3 } from "three";
3
- import { type GLTF as GLTF3 } from "three/examples/jsm/loaders/GLTFLoader.js";
4
-
5
4
  import { RGBAColor } from "../engine-components/js-extensions/RGBAColor.js";
6
5
  import { CollisionDetectionMode, type PhysicsMaterial, RigidbodyConstraints } from "./engine_physics.types.js";
7
- import { RenderTexture } from "./engine_texture.js";
8
6
  import { CircularBuffer } from "./engine_utils.js";
9
- import type { INeedleXRSessionEventReceiver, NeedleXRSession } from "./engine_xr.js";
7
+ import { type GLTF as GLTF3 } from "three/examples/jsm/loaders/GLTFLoader.js";
10
8
 
11
9
  export type GLTF = GLTF3 & {
12
10
  // asset: { generator: string, version: string }
@@ -74,14 +72,13 @@
74
72
 
75
73
  scripts: IComponent[];
76
74
  scripts_pausedChanged: IComponent[];
75
+ // scripts with update event
77
76
  scripts_earlyUpdate: IComponent[];
78
77
  scripts_update: IComponent[];
79
78
  scripts_lateUpdate: IComponent[];
80
79
  scripts_onBeforeRender: IComponent[];
81
80
  scripts_onAfterRender: IComponent[];
82
81
  scripts_WithCorroutines: IComponent[];
83
- scripts_immersive_vr: INeedleXRSessionEventReceiver[];
84
- scripts_immersive_ar: INeedleXRSessionEventReceiver[];
85
82
  coroutines: { [FrameEvent: number]: Array<CoroutineData> };
86
83
 
87
84
  post_setup_callbacks: Function[];
@@ -93,13 +90,10 @@
93
90
  new_script_start: IComponent[];
94
91
  new_scripts_pre_setup_callbacks: Function[];
95
92
  new_scripts_post_setup_callbacks: Function[];
96
- new_scripts_xr: INeedleXRSessionEventReceiver[];
97
93
 
98
94
  stopAllCoroutinesFrom(script: IComponent);
99
95
  }
100
96
 
101
- export type INeedleXRSession = NeedleXRSession;
102
-
103
97
  export declare interface INeedleEngineComponent extends HTMLElement {
104
98
  getAROverlayContainer(): HTMLElement;
105
99
  onEnterAR(session: XRSession, overlayContainer: HTMLElement);
@@ -513,18 +507,3 @@
513
507
  /** Enable to visualize raycasts in the scene with gizmos */
514
508
  debugRenderRaycasts: boolean;
515
509
  }
516
-
517
-
518
- /** Typical mouse button names for most devices */
519
- export type MouseButtonName = "left" | "right" | "middle";
520
-
521
- /** Button names on typical controllers (since there seems to be no agreed naming)
522
- * https://w3c.github.io/gamepad/#remapping
523
- */
524
- export type GamepadButtonName = "a-button" | "b-button" | "x-button" | "y-button";
525
- /** Button names as used in the xr profile */
526
-
527
- export type XRControllerButtonName = "thumbrest" | "xr-standard-trigger" | "xr-standard-squeeze" | "xr-standard-thumbstick" | "xr-standard-touchpad" | "menu" | GamepadButtonName;
528
-
529
- /** All known (types) button names for various devices and cases combined. You should use the device specific names if you e.g. know you only deal with a mouse use MouseButtonName */
530
- export type ButtonName = "unknown" | MouseButtonName | GamepadButtonName | XRControllerButtonName;
src/engine/engine_typestore.ts CHANGED
@@ -7,6 +7,10 @@
7
7
 
8
8
  private _types = {};
9
9
 
10
+ constructor() {
11
+ if (debug) console.warn("TypeStore: Created", this);
12
+ }
13
+
10
14
  public add(key, type) {
11
15
  if (debug) console.warn("ADD TYPE", key);
12
16
  const existing = this._types[key];
src/engine/engine_util_decorator.ts CHANGED
@@ -1,8 +1,7 @@
1
- import { Quaternion, Vector2, Vector3, Vector4 } from "three";
2
-
3
- import { isDevEnvironment, LogType, showBalloonMessage } from "./debug/index.js";
4
1
  import { $isAssigningProperties } from "./engine_serialization_core.js";
2
+ import { LogType, isDevEnvironment, showBalloonMessage } from "./debug/index.js";
5
3
  import { type Constructor, type IComponent } from "./engine_types.js";
4
+ import { Quaternion, Vector2, Vector3, Vector4 } from "three";
6
5
  import { watchWrite } from "./engine_utils.js";
7
6
 
8
7
 
src/engine/engine_utils_screenshot.ts CHANGED
@@ -1,7 +1,6 @@
1
- import { Camera,PerspectiveCamera } from "three";
2
-
3
1
  import { ContextRegistry } from "./engine_context_registry.js";
4
2
  import { Context } from "./engine_setup.js";
3
+ import { PerspectiveCamera, Camera } from "three";
5
4
 
6
5
  declare type ImageMimeType = "image/webp" | "image/png";
7
6
 
src/engine/engine_utils.ts CHANGED
@@ -1,8 +1,5 @@
1
1
  // use for typesafe interface method calls
2
2
  import { Quaternion, type Vector, Vector2, Vector3, Vector4 } from "three";
3
-
4
- import { type Context } from "./engine_context.js";
5
- import { ContextRegistry } from "./engine_context_registry.js";
6
3
  import { type SourceIdentifier } from "./engine_types.js";
7
4
 
8
5
  // https://schneidenbach.gitbooks.io/typescript-cookbook/content/nameof-operator.html
@@ -11,8 +8,6 @@
11
8
  return nameofFactory<T>()(name);
12
9
  }
13
10
 
14
- type ParseNumber<T> = T extends `${infer U extends number}` ? U : never;
15
- export type EnumToPrimitiveUnion<T> = `${T & string}` | ParseNumber<`${T & number}`>;
16
11
 
17
12
  export function isDebugMode(): boolean {
18
13
  return getParam("debug") ? true : false;
@@ -212,37 +207,12 @@
212
207
  return obj;
213
208
  }
214
209
 
215
- /** @returns a promise that resolves after a certain amount of milliseconds
216
- * e.g. `await delay(1000)` will wait for 1 second
217
- */
218
210
  export function delay(milliseconds: number): Promise<void> {
219
- return new Promise((resolve, _reject) => {
220
- setTimeout(resolve, milliseconds);
211
+ return new Promise((res, _) => {
212
+ setTimeout(res, milliseconds);
221
213
  });
222
214
  }
223
215
 
224
- /** @returns a promise that resolves after a certain amount of frames
225
- * e.g. `await delayForFrames(10)` will wait for 10 frames to pass
226
- */
227
- export function delayForFrames(frameCount: number, context?: Context): Promise<void> {
228
-
229
- if (frameCount <= 0) return Promise.resolve();
230
- if (!context) context = ContextRegistry.Current as Context;
231
- if (!context) return Promise.reject("No context");
232
-
233
- const endFrame = context.time.frameCount + frameCount;
234
- return new Promise((resolve, reject) => {
235
- if (!context) return reject("No context");
236
- const cb = () => {
237
- if (context!.time.frameCount >= endFrame) {
238
- context!.pre_update_callbacks.splice(context!.pre_update_callbacks.indexOf(cb), 1);
239
- resolve();
240
- }
241
- }
242
- context!.pre_update_callbacks.push(cb);
243
- });
244
- }
245
-
246
216
  // 1) if a timeline is exported via menu item the audio clip path is relative to the glb (same folder)
247
217
  // we need to detect that here and build the new audio source path relative to the new glb location
248
218
  // the same is/might be true for any file that is/will be exported via menu item
@@ -546,6 +516,10 @@
546
516
  return json;
547
517
  }
548
518
 
519
+
520
+
521
+
522
+
549
523
  declare type AttributeChangeCallback = (value: string | null) => void;
550
524
  declare type HtmlElementExtra = {
551
525
  observer: MutationObserver,
@@ -637,43 +611,4 @@
637
611
  anyFailed: anyFailed,
638
612
  results: res,
639
613
  };
640
- }
641
-
642
-
643
-
644
-
645
-
646
-
647
- /** using https://github.com/davidshimjs/qrcodejs */
648
- export async function generateQRCode(args: { domElement?: HTMLElement, text: string, width?: number, height?: number, colorDark?: string, colorLight?: string, correctLevel?: any }): Promise<HTMLElement> {
649
-
650
- // ensure that the QRCode library is loaded
651
- if (!globalThis["QRCode"]) {
652
- const url = "https://cdn.rawgit.com/davidshimjs/qrcodejs/gh-pages/qrcode.min.js";
653
- let script = document.head.querySelector(`script[src="${url}"]`) as HTMLScriptElement;
654
- if (!script) {
655
- script = document.createElement("script");
656
- script.src = url;
657
- document.head.appendChild(script);
658
- }
659
-
660
- await new Promise((resolve, _reject) => {
661
- script.addEventListener("load", () => {
662
- resolve(true);
663
- });
664
- });
665
- }
666
-
667
- const QRCODE = globalThis["QRCode"];
668
- const target = args.domElement ?? document.createElement("div");
669
- new QRCODE(target, {
670
- width: args.width ?? 256,
671
- height: args.height ?? 256,
672
- colorDark: "#000000",
673
- colorLight: "#ffffff",
674
- correctLevel: QRCODE.CorrectLevel.M,
675
- ...args,
676
- });
677
- console.log("QRCode generated for " + args.text);
678
- return target;
679
614
  }
src/engine/engine_xr.ts DELETED
@@ -1,2 +0,0 @@
1
-
2
- export * from "./xr/index.js"
src/engine/engine.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import "./engine_hot_reload.js";
2
+
3
+ import * as engine_setup from "./engine_setup.js";
4
+ import * as engine_scenetools from "./engine_scenetools.js";
2
5
  import "./tests/test_utils.js";
3
-
4
6
  import { RGBAColor } from "../engine-components/js-extensions/RGBAColor.js";
5
- import * as engine_scenetools from "./engine_scenetools.js";
6
- import * as engine_setup from "./engine_setup.js";
7
7
 
8
8
  const engine : any = {
9
9
  ...engine_setup,
src/engine-components/ui/EventSystem.ts CHANGED
@@ -1,18 +1,19 @@
1
- import { Intersection, Object3D } from "three";
2
-
3
- import { isDevEnvironment, showBalloonMessage } from "../../engine/debug/index.js";
4
- import { Input, InputEventNames, InputEvents, NEPointerEvent, PointerType } from "../../engine/engine_input.js";
5
- import { Mathf } from "../../engine/engine_math.js";
6
1
  import { RaycastOptions, RaycastTestObjectReturnType } from "../../engine/engine_physics.js";
2
+ import { Behaviour, Component, GameObject } from "../Component.js";
3
+ import { WebXR } from "../webxr/WebXR.js";
4
+ import { ControllerEvents, WebXRController } from "../webxr/WebXRController.js";
5
+ import * as ThreeMeshUI from 'three-mesh-ui'
7
6
  import { Context } from "../../engine/engine_setup.js";
8
- import { IComponent } from "../../engine/engine_types.js";
7
+ import { type IPointerEventHandler, PointerEventData, hasPointerEventComponent } from "./PointerEvents.js";
8
+ import { ObjectRaycaster, Raycaster } from "./Raycaster.js";
9
+ import { InputEvents, NEPointerEvent, PointerType } from "../../engine/engine_input.js";
10
+ import { Mesh, Object3D } from "three";
11
+ import type { ICanvasGroup } from "./Interfaces.js";
9
12
  import { getParam } from "../../engine/engine_utils.js";
10
- import { Behaviour, GameObject } from "../Component.js";
13
+ import { UIRaycastUtils } from "./RaycastUtils.js";
11
14
  import { $shadowDomOwner } from "./BaseUIComponent.js";
12
- import type { ICanvasGroup } from "./Interfaces.js";
13
- import { hasPointerEventComponent, type IPointerEventHandler, IPointerUpHandler, PointerEventData } from "./PointerEvents.js";
14
- import { ObjectRaycaster, Raycaster } from "./Raycaster.js";
15
- import { UIRaycastUtils } from "./RaycastUtils.js";
15
+ import { isDevEnvironment, showBalloonMessage } from "../../engine/debug/index.js";
16
+ import { Mathf } from "../../engine/engine_math.js";
16
17
  import { isUIObject } from "./Utils.js";
17
18
 
18
19
  const debug = getParam("debugeventsystem");
@@ -92,9 +93,10 @@
92
93
  const res = GameObject.findObjectOfType(Raycaster, this.context);
93
94
  if (!res) {
94
95
  const rc = GameObject.addNewComponent(this.context.scene, ObjectRaycaster);
96
+ rc.ignoreSkinnedMeshes = true;
95
97
  this.raycaster.push(rc);
96
98
  if (isDevEnvironment() || debug)
97
- console.warn("Added an ObjectRaycaster to the scene because no raycaster was found.");
99
+ console.warn("Added an ObjectRaycaster to the scene because no raycaster was found. Skinnedmeshes will be ignored for better performance");
98
100
  }
99
101
  }
100
102
  }
@@ -110,16 +112,89 @@
110
112
  }
111
113
  }
112
114
 
115
+ private _selectStartFn?: any;
116
+ private _selectEndFn?: any;
117
+ private _selectUpdateFn?: any;
118
+ private _handleEventCycleFn?: any;
113
119
  private _handleInputFn?: any;
114
120
 
115
121
  onEnable(): void {
116
- this._handleInputFn ??= this.onPointerEvent.bind(this);
122
+ const grabbed: Map<any, Object3D | null> = new Map();
123
+ this._selectStartFn ??= (ctrl, args: { grab: THREE.Object3D | null }) => {
124
+ if (!args.grab) return;
125
+ MeshUIHelper.resetLastSelected();
126
+ const opts = new PointerEventData(this.context.input);
127
+ opts.inputSource = ctrl;
128
+ opts.pointerId = 0;
129
+ opts.isDown = ctrl.selectionDown;
130
+ opts.isUp = ctrl.selectionUp;
131
+ opts.isPressed = ctrl.selectionPressed;
132
+ opts.isClicked = false;
133
+ grabbed.set(ctrl, args.grab);
134
+ if (args.grab && !this.handleEventOnObject(args.grab, opts)) {
135
+ args.grab = null;
136
+ };
137
+ }
138
+ this._selectEndFn ??= (ctrl: WebXRController, args: { grab: THREE.Object3D }) => {
139
+ if (!args.grab) return;
140
+ const opts = new PointerEventData(this.context.input);
141
+ opts.inputSource = ctrl;
142
+ opts.pointerId = 0;
143
+ opts.isDown = ctrl.selectionDown;
144
+ opts.isUp = ctrl.selectionUp;
145
+ opts.isPressed = ctrl.selectionPressed;
146
+ opts.isClicked = ctrl.selectionClick;
147
+ this.handleEventOnObject(args.grab, opts);
148
+
149
+ const prevGrabbed = grabbed.get(ctrl);
150
+ grabbed.set(ctrl, null);
151
+ if (prevGrabbed) {
152
+
153
+ for (const key of this.pressedByID.keys()) {
154
+ const e = this.pressedByID[key] as {
155
+ obj: Object3D<Event>;
156
+ data: PointerEventData;
157
+ handler: IPointerEventHandler;
158
+ };
159
+
160
+ if (e && e.obj === prevGrabbed && e.handler) {
161
+ e.handler.onPointerUp?.call(e.handler, opts);
162
+ this.pressedByID.delete(key);
163
+ }
164
+ }
165
+ }
166
+ };
167
+
168
+ const controllerRcOpts = new RaycastOptions();
169
+ this._selectUpdateFn ??= (_ctrl: WebXRController) => {
170
+ controllerRcOpts.ray = _ctrl.getRay();
171
+ const rc = this.performRaycast(controllerRcOpts) ?? [];
172
+ const opts = new PointerEventData(this.context.input);
173
+ opts.inputSource = _ctrl;
174
+ opts.pointerId = _ctrl.input?.handedness === "right" ? 0 : 1;
175
+ opts.isDown = _ctrl.selectionDown;
176
+ opts.isUp = _ctrl.selectionUp;
177
+ opts.isPressed = _ctrl.selectionPressed;
178
+ opts.isClicked = false;
179
+ this.handleIntersections(opts.pointerId, rc, opts);
180
+ };
181
+
182
+ WebXRController.addEventListener(ControllerEvents.SelectStart, this._selectStartFn);
183
+ WebXRController.addEventListener(ControllerEvents.SelectEnd, this._selectEndFn);
184
+ WebXRController.addEventListener(ControllerEvents.Update, this._selectUpdateFn);
185
+
186
+ this._handleInputFn = this.onPointerEvent.bind(this);
187
+
117
188
  this.context.input.addEventListener(InputEvents.PointerDown, this._handleInputFn);
118
189
  this.context.input.addEventListener(InputEvents.PointerUp, this._handleInputFn);
119
190
  this.context.input.addEventListener(InputEvents.PointerMove, this._handleInputFn);
120
191
  }
121
192
 
122
193
  onDisable(): void {
194
+ WebXRController.removeEventListener(ControllerEvents.SelectStart, this._selectStartFn);
195
+ WebXRController.removeEventListener(ControllerEvents.SelectEnd, this._selectEndFn);
196
+ WebXRController.removeEventListener(ControllerEvents.Update, this._selectUpdateFn);
197
+
123
198
  this.context.input.removeEventListener(InputEvents.PointerDown, this._handleInputFn);
124
199
  this.context.input.removeEventListener(InputEvents.PointerUp, this._handleInputFn);
125
200
  this.context.input.removeEventListener(InputEvents.PointerMove, this._handleInputFn);
@@ -149,39 +224,30 @@
149
224
  */
150
225
  private onPointerEvent(pointerEvent: NEPointerEvent) {
151
226
  if (pointerEvent === undefined) return;
152
- if (pointerEvent.propagationStopped) return;
153
227
 
154
- // Use the pointerID, this is the touch id or the mouse (always 0, could be index of the mouse DEVICE) or the index of the XRInputSource
228
+ // On mouse input has to be always 0 regardless of the button user pressed
229
+ // because otherwise it would be taken as 3 unique pointers and create OnEnter and OnExit events which is not expected
230
+ const id = pointerEvent.pointerType == PointerType.Touch ? pointerEvent.button : 0;
155
231
  const data = new PointerEventData(this.context.input, pointerEvent);
156
- this._currentPointerEventName = pointerEvent.type;
157
232
 
158
233
  data.inputSource = this.context.input;
159
- data.isClick = pointerEvent.isClick;
160
- data.isDoubleClick = pointerEvent.isDoubleClick;
234
+ data.pointerId = pointerEvent.button;
235
+ data.isClicked = pointerEvent.type == InputEvents.PointerUp && this.context.input.getPointerClicked(pointerEvent.button)
161
236
  // using the input type directly instead of input API -> otherwise onMove events can sometimes be getPointerUp() == true
162
237
  data.isDown = pointerEvent.type == InputEvents.PointerDown;
163
238
  data.isUp = pointerEvent.type == InputEvents.PointerUp;
164
- data.isPressed = this.context.input.getPointerPressed(pointerEvent.pointerId);
239
+ data.isPressed = this.context.input.getPointerPressed(pointerEvent.button);
165
240
 
166
- if (debug) {
167
- if (data.isDown) console.log("DOWN", data.pointerId);
168
- else if (data.isUp) console.log("UP", data.pointerId);
169
- if (data.isClick) console.log("CLICK", data.pointerId);
170
- }
241
+ if (debug && data.isClicked) console.log("CLICK", data.pointerId);
171
242
 
172
243
  // raycast
173
244
  const options = new RaycastOptions();
174
- if (pointerEvent.ray) {
175
- options.ray = pointerEvent.ray;
176
- }
177
- else {
178
- options.screenPoint = this.context.input.getPointerPositionRC(pointerEvent.pointerId)!;
179
- }
245
+ options.screenPoint = this.context.input.getPointerPositionRC(id)!;
180
246
 
181
-
182
247
  const hits = this.performRaycast(options);
248
+ if (!hits) return;
183
249
 
184
- if (debug && data.isClick) {
250
+ if (debug && data.isClicked) {
185
251
  showBalloonMessage("EventSystem: " + data.pointerId + " - " + this.context.time.frame + " - Up:" + data.isUp + ", Down:" + data.isDown)
186
252
  }
187
253
 
@@ -191,12 +257,12 @@
191
257
  hasActiveUI: this.currentActiveMeshUIComponents.length > 0,
192
258
  }
193
259
 
194
- this.dispatchEvent(new CustomEvent(EventSystemEvents.BeforeHandleInput, { detail: evt }));
260
+ this.dispatchEvent(new CustomEvent(EventSystemEvents.BeforeHandleInput, { detail: evt }))
195
261
 
196
- // then handle the intersections and call the callbacks on the regular objects
197
- this.handleIntersections(hits, data);
262
+ // handle hit objects
263
+ this.handleIntersections(id, hits, data)
198
264
 
199
- this.dispatchEvent(new CustomEvent<AfterHandleInputEvent>(EventSystemEvents.AfterHandleInput, { detail: evt }));
265
+ this.dispatchEvent(new CustomEvent<AfterHandleInputEvent>(EventSystemEvents.AfterHandleInput, { detail: evt }))
200
266
  }
201
267
 
202
268
  private readonly _sortedHits: THREE.Intersection[] = [];
@@ -205,10 +271,6 @@
205
271
  * cache for objects that we want to raycast against. It's cleared before each call to performRaycast invoking raycasters
206
272
  */
207
273
  private readonly _testObjectsCache = new Map<Object3D, boolean>();
208
- /** that's the raycaster that is CURRENTLY being used for raycasting (the shouldRaycastObject method uses this) */
209
- private _currentlyActiveRaycaster: Raycaster | null = null;
210
- private _currentPointerEventName: InputEventNames | null = null;
211
-
212
274
  /**
213
275
  * Checks if an object that we encounter has an event component and if it does, we add it to our objects cache
214
276
  * If it doesnt we tell our raycasting system to ignore it and continue in the child hierarchy
@@ -221,72 +283,57 @@
221
283
  * */
222
284
  private shouldRaycastObject = (obj: Object3D): RaycastTestObjectReturnType => {
223
285
  // check if this object is actually a UI shadow hierarchy object
224
- let uiOwner: Object3D | null = null;
286
+ let shadowComponent: Object3D | null = null;
225
287
  const isUI = isUIObject(obj);
226
288
  // if yes we want to grab the actual object that is the owner of the shadow dom
227
289
  // and check that object for the event component
228
290
  if (isUI) {
229
- uiOwner = obj[$shadowDomOwner]?.gameObject;
291
+ shadowComponent = obj[$shadowDomOwner]?.gameObject;
230
292
  }
231
293
 
232
294
  // check if the object was seen previously
233
- if (this._testObjectsCache.has(obj) || (uiOwner && this._testObjectsCache.has(uiOwner))) {
295
+ if (this._testObjectsCache.has(obj) || (shadowComponent && this._testObjectsCache.has(shadowComponent))) {
234
296
  // if yes we check if it was previously stored as "YES WE NEED TO RAYCAST THIS"
235
297
  const prev = this._testObjectsCache.get(obj)!;
236
298
  if (prev === false) return "continue in children"
237
299
  return true;
238
300
  }
239
301
  else {
240
-
241
- // if the object has another raycaster component than the one that is currently raycasting, we ignore this here
242
- // because then this other raycaster is responsible for raycasting this object
243
- // const rc = GameObject.getComponent(obj, Raycaster);
244
- // if (rc?.activeAndEnabled && rc !== this._currentlyActiveRaycaster) return false;
245
-
246
302
  // the object was not yet seen so we test if it has an event component
247
- let hasEventComponent = hasPointerEventComponent(obj, this._currentPointerEventName);
248
- if (!hasEventComponent && uiOwner) hasEventComponent = hasPointerEventComponent(uiOwner, this._currentPointerEventName);
303
+ let hasEventComponent = hasPointerEventComponent(obj);
304
+ if (!hasEventComponent && shadowComponent) hasEventComponent = hasPointerEventComponent(shadowComponent);
249
305
 
250
306
  if (hasEventComponent) {
251
307
  // it has an event component: we add it and all its children to the cache
252
308
  // we don't need to do the same for the shadow component hierarchy
253
309
  // because the next object that will be detecting that the shadow owner was already seen
254
310
  this._testObjectsCache.set(obj, true);
255
- for (const ch of obj.children) this.shouldRaycastObject_AddToYesCache(ch);
311
+ obj.traverse((o) => {
312
+ this._testObjectsCache.set(o, true);
313
+ })
256
314
  return true;
257
315
  }
258
316
  this._testObjectsCache.set(obj, false);
259
317
  return "continue in children"
260
318
  }
261
319
  }
262
- private shouldRaycastObject_AddToYesCache(obj: Object3D) {
263
- // if the object has another raycaster component than the one that is currently raycasting, we ignore this here
264
- // because then this other raycaster is responsible for raycasting this object
265
- // const rc = GameObject.getComponent(obj, Raycaster);
266
- // if (rc?.activeAndEnabled && rc !== this._currentlyActiveRaycaster) return;
267
320
 
268
- this._testObjectsCache.set(obj, true);
269
- for (const ch of obj.children) {
270
- this.shouldRaycastObject_AddToYesCache(ch);
271
- }
272
- }
273
-
274
321
  /** the raycast filter is always overriden */
275
322
  private performRaycast(opts: RaycastOptions | null): THREE.Intersection[] | null {
276
323
  if (!this.raycaster) return null;
277
- // we clear the cache of previously seen objects
278
- this._testObjectsCache.clear();
324
+
279
325
  this._sortedHits.length = 0;
280
326
 
281
327
  if (!opts) opts = new RaycastOptions();
328
+
329
+ // we clear the cache of previously seen objects
330
+ this._testObjectsCache.clear();
282
331
  opts.testObject = this.shouldRaycastObject;
283
332
 
284
333
  for (const rc of this.raycaster) {
285
334
  if (!rc.activeAndEnabled) continue;
286
335
 
287
- this._currentlyActiveRaycaster = rc;
288
336
  const res = rc.performRaycast(opts);
289
- this._currentlyActiveRaycaster = null;
290
337
 
291
338
  if (res && res.length > 0) {
292
339
  // console.log(res.length, res.map(r => r.object.name));
@@ -299,55 +346,36 @@
299
346
  return this._sortedHits;
300
347
  }
301
348
 
302
- private assignHitInformation(args: PointerEventData, hit?: Intersection) {
303
- if (!hit) {
304
- args.point = undefined;
305
- args.normal = undefined;
306
- args.face = undefined;
307
- args.distance = undefined;
308
- args.instanceId = undefined;
309
- }
310
- else {
311
- args.point = hit.point;
312
- args.normal = hit.normal;
313
- args.face = hit.face;
314
- args.distance = hit.distance;
315
- args.instanceId = hit.instanceId;
316
- }
317
- }
318
-
319
- private handleIntersections(hits: Intersection[] | null | undefined, args: PointerEventData): boolean {
320
-
349
+ private handleIntersections(id:number, hits: THREE.Intersection[], args: PointerEventData): boolean {
321
350
  if (hits?.length) {
322
351
  hits = this.sortCandidates(hits);
323
352
  for (const hit of hits) {
324
- if (args.event.immediatePropagationStopped) {
325
- return false;
326
- }
327
- this.assignHitInformation(args, hit);
328
- if (this.handleEventOnObject(hit.object, args)) {
353
+ const { object } = hit;
354
+ args.point = hit.point;
355
+ args.normal = hit.normal;
356
+ args.face = hit.face;
357
+ args.distance = hit.distance;
358
+ args.instanceId = hit.instanceId;
359
+ if (this.handleEventOnObject(object, args)) {
329
360
  return true;
330
361
  }
331
362
  }
332
363
  }
333
364
 
334
- // first invoke captured pointers
335
- this.assignHitInformation(args, hits?.[0]);
336
- this.invokePointerCapture(args);
337
-
338
365
  // pointer has not hit any object to handle
339
366
 
340
367
  // thus is not hovering over anything
341
- const hoveredData = this.hoveredByID.get(args.pointerId);
368
+ const hoveredData = this.hoveredByID.get(id);
342
369
  if (hoveredData) {
343
- this.triggerOnExit(hoveredData.obj, hoveredData.data, null);
370
+ this.triggerOnExit(hoveredData.obj, hoveredData.data);
344
371
  }
345
- this.hoveredByID.delete(args.pointerId);
372
+ this.hoveredByID.delete(id);
346
373
 
347
374
  // if it was up, it means it doesn't should notify things that it down on before
348
375
  if (args.isUp) {
349
- this.pressedByID.get(args.pointerId)?.handlers.forEach(h => this.invokeOnPointerUp(args, h));
350
- this.pressedByID.delete(args.pointerId);
376
+ const pressedData = this.pressedByID.get(id);
377
+ pressedData?.handlers.forEach(h => h.onPointerUp?.call(h, args));
378
+ this.pressedByID.delete(id);
351
379
  }
352
380
 
353
381
  return false;
@@ -388,29 +416,34 @@
388
416
  private handleEventOnObject(object: THREE.Object3D, args: PointerEventData): boolean {
389
417
  // ensures that invisible objects are ignored
390
418
  if (!this.testIsVisible(object)) {
391
- if (args.isClick && debug)
419
+ if (args.isClicked && debug)
392
420
  console.log("not allowed", object);
393
421
  return false;
394
422
  }
395
423
 
396
424
  // Event without pointer can't be handled
397
425
  if (args.pointerId === undefined) {
398
- if (debug) console.error("Event without pointer can't be handled", args);
426
+ if(debug) console.warn("Event without pointer can't be handled", args);
399
427
  return false;
400
428
  }
401
429
 
430
+ // We want to call all event methods even if the event was used
431
+ // Used event can't be handled
432
+ // if (args.used) return false;
433
+
402
434
  // Correct the handled object to match the relevant object in shadow dom (?)
435
+ const originalObject = object;
403
436
  args.object = object;
404
437
 
405
438
  const parent = object.parent as any;
406
439
  let isShadow = false;
407
- const clicked = args.isClick ?? false;
440
+ const clicked = args.isClicked ?? false;
408
441
 
409
442
  let canvasGroup: ICanvasGroup | null = null;
410
443
 
411
444
  // handle potential shadow dom built from three mesh ui
412
445
  if (parent && parent.isUI) {
413
- const pressedOrClicked = (args.isPressed || args.isClick) ?? false;
446
+ const pressedOrClicked = (args.isPressed || args.isClicked) ?? false;
414
447
  if (parent[$shadowDomOwner]) {
415
448
  const actualGo = parent[$shadowDomOwner].gameObject;
416
449
  if (actualGo) {
@@ -439,12 +472,11 @@
439
472
  // Handle OnPointerExit -> in case when we are about to hover something new
440
473
  // TODO: we need to keep track of the components that already received a PointerEnterEvent -> we can have a hierarchy where the hovered object changes but the component is on the parent and should not receive a PointerExit event because it's still hovered (just another child object)
441
474
  const hovering = this.hoveredByID.get(args.pointerId);
442
- const prevHovering = hovering?.obj;
443
- const isNewlyHovering = prevHovering !== object;
475
+ const isNewlyHovering = hovering?.obj !== object;
444
476
 
445
477
  // trigger onPointerExit
446
- if (isNewlyHovering && prevHovering) {
447
- this.triggerOnExit(prevHovering, hovering.data, object);
478
+ if (isNewlyHovering && hovering?.obj) {
479
+ this.triggerOnExit(hovering.obj, hovering.data);
448
480
  }
449
481
 
450
482
  // save hovered object
@@ -467,7 +499,7 @@
467
499
  }
468
500
  }
469
501
  if (canvasGroup === null || canvasGroup.interactable) {
470
- this.handleMainInteraction(object, args, prevHovering ?? null);
502
+ this.handleMainInteraction(object, args, isNewlyHovering);
471
503
  }
472
504
 
473
505
  return true;
@@ -476,17 +508,22 @@
476
508
  /**
477
509
  * Propagate up in hiearchy and call the callback for each component that is possibly a handler
478
510
  */
479
- private propagate(object: Object3D | null, onComponent: (behaviour: Behaviour) => void) {
511
+ private propagate(object: THREE.Object3D, _args: PointerEventData, onComponent: (behaviour: Behaviour) => void) {
480
512
 
481
513
  while (true) {
514
+ // Propagate up the hierarchy
482
515
 
483
- if (!object) break;
516
+ if(_args.used) return;
484
517
 
485
518
  GameObject.foreachComponent(object, comp => {
486
519
  // TODO: implement Stop Immediate Propagation
520
+
487
521
  onComponent(comp);
522
+ // return undefined to continue iterating
523
+ return undefined;
488
524
  }, false);
489
525
 
526
+ if (!object.parent) break;
490
527
  // walk up
491
528
  object = object.parent;
492
529
  }
@@ -496,40 +533,18 @@
496
533
  /**
497
534
  * Propagate up in hiearchy and call handlers based on the pointer event data
498
535
  */
499
- private handleMainInteraction(object: Object3D, args: PointerEventData, prevHovering: Object3D | null) {
536
+ private handleMainInteraction(object: THREE.Object3D, args: PointerEventData, isNewlyHovering: boolean) {
537
+ if (args.pointerId === undefined) return;
500
538
  const pressedEvent = this.pressedByID.get(args.pointerId);
501
- const hoveredObjectChanged = prevHovering !== object;
502
539
 
503
- // TODO: should we not move this check up before we even raycast for "pointerMove" events? We dont need to do any processing if the pointer didnt move
504
- let isMoving = true;
505
- switch (args.event.pointerType) {
506
- case "mouse":
507
- case "touch":
508
- const posLastFrame = this.context.input.getPointerPositionLastFrame(args.pointerId!)!;
509
- const posThisFrame = this.context.input.getPointerPosition(args.pointerId!)!;
510
- isMoving = posLastFrame && !Mathf.approximately(posLastFrame, posThisFrame);
511
- break;
512
- case "controller":
513
- case "hand":
514
- // for hands and controller we assume they are never totally still (except for simulated environments)
515
- // we might want to add a threshold here (e.g. if a user holds their hand very still or controller)
516
- // so maybe check the angle every frame?
517
- break;
518
- }
519
-
520
- this.propagate(object, (behaviour) => {
540
+ this.propagate(object, args, (behaviour) => {
521
541
  const comp = behaviour as any;
522
542
 
523
543
  if (comp.interactable === false) return;
524
- if (!comp.activeAndEnabled || !comp.enabled) return;
525
544
 
526
545
  if (comp.onPointerEnter) {
527
- if (hoveredObjectChanged) {
528
- if (!comp[this.pointerEnterSymbol]) {
529
- comp[this.pointerEnterSymbol] = true;
530
- delete comp[this.pointerExitSymbol];
531
- comp.onPointerEnter(args);
532
- }
546
+ if (isNewlyHovering) {
547
+ comp.onPointerEnter(args);
533
548
  }
534
549
  }
535
550
 
@@ -541,20 +556,20 @@
541
556
  // So we can call the up event on the same handler
542
557
  // In a scenario where we Down on one object and Up on another
543
558
  pressedEvent?.handlers.add(comp);
544
-
545
- this.handlePointerCapture(args, comp);
546
559
  }
547
560
  }
548
561
 
549
- if (comp.onPointerMove) {
550
- if (isMoving)
551
- comp.onPointerMove(args);
552
- this.handlePointerCapture(args, comp);
562
+ const posLastFrame = this.context.input.getPointerPositionLastFrame(args.pointerId!)!;
563
+ const posThisFrame = this.context.input.getPointerPosition(args.pointerId!)!;
564
+ const isMoving = posLastFrame && !Mathf.approximately(posLastFrame, posThisFrame);
565
+
566
+ if (isMoving && comp.onPointerMove) {
567
+ comp.onPointerMove(args);
553
568
  }
554
569
 
555
570
  if (args.isUp) {
556
571
  if (comp.onPointerUp) {
557
- this.invokeOnPointerUp(args, comp);
572
+ comp.onPointerUp(args);
558
573
 
559
574
  // We don't want to call Up twice if we Down and Up on the same object
560
575
  // But if we Down on one and Up on another we want to call Up on the first one as well
@@ -562,9 +577,16 @@
562
577
  // The original component that received the down event SHOULD also receive the up event
563
578
  pressedEvent?.handlers.delete(comp);
564
579
  }
580
+
581
+ // handle onExit on touchUp
582
+ // onExit on mouse is handled when we hover over something else / on nothing
583
+ if (comp.onPointerExit && args.event?.pointerType === PointerType.Touch) {
584
+ comp.onPointerExit(args);
585
+ this.hoveredByID.delete(args.pointerId!);
586
+ }
565
587
  }
566
588
 
567
- if (args.isClick) {
589
+ if (args.isClicked) {
568
590
  if (comp.onPointerClick) {
569
591
  comp.onPointerClick(args);
570
592
  }
@@ -575,7 +597,9 @@
575
597
  // If user drags away from the object, then it doesn't get the UP event
576
598
  if (args.isUp) {
577
599
  pressedEvent?.handlers.forEach((handler) => {
578
- this.invokeOnPointerUp(args, handler);
600
+ if (handler.onPointerUp) {
601
+ handler.onPointerUp(args);
602
+ }
579
603
  });
580
604
 
581
605
  this.pressedByID.delete(args.pointerId);
@@ -585,102 +609,19 @@
585
609
  /**
586
610
  * Propagate up in hiearchy and call OnExit regardless of the pointer event data
587
611
  */
588
- private triggerOnExit(object: Object3D, args: PointerEventData, newObject: Object3D | null) {
612
+ private triggerOnExit(object: THREE.Object3D, args: PointerEventData) {
613
+ args.used = false;
589
614
 
590
- this.propagate(object, (behaviour) => {
615
+ this.propagate(object, args, (behaviour) => {
591
616
  if (!behaviour.gameObject || behaviour.destroyed) return;
592
617
 
593
618
  const inst: any = behaviour;
594
619
  if (inst.onPointerExit) {
595
- // if the newly hovered object is a child of the current object, we don't want to call onPointerExit
596
- if (newObject && this.isChild(newObject, behaviour.gameObject)) {
597
- return;
598
- }
599
- if (inst[this.pointerExitSymbol]) return;
600
- inst[this.pointerExitSymbol] = true;
601
- delete inst[this.pointerEnterSymbol];
602
620
  inst.onPointerExit(args);
603
621
  }
604
622
  });
605
623
  }
606
624
 
607
- /** handles onPointerUp - this will also release the pointerCapture */
608
- private invokeOnPointerUp(evt: PointerEventData, handler: IPointerUpHandler) {
609
- handler.onPointerUp?.call(handler, evt);
610
- this.releasePointerCapture(evt, handler);
611
- }
612
-
613
- /** the list of component handlers that requested pointerCapture for a specific pointerId */
614
- private readonly _capturedPointer: { [id: number]: IPointerEventHandler[] } = {};
615
-
616
- /** check if the event was marked to be captured: if yes add the current component to the captured list */
617
- private handlePointerCapture(evt: PointerEventData, comp: IPointerEventHandler) {
618
- if (evt.z__pointer_ctured) {
619
- evt.z__pointer_ctured = false;
620
- const id = evt.pointerId;
621
- // only the onPointerMove event is called with captured pointers so we don't need to add it to our list if it doesnt implement onPointerMove
622
- if (comp.onPointerMove) {
623
- const list = this._capturedPointer[id] || [];
624
- list.push(comp);
625
- this._capturedPointer[id] = list;
626
- }
627
- else {
628
- if (isDevEnvironment() && !comp["z__warned_no_pointermove"]) {
629
- comp["z__warned_no_pointermove"] = true;
630
- console.warn("PointerCapture was requested but the component doesn't implement onPointerMove. It will not receive any pointer events");
631
- }
632
- }
633
- }
634
- else if (evt.z__pointer_cture_rleased) {
635
- evt.z__pointer_cture_rleased = false;
636
- this.releasePointerCapture(evt, comp);
637
- }
638
- }
639
-
640
- /** removes the component from the pointer capture list */
641
- releasePointerCapture(evt: PointerEventData, component: IPointerEventHandler) {
642
- const id = evt.pointerId;
643
- if (this._capturedPointer[id]) {
644
- const i = this._capturedPointer[id].indexOf(component);
645
- if (i !== -1) {
646
- this._capturedPointer[id].splice(i, 1);
647
- if (debug) console.log("released pointer capture", id, component, this._capturedPointer)
648
- }
649
- }
650
- }
651
- /** invoke the pointerMove event on all captured handlers */
652
- private invokePointerCapture(evt: PointerEventData) {
653
- if (evt.event.type === InputEvents.PointerMove) {
654
- const id = evt.pointerId;
655
- const captured = this._capturedPointer[id];
656
- if (captured) {
657
- if (debug) console.log("Captured", id, captured)
658
- for (let i = 0; i < captured.length; i++) {
659
- const handler = captured[i];
660
- // check if it was destroyed
661
- const comp = handler as IComponent;
662
- if (comp.destroyed) {
663
- captured.splice(i, 1);
664
- i--;
665
- continue;
666
- }
667
- // invoke pointer move
668
- handler.onPointerMove?.call(handler, evt);
669
- }
670
- }
671
- }
672
- }
673
-
674
- private readonly pointerEnterSymbol = Symbol("pointerEnter");
675
- private readonly pointerExitSymbol = Symbol("pointerExit");
676
-
677
- private isChild(obj: Object3D, possibleChild: Object3D): boolean {
678
- if (!obj || !possibleChild) return false;
679
- if (obj === possibleChild) return true;
680
- if (!obj.parent) return false;
681
- return this.isChild(obj.parent, possibleChild);
682
- }
683
-
684
625
  private handleMeshUiObjectWithoutShadowDom(obj: any, pressed: boolean) {
685
626
  if (!obj || !obj.isUI) return true;
686
627
  const hit = this.handleMeshUIIntersection(obj, pressed);
@@ -688,7 +629,7 @@
688
629
  return hit;
689
630
  }
690
631
 
691
- private currentActiveMeshUIComponents: Object3D[] = [];
632
+ private currentActiveMeshUIComponents: ThreeMeshUI.Block[] = [];
692
633
 
693
634
  private handleMeshUIIntersection(meshUiObject: THREE.Object3D, pressed: boolean): boolean {
694
635
  const res = MeshUIHelper.updateState(meshUiObject, pressed);
@@ -756,8 +697,8 @@
756
697
  threeMeshUI.update();
757
698
  }
758
699
 
759
- static updateState(intersect: THREE.Object3D, _selectState: boolean): Object3D | null {
760
- let foundBlock: Object3D | null = null;
700
+ static updateState(intersect: THREE.Object3D, _selectState: boolean): ThreeMeshUI.Block | null {
701
+ let foundBlock: ThreeMeshUI.Block | null = null;
761
702
 
762
703
  if (intersect) {
763
704
  foundBlock = this.findBlockInParent(intersect);
@@ -784,7 +725,7 @@
784
725
  this.needsUpdate = true;
785
726
  }
786
727
 
787
- static findBlockInParent(elem: any): Object3D | null {
728
+ static findBlockInParent(elem: any): ThreeMeshUI.Block | null {
788
729
  if (!elem) return null;
789
730
  if (elem.isBlock) {
790
731
  // @TODO : Replace states managements
src/engine-components/EventTrigger.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { serializable } from "../engine/engine_serialization.js";
2
+ import { EventList } from "./EventList.js";
3
+ import type { IPointerEventHandler, PointerEventData } from "./ui/PointerEvents.js"
2
4
  import { Behaviour } from "./Component.js"
3
- import { EventList } from "./EventList.js";
4
5
  import { EventType } from "./EventType.js"
5
- import type { IPointerEventHandler, PointerEventData } from "./ui/PointerEvents.js"
6
6
 
7
7
  class TriggerEvent {
8
8
  @serializable()
src/engine/extensions/EXT_texture_exr.ts CHANGED
@@ -1,10 +1,9 @@
1
+ import { getParam } from "../engine_utils.js";
2
+ import { EXRLoader } from "three/examples/jsm/loaders/EXRLoader.js";
1
3
  import { Texture } from "three";
2
- import { EXRLoader } from "three/examples/jsm/loaders/EXRLoader.js";
3
4
  import { type GLTFLoaderPlugin, GLTFParser } from "three/examples/jsm/loaders/GLTFLoader.js";
4
5
 
5
- import { getParam } from "../engine_utils.js";
6
6
 
7
-
8
7
  const debug = getParam("debugexr");
9
8
 
10
9
  export class EXT_texture_exr implements GLTFLoaderPlugin {
src/engine/extensions/extension_utils.ts CHANGED
@@ -1,8 +1,7 @@
1
+ import { type IExtensionReferenceResolver } from "./extension_resolver.js";
1
2
  import { GLTFParser } from "three/examples/jsm/loaders/GLTFLoader.js";
2
-
3
3
  import { debugExtension } from "../engine_default_parameters.js";
4
4
  import { getParam } from "../engine_utils.js";
5
- import { type IExtensionReferenceResolver } from "./extension_resolver.js";
6
5
 
7
6
  const debug = getParam("debugresolvedependencies");
8
7
 
src/engine-components/export/usdz/Extension.ts CHANGED
@@ -1,7 +1,6 @@
1
+ import { USDObject, USDZExporterContext } from "./ThreeUSDZExporter.js";
1
2
  import { Object3D } from "three";
2
3
 
3
- import { USDObject, USDZExporterContext } from "./ThreeUSDZExporter.js";
4
-
5
4
  export interface IUSDExporterExtension {
6
5
 
7
6
  get extensionName(): string;
src/engine/extensions/extensions.ts CHANGED
@@ -1,21 +1,20 @@
1
- import { GLTFExporter, GLTFExporterPlugin, GLTFWriter } from "three/examples/jsm/exporters/GLTFExporter.js";
1
+ import { NEEDLE_techniques_webgl } from "./NEEDLE_techniques_webgl.js";
2
2
  import { GLTFLoader, GLTFParser } from "three/examples/jsm/loaders/GLTFLoader.js";
3
-
4
- import { isDevEnvironment } from "../debug/index.js";
5
- import { isResourceTrackingEnabled } from "../engine_assetdatabase.js";
6
- import { Context } from "../engine_setup.js";
7
- import { type ConstructorConcrete, type SourceIdentifier } from "../engine_types.js";
8
- import { getParam } from "../engine_utils.js";
9
- import { NEEDLE_lightmaps } from "../extensions/NEEDLE_lightmaps.js";
3
+ import { NEEDLE_components } from "./NEEDLE_components.js";
10
4
  import { EXT_texture_exr } from "./EXT_texture_exr.js";
11
- import { NEEDLE_components } from "./NEEDLE_components.js";
12
5
  import { NEEDLE_gameobject_data } from "./NEEDLE_gameobject_data.js";
6
+ import { NEEDLE_persistent_assets } from "./NEEDLE_persistent_assets.js";
7
+ import { NEEDLE_lightmaps } from "../extensions/NEEDLE_lightmaps.js";
8
+ import { type ConstructorConcrete, type SourceIdentifier } from "../engine_types.js";
9
+ import { Context } from "../engine_setup.js";
13
10
  import { NEEDLE_lighting_settings } from "./NEEDLE_lighting_settings.js";
14
- import { NEEDLE_persistent_assets } from "./NEEDLE_persistent_assets.js";
11
+ import { NEEDLE_render_objects } from "./NEEDLE_render_objects.js";
15
12
  import { NEEDLE_progressive } from "./NEEDLE_progressive.js";
16
- import { NEEDLE_render_objects } from "./NEEDLE_render_objects.js";
17
- import { NEEDLE_techniques_webgl } from "./NEEDLE_techniques_webgl.js";
18
13
  import { InternalUsageTrackerPlugin } from "./usage_tracker.js";
14
+ import { isResourceTrackingEnabled } from "../engine_assetdatabase.js";
15
+ import { getParam } from "../engine_utils.js";
16
+ import { isDevEnvironment } from "../debug/index.js";
17
+ import { GLTFExporter, GLTFExporterPlugin, GLTFWriter } from "three/examples/jsm/exporters/GLTFExporter.js";
19
18
 
20
19
  const debug = getParam("debugextensions");
21
20
 
src/engine-components/js-extensions/ExtensionUtils.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import { Object3D } from "three";
2
-
3
2
  import type { Constructor } from "../../engine/engine_types.js";
4
3
 
5
4
  const handlers: Map<any, ApplyPrototypeExtension> = new Map();
src/engine-components/FlyControls.ts CHANGED
@@ -1,7 +1,6 @@
1
+ import { Behaviour, GameObject } from "./Component.js";
1
2
  import { FlyControls as ThreeFlyControls } from "three/examples/jsm/controls/FlyControls.js";
2
-
3
3
  import { Camera } from "./Camera.js";
4
- import { Behaviour, GameObject } from "./Component.js";
5
4
 
6
5
  export class FlyControls extends Behaviour {
7
6
  private _controls: ThreeFlyControls | null = null;
src/engine-components/Fog.ts CHANGED
@@ -1,7 +1,6 @@
1
+ import { Behaviour } from "./Component.js";
1
2
  import { Color, Fog as Fog3 } from "three";
2
-
3
3
  import { serializable } from "../engine/engine_serialization.js";
4
- import { Behaviour } from "./Component.js";
5
4
 
6
5
 
7
6
  export enum FogMode {
src/engine-components/Gizmos.ts CHANGED
@@ -1,11 +1,10 @@
1
+ import { Behaviour } from "./Component.js";
1
2
  import * as THREE from "three";
3
+ import * as Gizmos from "../engine/engine_gizmos.js";
4
+ import * as params from "../engine/engine_default_parameters.js";
5
+ import { FrameEvent } from "../engine/engine_setup.js";
2
6
  import { BoxHelper, Color } from "three";
3
-
4
- import * as params from "../engine/engine_default_parameters.js";
5
- import * as Gizmos from "../engine/engine_gizmos.js";
6
7
  import { serializable } from "../engine/engine_serialization_decorator.js";
7
- import { FrameEvent } from "../engine/engine_setup.js";
8
- import { Behaviour } from "./Component.js";
9
8
 
10
9
 
11
10
  export class BoxGizmo extends Behaviour {
src/engine-components/export/gltf/GltfExport.ts CHANGED
@@ -1,17 +1,17 @@
1
1
  import { Object3D, Vector3 } from "three";
2
- import { AnimationClip } from "three";
3
2
  import { GLTFExporter, type GLTFExporterOptions } from 'three/examples/jsm/exporters/GLTFExporter.js';
4
3
 
4
+ import { Behaviour, GameObject } from "../../Component.js";
5
+ import GLTFMeshGPUInstancingExtension from '../../../include/three/EXT_mesh_gpu_instancing_exporter.js';
6
+ import { Renderer } from "../../Renderer.js";
5
7
  import { SerializationContext } from "../../../engine/engine_serialization_core.js";
6
8
  import { serializable } from "../../../engine/engine_serialization_decorator.js";
9
+ import { NEEDLE_components } from "../../../engine/extensions/NEEDLE_components.js";
7
10
  import { getWorldPosition } from "../../../engine/engine_three_utils.js";
11
+ import { BoxHelperComponent } from "../../BoxHelperComponent.js";
12
+ import { AnimationClip } from "three";
8
13
  import { getParam } from "../../../engine/engine_utils.js";
9
14
  import { registerExportExtensions } from "../../../engine/extensions/index.js";
10
- import { NEEDLE_components } from "../../../engine/extensions/NEEDLE_components.js";
11
- import GLTFMeshGPUInstancingExtension from '../../../include/three/EXT_mesh_gpu_instancing_exporter.js';
12
- import { BoxHelperComponent } from "../../BoxHelperComponent.js";
13
- import { Behaviour, GameObject } from "../../Component.js";
14
- import { Renderer } from "../../Renderer.js";
15
15
 
16
16
  const debugExport = getParam("debuggltfexport");
17
17
 
src/engine-components/ui/Graphic.ts CHANGED
@@ -1,15 +1,14 @@
1
- import { Color, LinearSRGBColorSpace, Object3D, SRGBColorSpace, Texture } from 'three';
1
+ import { type IGraphic, type IRectTransformChangedReceiver } from './Interfaces.js';
2
2
  import * as ThreeMeshUI from 'three-mesh-ui'
3
- import SimpleStateBehavior from "three-mesh-ui/examples/behaviors/states/SimpleStateBehavior.js"
4
-
5
- import { serializable } from '../../engine/engine_serialization_decorator.js';
6
- import { GameObject } from '../Component.js';
7
3
  import { RGBAColor } from "../js-extensions/RGBAColor.js"
8
4
  import { BaseUIComponent } from "./BaseUIComponent.js";
9
- import { type IGraphic, type IRectTransformChangedReceiver } from './Interfaces.js';
10
- import { Outline } from './Outline.js';
5
+ import { serializable } from '../../engine/engine_serialization_decorator.js';
6
+ import { Color, LinearSRGBColorSpace, SRGBColorSpace, Texture } from 'three';
11
7
  import { RectTransform } from './RectTransform.js';
12
8
  import { onChange, scheduleAction } from "./Utils.js"
9
+ import { GameObject } from '../Component.js';
10
+ import SimpleStateBehavior from "three-mesh-ui/examples/behaviors/states/SimpleStateBehavior.js"
11
+ import { Outline } from './Outline.js';
13
12
 
14
13
  const _colorStateObject: { backgroundColor: Color, backgroundOpacity: number, borderColor: Color, borderOpacity: number } = {
15
14
  backgroundColor: new Color(1, 1, 1),
@@ -138,7 +137,7 @@
138
137
  onEnable(): void {
139
138
  super.onEnable();
140
139
  if (this.uiObject) {
141
- this.rectTransform.shadowComponent?.add(this.uiObject as unknown as Object3D);
140
+ this.rectTransform.shadowComponent?.add(this.uiObject);
142
141
  this.addShadowComponent(this.uiObject, this.rectTransform);
143
142
  }
144
143
 
src/engine-components/GridHelper.ts CHANGED
@@ -1,9 +1,8 @@
1
+ import { Behaviour } from "./Component.js";
2
+ import { serializable } from "../engine/engine_serialization_decorator.js";
3
+ import * as params from "../engine/engine_default_parameters.js";
1
4
  import { Color, GridHelper as _GridHelper } from "three";
2
5
 
3
- import * as params from "../engine/engine_default_parameters.js";
4
- import { serializable } from "../engine/engine_serialization_decorator.js";
5
- import { Behaviour } from "./Component.js";
6
-
7
6
  export class GridHelper extends Behaviour {
8
7
 
9
8
  @serializable()
src/engine-components/GroundProjection.ts CHANGED
@@ -1,10 +1,9 @@
1
+ import { Behaviour, GameObject } from "./Component.js";
2
+ import { GroundProjectedSkybox as GroundProjection } from 'three/examples/jsm/objects/GroundProjectedSkybox.js';
3
+ import { serializable } from "../engine/engine_serialization_decorator.js";
4
+ import { Watch as Watch, getParam } from "../engine/engine_utils.js";
1
5
  import { Texture } from "three";
2
- import { GroundedSkybox as GroundProjection } from 'three/examples/jsm/objects/GroundedSkybox.js';
3
6
 
4
- import { serializable } from "../engine/engine_serialization_decorator.js";
5
- import { getParam,Watch as Watch } from "../engine/engine_utils.js";
6
- import { Behaviour, GameObject } from "./Component.js";
7
-
8
7
  const debug = getParam("debuggroundprojection");
9
8
 
10
9
  export class GroundProjectedEnv extends Behaviour {
@@ -82,19 +81,14 @@
82
81
  if (!this.env || this.context.scene.environment !== this._lastEnvironment) {
83
82
  if (debug)
84
83
  console.log("Create/Update Ground Projection", this.context.scene.environment.name);
85
- this.env = new GroundProjection(this.context.scene.environment, this._height, this.radius, 32);
86
- this.env.position.y = this._height;
84
+ this.env = new GroundProjection(this.context.scene.environment);
87
85
  }
88
86
  this._lastEnvironment = this.context.scene.environment;
89
87
  if (!this.env.parent)
90
88
  this.gameObject.add(this.env);
91
-
92
- /* TODO realtime adjustments aren't possible anymore with GroundedSkybox (mesh generation)
93
89
  this.env.scale.setScalar(this._scale);
94
90
  this.env.radius = this._radius;
95
91
  this.env.height = this._height;
96
- */
97
-
98
92
  // dont make the ground projection raycastable by default
99
93
  if (this.env.isObject3D === true) {
100
94
  this.env.layers.set(2);
src/engine-components/ui/Image.ts CHANGED
@@ -1,6 +1,5 @@
1
+ import { serializable } from '../../engine/engine_serialization_decorator.js';
1
2
  import { Color, Texture } from 'three';
2
-
3
- import { serializable } from '../../engine/engine_serialization_decorator.js';
4
3
  import { MaskableGraphic } from './Graphic.js';
5
4
 
6
5
 
src/engine-components/export/usdz/index.ts CHANGED
@@ -1,3 +1,3 @@
1
- export { type UsdzBehaviour } from "./extensions/behavior/Behaviour.js";
2
- export { imageToCanvas,USDObject } from "./ThreeUSDZExporter.js";
3
- export { USDZExporter } from "./USDZExporter.js";
1
+ export { USDZExporter } from "./USDZExporter.js";
2
+ export { USDObject, imageToCanvas } from "./ThreeUSDZExporter.js";
3
+ export { type UsdzBehaviour } from "./extensions/behavior/Behaviour.js";
src/engine-components/postprocessing/index.ts CHANGED
@@ -1,4 +1,4 @@
1
+ export * from "./VolumeParameter.js"
2
+ export * from "./PostProcessingHandler.js"
1
3
  export * from "./PostProcessingEffect.js";
2
- export * from "./PostProcessingHandler.js"
3
- export * from "./VolumeParameter.js"
4
4
  export * from "./VolumeProfile.js";
src/engine-components/timeline/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { type ITimelineAnimationCallbacks as ITimelineAnimationOverride } from "./PlayableDirector.js"
2
1
  export * from "./SignalAsset.js"
2
+ export * from "./TimelineTracks.js"
3
3
  export * from "./TimelineModels.js"
4
- export * from "./TimelineTracks.js"
4
+ export { type ITimelineAnimationCallbacks as ITimelineAnimationOverride } from "./PlayableDirector.js"
src/engine-components/webxr/index.ts CHANGED
@@ -1,3 +1,4 @@
1
- export { WebXR as WebXR } from "./WebXR.js";
1
+ export * from "./WebXR.js";
2
+ export * from "./WebXRPlaneTracking.js";
2
3
  export * from "./WebXRImageTracking.js";
3
- export * from "./WebXRPlaneTracking.js";
4
+ export * from "./WebXRController.js";
src/engine/extensions/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export * from "./extensions.js"
2
2
  export * from "./NEEDLE_animator_controller_model.js"
3
- export { SceneLightSettings } from "./NEEDLE_lighting_settings.js"
4
3
  export * from "./NEEDLE_progressive.js"
5
- export { CustomShader } from "./NEEDLE_techniques_webgl.js"
4
+ export { CustomShader } from "./NEEDLE_techniques_webgl.js"
5
+ export { SceneLightSettings } from "./NEEDLE_lighting_settings.js"
src/engine/xr/index.ts DELETED
@@ -1,5 +0,0 @@
1
- export * from "./NeedleXRController.js";
2
- export * from "./NeedleXRSession.js";
3
- export * from "./NeedleXRSync.js"
4
- export * from "./utils.js"
5
- export * from "./XRRig.js";
src/engine-components/ui/InputField.ts CHANGED
@@ -1,10 +1,10 @@
1
+ import { Behaviour, GameObject } from "../Component.js";
2
+ import { type IPointerEventHandler } from "./PointerEvents.js";
3
+ import { FrameEvent } from "../../engine/engine_setup.js";
1
4
  import { serializable } from "../../engine/engine_serialization_decorator.js";
2
- import { FrameEvent } from "../../engine/engine_setup.js";
5
+ import { Text } from "./Text.js";
3
6
  import { getParam, isiOS } from "../../engine/engine_utils.js";
4
- import { Behaviour, GameObject } from "../Component.js";
5
7
  import { EventList } from "../EventList.js";
6
- import { type IPointerEventHandler } from "./PointerEvents.js";
7
- import { Text } from "./Text.js";
8
8
  import { tryGetUIComponent } from "./Utils.js";
9
9
 
10
10
  const debug = getParam("debuginputfield");
src/engine-components/Interactable.ts CHANGED
@@ -1,11 +1,19 @@
1
1
  import { Behaviour } from "./Component.js";
2
+ import type { IPointerClickHandler, PointerEventData } from "./ui/PointerEvents.js";
2
3
 
3
- /**
4
- * Marks an object as currently being interacted with.
5
- * For example, DragControls set this on the dragged object to prevent DeleteBox from deleting it.
6
- */
4
+
5
+ export class Interactable extends Behaviour implements IPointerClickHandler {
6
+
7
+ canGrab : boolean = true;
8
+
9
+ onPointerClick(_args: PointerEventData) {
10
+ }
11
+ }
12
+
13
+
14
+ // TODO: how do we sync things like that...
7
15
  export class UsageMarker extends Behaviour
8
16
  {
9
- public isUsed: boolean = true;
10
- public usedBy: any = null;
17
+ public isUsed : boolean = true;
18
+ public usedBy : any = null;
11
19
  }
src/engine/xr/internal.ts DELETED
@@ -1,35 +0,0 @@
1
- import { Matrix4, Object3D, Quaternion, Scene, Vector3 } from 'three';
2
-
3
- import { CreateWireCube, Gizmos } from '../engine_gizmos.js';
4
- import { IGameObject } from '../engine_types.js';
5
- import { getParam } from '../engine_utils.js';
6
- import { IXRRig } from './XRRig.js';
7
-
8
- export const flipForwardMatrix = new Matrix4().makeRotationY(Math.PI);
9
- export const flipForwardQuaternion = new Quaternion().setFromAxisAngle(new Vector3(0, 1, 0), Math.PI);
10
-
11
- const debug = getParam("debugwebxr");
12
-
13
- export class ImplictXRRig implements IXRRig {
14
-
15
- priority = -100000;
16
- gameObject: IGameObject;
17
-
18
- isXRRig(): boolean {
19
- return true;
20
- }
21
-
22
- get isActive(): boolean {
23
- return this.gameObject.visible;
24
- }
25
-
26
- constructor() {
27
- this.gameObject = new Object3D() as IGameObject;
28
- this.gameObject.name = "Implicit XR Rig";
29
- if (debug) {
30
- const cube = CreateWireCube(0xff55dd);
31
- cube.position.y += .5;
32
- this.gameObject.add(cube);
33
- }
34
- }
35
- }
src/engine-components/Joints.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import { Vector3 } from "three";
2
-
3
2
  import { serializable } from "../engine/engine_serialization.js";
4
3
  import { Behaviour } from "./Component.js";
5
4
  import { Rigidbody } from "./RigidBody.js";
src/engine-components/ui/Layout.ts CHANGED
@@ -1,9 +1,9 @@
1
+ import { type ILayoutGroup, type IRectTransform } from "./Interfaces.js";
2
+ import { Behaviour, GameObject } from "../Component.js";
1
3
  import { serializable } from "../../engine/engine_serialization.js";
2
- import { getParam } from "../../engine/engine_utils.js";
3
- import { Behaviour, GameObject } from "../Component.js";
4
4
  import { Canvas } from "./Canvas.js";
5
- import { type ILayoutGroup, type IRectTransform } from "./Interfaces.js";
6
5
  import { RectTransform } from "./RectTransform.js";
6
+ import { getParam } from "../../engine/engine_utils.js";
7
7
 
8
8
  const debug = getParam("debuguilayout");
9
9
 
src/engine-components/Light.ts CHANGED
@@ -1,14 +1,13 @@
1
+ import { Behaviour, GameObject } from "./Component.js";
1
2
  import * as THREE from "three";
3
+ import { getParam, isMobileDevice } from "../engine/engine_utils.js";
4
+ import { setWorldPositionXYZ } from "../engine/engine_three_utils.js";
5
+ import { FrameEvent } from "../engine/engine_setup.js";
6
+ import { serializable } from "../engine/engine_serialization_decorator.js";
2
7
  import { Color, DirectionalLight, OrthographicCamera } from "three";
3
-
4
- import { serializable } from "../engine/engine_serialization_decorator.js";
5
- import { FrameEvent } from "../engine/engine_setup.js";
6
- import { setWorldPositionXYZ } from "../engine/engine_three_utils.js";
8
+ import { WebXR, WebXREvent } from "./webxr/WebXR.js";
9
+ import { WebARSessionRoot } from "./webxr/WebARSessionRoot.js";
7
10
  import type { ILight } from "../engine/engine_types.js";
8
- import { getParam, isMobileDevice } from "../engine/engine_utils.js";
9
- import { NeedleXREventArgs } from "../engine/xr/index.js";
10
- import { Behaviour, GameObject } from "./Component.js";
11
- import { WebARSessionRoot } from "./webxr/WebARSessionRoot.js";
12
11
 
13
12
  // https://threejs.org/examples/webgl_shadowmap_csm.html
14
13
 
@@ -271,6 +270,8 @@
271
270
  }
272
271
  if (this.type === LightType.Directional)
273
272
  this.startCoroutine(this.updateMainLightRoutine(), FrameEvent.LateUpdate);
273
+ this._webXRStartedListener = WebXR.addEventListener(WebXREvent.XRStarted, this.onWebXRStarted.bind(this));
274
+ this._webXREndedListener = WebXR.addEventListener(WebXREvent.XRStopped, this.onWebXREnded.bind(this));
274
275
  }
275
276
 
276
277
  onDisable() {
@@ -281,13 +282,15 @@
281
282
  else
282
283
  this.light.visible = false;
283
284
  }
285
+ WebXR.removeEventListener(WebXREvent.XRStarted, this._webXRStartedListener);
286
+ WebXR.removeEventListener(WebXREvent.XRStopped, this._webXREndedListener);
284
287
  }
285
288
 
286
289
  private _webXRStartedListener?: Function;
287
290
  private _webXREndedListener?: Function;
288
291
  private _webARRoot?: WebARSessionRoot;
289
292
 
290
- onEnterXR(_args: NeedleXREventArgs): void {
293
+ private onWebXRStarted() {
291
294
  this._webARRoot = GameObject.getComponentInParent(this.gameObject, WebARSessionRoot) ?? undefined;
292
295
  // this.startCoroutine(this._updateLightIntensityInARRoutine());
293
296
  }
@@ -300,7 +303,7 @@
300
303
  // }
301
304
  // }
302
305
 
303
- onLeaveXR(_args: NeedleXREventArgs): void {
306
+ private onWebXREnded() {
304
307
  // this.updateIntensity();
305
308
  }
306
309
 
src/engine-components/LODGroup.ts CHANGED
@@ -1,11 +1,10 @@
1
+ import { Behaviour, GameObject } from "./Component.js";
1
2
  import * as THREE from "three";
3
+ import { Renderer } from "./Renderer.js";
4
+ import { getParam } from "../engine/engine_utils.js";
5
+ import { serializable } from "../engine/engine_serialization_decorator.js";
2
6
  import { Vector3 } from "three";
3
7
 
4
- import { serializable } from "../engine/engine_serialization_decorator.js";
5
- import { getParam } from "../engine/engine_utils.js";
6
- import { Behaviour, GameObject } from "./Component.js";
7
- import { Renderer } from "./Renderer.js";
8
-
9
8
  const debug = getParam("debuglods");
10
9
  const noLods = getParam("nolods");
11
10
 
src/engine-components/debug/LogStats.ts CHANGED
@@ -1,6 +1,6 @@
1
+ import { Behaviour } from "../../engine-components/Component.js";
1
2
  import { FrameEvent } from "../../engine/engine_setup.js";
2
3
  import { getParam } from "../../engine/engine_utils.js";
3
- import { Behaviour } from "../../engine-components/Component.js";
4
4
 
5
5
  const debug = getParam("logstats");
6
6
 
src/engine-components/utils/LookAt.ts CHANGED
@@ -1,11 +1,11 @@
1
+ import { serializable } from "../../engine/engine_serialization.js";
2
+ import { Behaviour } from "../Component.js";
1
3
  import { Matrix4, Object3D, Quaternion, Vector3 } from "three";
4
+ import { getWorldPosition, getWorldQuaternion, lookAtObject, setWorldQuaternion } from "../../engine/engine_three_utils.js";
2
5
 
3
- import { serializable } from "../../engine/engine_serialization.js";
4
- import { getWorldPosition, getWorldQuaternion, lookAtObject, setWorldQuaternion } from "../../engine/engine_three_utils.js";
6
+ import { USDObject } from "../../engine-components/export/usdz/ThreeUSDZExporter.js";
5
7
  import { type UsdzBehaviour } from "../../engine-components/export/usdz/extensions/behavior/Behaviour.js";
6
8
  import { ActionBuilder, BehaviorModel, TriggerBuilder, USDVec3 } from "../../engine-components/export/usdz/extensions/behavior/BehavioursBuilder.js";
7
- import { USDObject } from "../../engine-components/export/usdz/ThreeUSDZExporter.js";
8
- import { Behaviour } from "../Component.js";
9
9
 
10
10
  export class LookAt extends Behaviour implements UsdzBehaviour {
11
11
 
src/engine-components/LookAtConstraint.ts CHANGED
@@ -1,9 +1,8 @@
1
+ import { Behaviour, GameObject } from "./Component.js";
1
2
  import * as THREE from "three";
3
+ import { serializable } from "../engine/engine_serialization_decorator.js";
2
4
  import { Object3D } from "three";
3
5
 
4
- import { serializable } from "../engine/engine_serialization_decorator.js";
5
- import { Behaviour, GameObject } from "./Component.js";
6
-
7
6
  export class LookAtConstraint extends Behaviour {
8
7
 
9
8
  constraintActive: boolean = true;
src/engine/extensions/NEEDLE_animator_controller_model.ts CHANGED
@@ -1,8 +1,7 @@
1
+ import { Animator } from "../../engine-components/Animator.js";
1
2
  import { AnimationAction, AnimationClip, MathUtils, Object3D } from "three"
2
-
3
+ import { Context } from "../engine_setup.js";
3
4
  import { InstantiateIdProvider } from "../../engine/engine_networking_instantiate.js";
4
- import { Animator } from "../../engine-components/Animator.js";
5
- import { Context } from "../engine_setup.js";
6
5
 
7
6
 
8
7
  export declare type AnimatorControllerModel = {
src/engine/extensions/NEEDLE_components.ts CHANGED
@@ -1,13 +1,12 @@
1
- import { Object3D } from "three";
1
+ import { type GLTF, type GLTFLoaderPlugin, GLTFParser } from "three/examples/jsm/loaders/GLTFLoader.js";
2
+ import { type NodeToObjectMap, type ObjectToNodeMap, SerializationContext } from "../engine_serialization_core.js";
2
3
  import { GLTFExporter } from 'three/examples/jsm/exporters/GLTFExporter.js';
3
- import { type GLTF, type GLTFLoaderPlugin, GLTFParser } from "three/examples/jsm/loaders/GLTFLoader.js";
4
-
4
+ import { debugExtension } from "../engine_default_parameters.js";
5
+ import { builtinComponentKeyName } from "../engine_constants.js";
6
+ import { resolveReferences } from "./extension_utils.js";
5
7
  import { apply } from "../../engine-components/js-extensions/Object3D.js";
6
- import { builtinComponentKeyName } from "../engine_constants.js";
7
- import { debugExtension } from "../engine_default_parameters.js";
8
8
  import { getLoader } from "../engine_gltf.js";
9
- import { type NodeToObjectMap, type ObjectToNodeMap, SerializationContext } from "../engine_serialization_core.js";
10
- import { resolveReferences } from "./extension_utils.js";
9
+ import { Object3D } from "three";
11
10
 
12
11
  export const debug = debugExtension
13
12
  const componentsArrayExportKey = "$___Export_Components";
src/engine/extensions/NEEDLE_gameobject_data.ts CHANGED
@@ -39,7 +39,7 @@
39
39
  // }
40
40
 
41
41
  // private lastIndex: number = -1;
42
- afterRoot(_result: GLTF): Promise<any> | null {
42
+ afterRoot(_result: GLTF): Promise<void> | null {
43
43
  // console.log("AFTER ROOT", _result);
44
44
  const promises: Promise<void>[] = [];
45
45
  for (let index = 0; index < this.parser.json.nodes?.length; index++) {
@@ -52,7 +52,7 @@
52
52
  }
53
53
  }
54
54
  }
55
- return Promise.all(promises).then(() => null);
55
+ return Promise.all(promises).then(() => { });
56
56
  }
57
57
 
58
58
  private async findAndApplyExtensionData(nodeId: number, ext: GameObjectData) {
@@ -76,7 +76,7 @@
76
76
  node.userData.static = ext.static ?? false;
77
77
 
78
78
  node.visible = ext.activeSelf ?? true;
79
-
79
+
80
80
  node["guid"] = ext.guid;
81
81
  // console.log(node.name, ext.activeSelf, node);
82
82
  }
src/engine/extensions/NEEDLE_lighting_settings.ts CHANGED
@@ -1,15 +1,14 @@
1
1
  import { AmbientLight, Color, HemisphereLight, Object3D } from "three";
2
- import { LightProbe } from "three";
3
2
  import { type GLTF, type GLTFLoaderPlugin, GLTFParser } from "three/examples/jsm/loaders/GLTFLoader.js";
4
-
3
+ import { type SourceIdentifier } from "../engine_types.js";
5
4
  import { Behaviour, GameObject } from "../../engine-components/Component.js";
5
+ import { AmbientMode, DefaultReflectionMode } from "../engine_scenelighting.js";
6
+ import { LightmapType } from "./NEEDLE_lightmaps.js";
7
+ import { getParam } from "../engine_utils.js";
8
+ import { Context } from "../engine_setup.js";
9
+ import { LightProbe } from "three";
6
10
  import { ContextEvent, ContextRegistry } from "../engine_context_registry.js";
7
11
  import { Mathf } from "../engine_math.js";
8
- import { AmbientMode, DefaultReflectionMode } from "../engine_scenelighting.js";
9
- import { Context } from "../engine_setup.js";
10
- import { type SourceIdentifier } from "../engine_types.js";
11
- import { getParam } from "../engine_utils.js";
12
- import { LightmapType } from "./NEEDLE_lightmaps.js";
13
12
 
14
13
  export const EXTENSION_NAME = "NEEDLE_lighting_settings";
15
14
  const debug = getParam("debugenvlight");
src/engine/extensions/NEEDLE_lightmaps.ts CHANGED
@@ -1,13 +1,12 @@
1
+ import { type ILightDataRegistry } from "../engine_lightdata.js";
1
2
  import { LinearSRGBColorSpace, SRGBColorSpace, Texture, TextureLoader } from "three";
3
+ import { type GLTF, type GLTFLoaderPlugin, GLTFParser } from "three/examples/jsm/loaders/GLTFLoader.js";
4
+ import { type SourceIdentifier } from "../engine_types.js";
5
+ import { resolveReferences } from "./extension_utils.js";
6
+ import { getParam, PromiseAllWithErrors, resolveUrl } from "../engine_utils.js";
2
7
  import { EXRLoader } from "three/examples/jsm/loaders/EXRLoader.js";
3
- import { type GLTF, type GLTFLoaderPlugin, GLTFParser } from "three/examples/jsm/loaders/GLTFLoader.js";
4
8
  import { RGBELoader } from "three/examples/jsm/loaders/RGBELoader.js";
5
-
6
9
  import { isDevEnvironment } from "../debug/debug.js";
7
- import { type ILightDataRegistry } from "../engine_lightdata.js";
8
- import { type SourceIdentifier } from "../engine_types.js";
9
- import { getParam, PromiseAllWithErrors, resolveUrl } from "../engine_utils.js";
10
- import { resolveReferences } from "./extension_utils.js";
11
10
 
12
11
  // the lightmap extension is aimed to also export export skyboxes and custom reflection maps
13
12
  // should we rename it?
@@ -61,7 +60,7 @@
61
60
  if (debug)
62
61
  console.log(ext);
63
62
 
64
- return new Promise(async (resolve, _reject) => {
63
+ return new Promise(async (res, _rej) => {
65
64
 
66
65
  const dependencies: Array<Promise<any>> = [];
67
66
  for (const entry of arr) {
@@ -98,7 +97,7 @@
98
97
  if (isDevEnvironment())
99
98
  console.error("Failed to load lightmap extension", results);
100
99
  }
101
- resolve();
100
+ res();
102
101
  });
103
102
  }
104
103
  }
src/engine/extensions/NEEDLE_persistent_assets.ts CHANGED
@@ -1,9 +1,8 @@
1
+ import { resolveReferences } from "./extension_utils.js";
1
2
  import { type GLTF, type GLTFLoaderPlugin, GLTFParser } from "three/examples/jsm/loaders/GLTFLoader.js";
2
-
3
+ import { type IExtensionReferenceResolver } from "./extension_resolver.js";
3
4
  import { debugExtension } from "../engine_default_parameters.js";
4
5
  import { TypeStore } from "../engine_typestore.js";
5
- import { type IExtensionReferenceResolver } from "./extension_resolver.js";
6
- import { resolveReferences } from "./extension_utils.js";
7
6
 
8
7
  export const EXTENSION_NAME = "NEEDLE_persistent_assets";
9
8
 
src/engine/extensions/NEEDLE_progressive.ts CHANGED
@@ -1,10 +1,9 @@
1
1
  import { Material, RawShaderMaterial, Texture, TextureLoader } from "three";
2
2
  import { type GLTF, GLTFLoader, type GLTFLoaderPlugin, GLTFParser } from "three/examples/jsm/loaders/GLTFLoader.js";
3
-
3
+ import { type SourceIdentifier } from "../engine_types.js";
4
+ import { Context } from "../engine_setup.js";
4
5
  import { addDracoAndKTX2Loaders } from "../engine_loaders.js";
5
- import { Context } from "../engine_setup.js";
6
- import { type SourceIdentifier } from "../engine_types.js";
7
- import { delay, getParam, PromiseAllWithErrors, PromiseErrorResult, resolveUrl } from "../engine_utils.js";
6
+ import { PromiseAllWithErrors, PromiseErrorResult, delay, getParam, resolveUrl } from "../engine_utils.js";
8
7
 
9
8
  export const EXTENSION_NAME = "NEEDLE_progressive";
10
9
 
@@ -133,7 +132,6 @@
133
132
  if (t.source)
134
133
  t.source[$progressiveTextureExtension] = ext;
135
134
  NEEDLE_progressive.cache.set(t.uuid, ext);
136
- return t;
137
135
  });
138
136
  }
139
137
  }
src/engine/extensions/NEEDLE_render_objects.ts CHANGED
@@ -1,34 +1,34 @@
1
1
 
2
+ import { type SourceIdentifier } from "../engine_types.js";
3
+ import { type GLTF, type GLTFLoaderPlugin, GLTFParser } from "three/examples/jsm/loaders/GLTFLoader.js";
4
+ import { type IComponent as Component, type IRenderer } from "../engine_types.js";
5
+
2
6
  import {
3
- AlwaysStencilFunc,
4
- DecrementStencilOp,
5
- DecrementWrapStencilOp,
7
+ // stencil funcs
8
+ NeverStencilFunc,
9
+ LessStencilFunc,
6
10
  EqualStencilFunc,
11
+ LessEqualStencilFunc,
12
+ GreaterStencilFunc,
13
+ NotEqualStencilFunc,
7
14
  GreaterEqualStencilFunc,
8
- GreaterStencilFunc,
15
+ AlwaysStencilFunc,
16
+ // stencil ops
17
+ ZeroStencilOp,
18
+ KeepStencilOp,
19
+ ReplaceStencilOp,
9
20
  IncrementStencilOp,
21
+ DecrementStencilOp,
10
22
  IncrementWrapStencilOp,
23
+ DecrementWrapStencilOp,
11
24
  InvertStencilOp,
12
- KeepStencilOp,
13
- LessEqualStencilFunc,
14
- LessStencilFunc,
15
- // stencil funcs
16
- NeverStencilFunc,
17
- NotEqualStencilFunc,
18
- ReplaceStencilOp,
19
25
  type StencilFunc,
20
26
  type StencilOp as ThreeStencilOp,
21
- // stencil ops
22
- ZeroStencilOp,
23
27
  } from "three";
24
- import { type GLTF, type GLTFLoaderPlugin, GLTFParser } from "three/examples/jsm/loaders/GLTFLoader.js";
25
-
28
+ import { getParam } from "../engine_utils.js";
26
29
  import { showBalloonWarning } from "../debug/index.js";
27
30
  import { isUsingInstancing } from "../engine_gameobject.js";
28
31
  import { isLocalNetwork } from "../engine_networking_utils.js";
29
- import { type SourceIdentifier } from "../engine_types.js";
30
- import { type IComponent as Component, type IRenderer } from "../engine_types.js";
31
- import { getParam } from "../engine_utils.js";
32
32
 
33
33
  const debug = getParam("debugstencil");
34
34
 
src/engine/extensions/NEEDLE_techniques_webgl.ts CHANGED
@@ -1,13 +1,12 @@
1
+ import { type GLTFLoaderPlugin, GLTFParser } from "three/examples/jsm/loaders/GLTFLoader.js";
2
+ import { FindShaderTechniques, whiteDefaultTexture, ToUnityMatrixArray, SetUnitySphericalHarmonics } from '../engine_shaders.js';
1
3
  import { AlwaysDepth, BackSide, Camera, DoubleSide, EqualDepth, FrontSide, GLSL3, GreaterDepth, GreaterEqualDepth, type IUniform, LessDepth, LessEqualDepth, LinearSRGBColorSpace, Material, Matrix4, NotEqualDepth, Object3D, RawShaderMaterial, Texture, Vector3, Vector4 } from 'three';
2
- import { type GLTFLoaderPlugin, GLTFParser } from "three/examples/jsm/loaders/GLTFLoader.js";
3
-
4
4
  import { Context } from '../engine_setup.js';
5
- import { FindShaderTechniques, SetUnitySphericalHarmonics,ToUnityMatrixArray, whiteDefaultTexture } from '../engine_shaders.js';
6
- import { getWorldPosition } from "../engine_three_utils.js";
5
+ import { getParam } from "../engine_utils.js";
6
+ import * as SHADERDATA from "../shaders/shaderData.js"
7
7
  import { type SourceIdentifier } from "../engine_types.js";
8
8
  import { type ILight } from "../engine_types.js";
9
- import { getParam } from "../engine_utils.js";
10
- import * as SHADERDATA from "../shaders/shaderData.js"
9
+ import { getWorldPosition } from "../engine_three_utils.js";
11
10
 
12
11
  const debug = getParam("debugcustomshader");
13
12
 
@@ -89,9 +88,7 @@
89
88
  if (debug)
90
89
  console.log(this);
91
90
 
92
- //@ts-ignore - TODO: how to override and do we even need this?
93
91
  this.type = "NEEDLE_CUSTOM_SHADER";
94
-
95
92
  if (!this.uniforms[this._objToWorldName])
96
93
  this.uniforms[this._objToWorldName] = { value: [] };
97
94
  if (!this.uniforms[this._worldToObjectName])
src/needle-engine.ts CHANGED
@@ -1,3 +1,6 @@
1
+ import { makeErrorsVisibleForDevelopment } from "./engine/debug/debug_overlay.js";
2
+ makeErrorsVisibleForDevelopment();
3
+
1
4
  import "./engine/engine_element.js";
2
5
  import "./engine/engine_setup.js";
3
6
  export * from "./engine/api.js";
src/engine/xr/NeedleXRController.ts DELETED
@@ -1,639 +0,0 @@
1
- import { fetchProfile, MotionController } from "@webxr-input-profiles/motion-controllers";
2
- import { AxesHelper, Int8BufferAttribute, Object3D, Quaternion, Ray, Vector3 } from "three";
3
-
4
- import { Context } from "../engine_context.js";
5
- import { Gizmos } from "../engine_gizmos.js";
6
- import { InputEventNames, InputEvents, NEPointerEvent, NEPointerEventInit, PointerType } from "../engine_input.js";
7
- import { getTempQuaternion, getTempVector, getWorldQuaternion } from "../engine_three_utils.js";
8
- import type { ButtonName, IGameObject, Vec3, XRControllerButtonName } from "../engine_types.js";
9
- import { getParam } from "../engine_utils.js";
10
- import { flipForwardMatrix, flipForwardQuaternion } from "./internal.js";
11
- import type { NeedleXRHitTestResult, NeedleXRSession } from "./NeedleXRSession.js";
12
-
13
- const debug = getParam("debugwebxr");
14
-
15
- // https://github.com/immersive-web/webxr-input-profiles/blob/4484a05e30bcd43fe86bb4e06b7a707861a26796/packages/registry/profiles/meta/meta-quest-touch-plus.json
16
- declare type ControllerAxes = "xr-standard-thumbstick";
17
- declare type StickName = "xr-standard-thumbstick";
18
- declare type Mapping = "xr-standard";
19
- declare type ComponentType = "button" | "thumbstick" | "squeeze";
20
- declare type GamepadKey = "button" | "xAxis" | "yAxis";
21
-
22
-
23
- declare type ComponentMap = {
24
- type: ComponentType,
25
- rootNodeName?: string,
26
- gamepadIndices?: { [key in GamepadKey]?: number },
27
- visualResponses?: { [key: string]: { states: Array<string> } }
28
- }
29
-
30
- declare type InputDeviceLayout = {
31
- selectComponentId: string,
32
- components: { [key: string]: ComponentMap }
33
- mapping: Mapping;
34
- gamepad: Array<XRControllerButtonName>,
35
- axes: Array<{
36
- componentId: ControllerAxes,
37
- axis: "x-axis" | "y-axis",
38
- }>,
39
- }
40
- declare type InputDeviceProfile = {
41
- profileId: string,
42
- fallbackProfileIds: string[],
43
- layouts: [
44
- left: InputDeviceLayout,
45
- right: InputDeviceLayout
46
- ]
47
- }
48
-
49
- // https://github.com/immersive-web/webxr-input-profiles/blob/4484a05e30bcd43fe86bb4e06b7a707861a26796/packages/registry/profiles/meta/meta-quest-touch-plus.json
50
- const DEFAULT_PROFILES_PATH = 'https://cdn.jsdelivr.net/npm/@webxr-input-profiles/[email protected]/dist/profiles';
51
- const DEFAULT_PROFILE = 'generic-trigger';
52
-
53
-
54
- /**
55
- * A NeedleXRController wraps a connected XRInputDevice that is either a physical controller or a hand
56
- * You can access specific buttons using `getButton` and `getStick`
57
- * To get spatial data in rig space (position, rotation) use the `gripPosition`, `gripQuaternion`, `rayPosition` and `rayQuaternion` properties
58
- * To get spatial data in world space use the `gripWorldPosition`, `gripWorldQuaternion`, `rayWorldPosition` and `rayWorldQuaternion` properties
59
- * Inputs will also be emitted as pointer events on `this.context.input` - so you can receive controller inputs on objects using the appropriate input events on your components (e.g. `onPointerDown`, `onPointerUp` etc) - use the `pointerType` property to check if the event is from a controller or not
60
- * @link https://developer.mozilla.org/en-US/docs/Web/API/XRInputSource
61
- */
62
- export class NeedleXRController {
63
- /** the Needle XR Session */
64
- readonly xr: NeedleXRSession;
65
- /**
66
- * https://developer.mozilla.org/en-US/docs/Web/API/XRInputSource
67
- */
68
- readonly inputSource: XRInputSource;
69
- /** the input source index */
70
- readonly index: number = 0;
71
-
72
- /** When enabled the controller will create input events in the Needle Engine input system (e.g. when a button is pressed or the controller is moved)
73
- * You can disable this if you don't want inputs to go through the input system but be aware that this will result in `onPointerDown` component callbacks to not be invoked anymore for this XRController
74
- */
75
- emitEvents = true;
76
-
77
- // EXPOSE API
78
- /**
79
- * Is the controller still connected?
80
- */
81
- get connected() { return this.inputSource.gamepad?.connected ?? false; }
82
- get isTracking() { return this._isTracking; }
83
- private _isTracking: boolean = false;
84
- /** the input source gamepad giving raw access to the gamepad values
85
- * You should usually use the `getButton` and `getStick` methods instead to get access to named buttons and sticks
86
- */
87
- get gamepad() { return this.inputSource.gamepad; }
88
- /**
89
- * If this is a hand then this is the hand info (XRHand)
90
- * @link https://developer.mozilla.org/en-US/docs/Web/API/XRHand
91
- */
92
- get hand() { return this.inputSource.hand; }
93
- /** The input source profiles */
94
- get profiles() { return this.inputSource.profiles; }
95
- /** The device input layout */
96
- get layout() { return this._layout; }
97
-
98
- /** shorthand for `inputSource.targetRayMode` */
99
- get targetRayMode() { return this.inputSource.targetRayMode; }
100
- /** shorthand for `inputSource.targetRaySpace` */
101
- get targetRaySpace() { return this.inputSource.targetRaySpace; }
102
- /** shorthand for `inputSource.gripSpace` */
103
- get gripSpace() { return this.inputSource.gripSpace; }
104
- /**
105
- * If the controller if held in the left or right hand (or if it's a left or right hand)
106
- **/
107
- get side() { return this.inputSource.handedness; }
108
- /** is right side. shorthand for `side === 'right'` */
109
- get isRight() { return this.side === 'right'; }
110
- /** is left side. shorthand for `side === 'left'` */
111
- get isLeft() { return this.side === 'left'; }
112
-
113
- /** The XRTransientInputHitTestSource can be used to perform hit tests with the controller ray against the real world.
114
- * see https://developer.mozilla.org/en-US/docs/Web/API/XRSession/requestHitTestSourceForTransientInput for more information
115
- * Requires the hit-test feature to be enabled in the XRSession
116
- */
117
- get hitTestSource() { return this._hitTestSource; }
118
- private _hitTestSource: XRTransientInputHitTestSource | undefined = undefined;
119
-
120
- /** Perform a hit test against the XR planes or meshes. shorthand for `xr.getHitTest(controller)`
121
- * @returns the hit test result (with position and rotation in worldspace) or null if no hit was found
122
- */
123
- getHitTest(): NeedleXRHitTestResult | null {
124
- return this.xr.getHitTest(this);
125
- }
126
-
127
- private readonly _gripPosition = new Vector3();
128
- private readonly _gripQuaternion = new Quaternion();
129
- private readonly _linearVelocity: Vector3 = new Vector3();
130
- private readonly _rayPosition = new Vector3();
131
- private readonly _rayQuaternion = new Quaternion();
132
-
133
- /** Grip position in rig space */
134
- get gripPosition() { return getTempVector(this._gripPosition).applyMatrix4(flipForwardMatrix) }
135
- /** Grip rotation in rig space */
136
- get gripQuaternion() { return getTempQuaternion(this._gripQuaternion).premultiply(flipForwardQuaternion) }
137
- /** Grip linear velocity in rig space
138
- * @link https://developer.mozilla.org/en-US/docs/Web/API/XRPose/linearVelocity
139
- */
140
- get gripLinearVelocity() {
141
- return getTempVector(this._linearVelocity).applyQuaternion(flipForwardQuaternion);
142
- }
143
- /** Ray position in rig space */
144
- get rayPosition() { return getTempVector(this._rayPosition).applyMatrix4(flipForwardMatrix) }
145
- /** Ray rotation in rig space */
146
- get rayQuaternion() { return getTempQuaternion(this._rayQuaternion).premultiply(flipForwardQuaternion) }
147
-
148
- /** Controller grip position in worldspace */
149
- get gripWorldPosition() {
150
- return getTempVector(this._gripWorldPosition);
151
- }
152
- private readonly _gripWorldPosition: Vector3 = new Vector3();
153
-
154
- /** Controller grip rotation in wordspace */
155
- get gripWorldQuaternion() {
156
- return getTempQuaternion(this._gripWorldQuaternion);
157
- }
158
- private readonly _gripWorldQuaternion: Quaternion = new Quaternion();
159
-
160
- /** Controller ray position in worldspace */
161
- get rayWorldPosition() {
162
- return getTempVector(this._rayWorldPosition);
163
- }
164
- private readonly _rayWorldPosition: Vector3 = new Vector3();
165
-
166
- /** Controller ray rotation in wordspace */
167
- get rayWorldQuaternion() {
168
- return getTempQuaternion(this._rayWorldQuaternion);
169
- }
170
- private readonly _rayWorldQuaternion: Quaternion = new Quaternion();
171
-
172
- /** The controller ray in worldspace */
173
- get ray(): Ray {
174
- this._ray.origin.copy(this.rayWorldPosition);
175
- this._ray.direction.copy(getTempVector(0, 0, 1).applyQuaternion(this.rayWorldQuaternion));
176
- return this._ray;
177
- }
178
- private readonly _ray;
179
-
180
-
181
- /** The controller object space.
182
- * You can use it to attach objects to the controller.
183
- * Children will be automatically detached and put into the scene when the controller disconnects
184
- */
185
- get object() { return this._object; }
186
- private readonly _object: IGameObject;
187
-
188
- private readonly _debugAxesHelper = new AxesHelper(.03);
189
-
190
- /** returns the URL of the default controller model */
191
- async getModelUrl(): Promise<string | null> {
192
- return this.getMotionController?.then(res => res.assetUrl || null);
193
- }
194
-
195
- constructor(session: NeedleXRSession, device: XRInputSource, index: number) {
196
- this.xr = session;
197
- this.inputSource = device;
198
- this.index = index;
199
- this._object = new Object3D() as unknown as IGameObject;
200
- if (debug)
201
- this._object.add(this._debugAxesHelper);
202
- this.xr.context.scene.add(this._object);
203
- this._ray = new Ray();
204
- this.pointerInit = {
205
- origin: this,
206
- pointerType: this.hand ? "hand" : "controller",
207
- deviceIndex: this.index,
208
- pointerId: -1, // < this will be updated in the emitPointerEvent method
209
- mode: this.inputSource.targetRayMode,
210
- ray: this._ray,
211
- device: this._object,
212
- buttonName: "none",
213
- }
214
- this.initialize();
215
- this.subscribeEvents();
216
-
217
- // TODO: change this to check if we have hit-testing enabled instead of pass through.
218
- if (this.xr.mode === "immersive-ar" && this.inputSource.targetRayMode === "tracked-pointer") {
219
- // request hittest source
220
- this.xr.session.requestHitTestSourceForTransientInput?.({
221
- profile: this.inputSource.profiles[0],
222
- offsetRay: new XRRay(),
223
- })?.then(hitTestSource => {
224
- return this._hitTestSource = hitTestSource;
225
- });
226
- }
227
- }
228
-
229
- onUpdate(frame: XRFrame) {
230
- this.onUpdateFrame(frame);
231
- this.updateInputEvents();
232
- this.onUpdateMove();
233
- }
234
-
235
- onRenderDebug() {
236
- Gizmos.DrawWireSphere(this.rayWorldPosition, .02);
237
- Gizmos.DrawDirection(this.rayWorldPosition, getTempVector(0, 0, 10).applyQuaternion(this.rayWorldQuaternion));
238
- }
239
-
240
- private onUpdateFrame(frame: XRFrame) {
241
- if (!this.xr.referenceSpace) {
242
- this._isTracking = false;
243
- return;
244
- }
245
-
246
- const rayPose = frame.getPose(this.inputSource.targetRaySpace, this.xr.referenceSpace);
247
- this._isTracking = rayPose != null;
248
-
249
- if (rayPose) {
250
- const t = rayPose.transform;
251
- this._rayPosition.set(t.position.x, t.position.y, t.position.z);
252
- this._rayQuaternion.set(t.orientation.x, t.orientation.y, t.orientation.z, t.orientation.w);
253
- }
254
-
255
- if (this.inputSource.gripSpace) {
256
- const gripPose = frame.getPose(this.inputSource.gripSpace, this.xr.referenceSpace!);
257
- if (gripPose) {
258
- const t = gripPose.transform;
259
- this._gripPosition.set(t.position.x, t.position.y, t.position.z);
260
- this._gripQuaternion.set(t.orientation.x, t.orientation.y, t.orientation.z, t.orientation.w);
261
- if (gripPose.linearVelocity)
262
- this._linearVelocity.set(gripPose.linearVelocity.x, gripPose.linearVelocity.y, gripPose.linearVelocity.z);
263
- }
264
- }
265
-
266
- // update controller object position
267
- if (this.xr.context.mainCamera?.parent && this._object.parent !== this.xr.context.mainCamera?.parent)
268
- this.xr.context.mainCamera.parent.add(this._object);
269
-
270
- // for controllers, we set the position and rotation of the object to the ray position and rotation
271
- // for hands, we take the wrist position and rotation
272
- const hand = this.hand;
273
- if (hand) {
274
- // https://www.w3.org/TR/webxr-hand-input-1/#xrhand-interface
275
- let gotWrist = false;
276
- // TODO check why types are not correct here
277
- // @ts-ignore
278
- const wrist = hand.get("wrist");
279
- if (wrist && frame.getJointPose) {
280
- const pose = frame.getJointPose(wrist, this.xr.referenceSpace);
281
- if (pose) {
282
- gotWrist = true;
283
- const p = pose.transform.position;
284
- const q = pose.transform.orientation;
285
- this._object.position.set(p.x, p.y, p.z);
286
- this._object.quaternion.set(q.x, q.y, q.z, q.w).multiply(flipForwardQuaternion);
287
- }
288
- }
289
- if (!gotWrist) {
290
- this._object.position.copy(this._rayPosition);
291
- this._object.quaternion.copy(this._rayQuaternion).multiply(flipForwardQuaternion);
292
- }
293
-
294
- //@ts-ignore
295
- const middle = hand.get("middle-finger-metacarpal");
296
- if (middle && frame.getJointPose) {
297
- const pose = frame.getJointPose(middle, this.xr.referenceSpace);
298
- if (pose) {
299
- const p = pose.transform.position;
300
- const q = pose.transform.orientation;
301
- // for some reason the grip rotation is different from the wrist rotation
302
- // but we want to use the wrist rotation for the grip
303
- this._gripPosition.set(p.x, p.y, p.z);
304
- this._gripQuaternion.set(q.x, q.y, q.z, q.w);
305
- }
306
- }
307
- }
308
- else {
309
- this._object.position.copy(this._rayPosition);
310
- this._object.quaternion.copy(this._rayQuaternion).multiply(flipForwardQuaternion);
311
- }
312
-
313
-
314
- // UPDATE WORLD TRANSFORM DATA
315
- const parent = this.xr.context.mainCamera?.parent;
316
- const parentWorldQuaternion = parent ? getWorldQuaternion(parent) : undefined;
317
-
318
- // GRIP
319
- this._gripWorldPosition.copy(this._gripPosition);
320
- if (parent) this._gripWorldPosition.applyMatrix4(parent.matrixWorld);
321
- this._gripWorldQuaternion.copy(this._gripQuaternion);
322
- // flip forward because we want +Z to be forward
323
- this._gripWorldQuaternion.multiply(flipForwardQuaternion);
324
- if (parentWorldQuaternion) this._gripWorldQuaternion.premultiply(parentWorldQuaternion)
325
-
326
- // RAY
327
- this._rayWorldPosition.copy(this._rayPosition);
328
- if (parent) this._rayWorldPosition.applyMatrix4(parent.matrixWorld);
329
- this._rayWorldQuaternion.copy(this._rayQuaternion)
330
- // flip forward because we want +Z to be forward
331
- .multiply(flipForwardQuaternion);
332
- if (parentWorldQuaternion) this._rayWorldQuaternion.premultiply(parentWorldQuaternion)
333
- }
334
-
335
- /** Called when the input source disconnects */
336
- onDisconnected() {
337
- if (this.connected) return;
338
- // move all attached objects into the scene
339
- for (const child of this._object.children) {
340
- this.xr.context.scene.attach(child);
341
- }
342
- this._object.removeFromParent();
343
- this._debugAxesHelper.removeFromParent();
344
- this.unsubscribeEvents();
345
- }
346
-
347
- /**
348
- * Get a gamepad button
349
- * @link https://github.com/immersive-web/webxr-gamepads-module/blob/main/gamepads-module-explainer.md
350
- * @param key the controller button name e.g. x-button
351
- * @returns the gamepad button if it exists on the controller - otherwise undefined
352
- */
353
- getButton(key: ButtonName | "primary-button" | "primary"): NeedleGamepadButton | undefined {
354
- if (!this._layout) return undefined;
355
-
356
- switch (key) {
357
- case "primary-button":
358
- if (this.isLeft) key = "x-button";
359
- else if (this.isRight) key = "a-button";
360
- else return undefined;
361
- break;
362
- case "primary":
363
- return this.toNeedleGamepadButton(0);
364
- }
365
-
366
-
367
- if (this._buttonMap.has(key)) {
368
- return this.toNeedleGamepadButton(this._buttonMap.get(key)!);
369
- }
370
- const componentModel = this._layout?.components[key];
371
- if (componentModel?.gamepadIndices) {
372
- switch (componentModel.type) {
373
- case "button":
374
- case "squeeze":
375
- if (this.inputSource.gamepad) {
376
- const index = componentModel.gamepadIndices!.button!;
377
- this._buttonMap.set(key, index);
378
- return this.toNeedleGamepadButton(index);
379
- }
380
- break;
381
- default:
382
- console.warn("Unsupported component type", componentModel.type);
383
- break;
384
- }
385
- }
386
- this._buttonMap.set(key, undefined!);
387
- return undefined;
388
- }
389
-
390
- private readonly _needleGamepadButtons = new Array<NeedleGamepadButton>();
391
- /** combine the InputState information + the GamepadButton information (since GamepadButtons can not be extended) */
392
- private toNeedleGamepadButton(index: number): NeedleGamepadButton {
393
- const button = this.inputSource.gamepad?.buttons[index];
394
- const state = this.states[index];
395
- const needleButton = this._needleGamepadButtons[index] || new NeedleGamepadButton();
396
- if (button) {
397
- needleButton.pressed = button.pressed;
398
- needleButton.value = button.value;
399
- needleButton.touched = button.touched;
400
- }
401
- if (state) {
402
- needleButton.isDown = state.isDown;
403
- needleButton.isUp = state.isUp;
404
- }
405
- this._needleGamepadButtons[index] = needleButton;
406
- return needleButton;
407
- }
408
-
409
- /**
410
- * Get the values of a controller joystick
411
- * @link https://github.com/immersive-web/webxr-gamepads-module/blob/main/gamepads-module-explainer.md
412
- * @returns the stick values where x is left/right, y is up/down and z is the button value
413
- */
414
- getStick(key: StickName | "primary"): Vec3 {
415
- if (!this._layout) return { x: 0, y: 0, z: 0 };
416
-
417
- if (key === "primary") {
418
- const x = this.inputSource.gamepad?.axes[0] || 0;
419
- const y = this.inputSource.gamepad?.axes[1] || 0;
420
- // the primary thumbstick is button 3 (see gamepads module explainer)
421
- const z = this.inputSource.gamepad?.buttons[3].value || 0;
422
- return { x, y, z }
423
- }
424
-
425
- const componentModel = this._layout?.components[key];
426
- if (componentModel?.gamepadIndices) {
427
- switch (componentModel.type) {
428
- case "thumbstick":
429
- if (this.inputSource.gamepad) {
430
- const xIndex = componentModel.gamepadIndices!.xAxis!;
431
- const yIndex = componentModel.gamepadIndices!.yAxis!;
432
- let x = this.inputSource.gamepad?.axes[xIndex];
433
- let y = this.inputSource.gamepad?.axes[yIndex];
434
- x *= -1;
435
- y *= -1;
436
- const buttonIndex = componentModel.gamepadIndices!.button!;
437
- const z = this.inputSource.gamepad?.buttons[buttonIndex].value;
438
- return { x, y, z }
439
- }
440
- }
441
- }
442
- return { x: 0, y: 0, z: 0 }
443
- }
444
-
445
-
446
- private readonly _buttonMap = new Map<ButtonName, number>();
447
-
448
- // the motion controller contains the controller scheme, we use this to simplify button access
449
- private _motioncontroller?: MotionController;
450
- private _layout: InputDeviceLayout | undefined;
451
- private getMotionController!: Promise<MotionController>;
452
- private initialize() {
453
- if (!this._layout) {
454
- // TODO: we should fetch the profiles or better yet the profile list once and cache it
455
- const fetchProfileCall = fetchProfile(this.inputSource, DEFAULT_PROFILES_PATH, DEFAULT_PROFILE);
456
- /** @ts-ignore */
457
- this.getMotionController = fetchProfileCall.then(res => {
458
-
459
- if (!this.connected) return null;
460
-
461
- this._motioncontroller = new MotionController(
462
- this.inputSource,
463
- res.profile,
464
- res.assetPath || ""
465
- );
466
-
467
- const profile = res.profile as InputDeviceProfile;
468
- const layout = profile.layouts[this.inputSource.handedness];
469
- this._layout = layout;
470
- if (this._layout) {
471
- if (!this._layout.gamepad?.length) {
472
- this._layout.gamepad = [];
473
- for (const key in this._layout.components) {
474
- const component = this._layout.components[key];
475
- this._layout.gamepad[component.gamepadIndices!.button!] = key as XRControllerButtonName;
476
- }
477
- }
478
- }
479
- // if (debug) console.log(this._layout, this.inputSource);
480
- // debugger;
481
- // this.getButton("a-button")
482
- return this._motioncontroller;
483
- }).catch(err => {
484
- console.error(err);
485
- });
486
- }
487
- }
488
-
489
- private subscribeEvents() {
490
- // https://developer.mozilla.org/en-US/docs/Web/API/XRSession/selectstart_event
491
- this.xr.session.addEventListener("selectstart", this.onSelectStart);
492
- this.xr.session.addEventListener("selectend", this.onSelectEnd);
493
- // https://developer.mozilla.org/en-US/docs/Web/API/XRSession/squeeze_event
494
- this.xr.session.addEventListener("squeezestart", this.onSequeezeStart);
495
- this.xr.session.addEventListener("squeezeend", this.onSequeezeEnd);
496
- }
497
- private unsubscribeEvents() {
498
- this.xr.session.removeEventListener("selectstart", this.onSelectStart);
499
- this.xr.session.removeEventListener("selectend", this.onSelectEnd);
500
- this.xr.session.removeEventListener("squeezestart", this.onSequeezeStart);
501
- this.xr.session.removeEventListener("squeezeend", this.onSequeezeEnd);
502
- }
503
-
504
- private _selectButtonIndex: number | undefined = undefined;
505
- private _squeezeButtonIndex: number | undefined = undefined;
506
-
507
- private onSelectStart = (evt: XRInputSourceEvent) => {
508
- if (this.inputSource !== evt.inputSource) return;
509
- const selectComponentId = this._layout?.selectComponentId;
510
- const i = this._layout?.components[selectComponentId!]?.gamepadIndices?.button;
511
- if (i !== undefined) this._selectButtonIndex = i;
512
- if (debug) Gizmos.DrawDirection(this.rayWorldPosition, getTempVector(0, .01, 1).applyQuaternion(this.rayWorldQuaternion), 0xff0000, 10);
513
- this.emitPointerEvent(InputEvents.PointerDown, this._selectButtonIndex || 0, "xr-standard-trigger", true, evt);
514
- }
515
- private onSelectEnd = (evt: XRInputSourceEvent) => {
516
- if (this.inputSource !== evt.inputSource) return;
517
- this.emitPointerEvent(InputEvents.PointerUp, this._selectButtonIndex || 0, "xr-standard-trigger", true, evt);
518
- }
519
- private onSequeezeStart = (evt: XRInputSourceEvent) => {
520
- if (this.inputSource !== evt.inputSource) return;
521
- this._squeezeButtonIndex = this._layout?.components["xr-standard-squeeze"]?.gamepadIndices?.button;
522
- if (this._squeezeButtonIndex !== undefined) {
523
- if (debug) Gizmos.DrawDirection(this.rayWorldPosition, getTempVector(0, .01, 1).applyQuaternion(this.rayWorldQuaternion), 0x0000ff, 10);
524
- this.emitPointerEvent(InputEvents.PointerDown, this._squeezeButtonIndex || 0, "xr-standard-squeeze", true, evt);
525
- }
526
- };
527
- private onSequeezeEnd = (evt: XRInputSourceEvent) => {
528
- if (this.inputSource !== evt.inputSource) return;
529
- if (this._squeezeButtonIndex !== undefined)
530
- this.emitPointerEvent(InputEvents.PointerUp, this._squeezeButtonIndex || 0, "xr-standard-squeeze", true, evt);
531
- };
532
-
533
- /** Index = button index */
534
- private readonly states = new Array<InputState>();
535
- // If we want to invoke button events for ALL buttons we need to keep track of the previous state
536
- // instead of using XR input select start events which is only raised for the primary button
537
- // we should probably do both but then we need to ignore the primary index in the following function (to not raise an event for the same button twice)
538
- // and start with index = 1
539
- private updateInputEvents() {
540
- if (!this._layout) return;
541
-
542
- // https://immersive-web.github.io/webxr-gamepads-module/#xr-standard-heading
543
- if (this.gamepad?.buttons) {
544
- for (let k = 0; k < this.gamepad.buttons.length; k++) {
545
- const button = this.gamepad.buttons[k];
546
- const state = this.states[k] || new InputState();
547
- let eventName: InputEventNames | null = null;
548
-
549
- // is down
550
- if (button.pressed && !state.pressed) {
551
- eventName = "pointerdown";
552
- state.isDown = true;
553
- state.isUp = false;
554
- }
555
- // is up
556
- else if (!button.pressed && state.pressed) {
557
- eventName = "pointerup"
558
- state.isDown = false;
559
- state.isUp = true;
560
- }
561
- else {
562
- state.isDown = false;
563
- state.isUp = false;
564
- }
565
-
566
- state.value = button.value;
567
- state.pressed = button.pressed;
568
- this.states[k] = state;
569
-
570
- // the selection event is handled in the "selectstart" callback
571
- const emitEvent = k !== this._selectButtonIndex && k !== this._squeezeButtonIndex;
572
-
573
- if (eventName != null && emitEvent) {
574
- const name = this._layout?.gamepad[k];
575
- this.emitPointerEvent(eventName, k, name ?? "none", false, null, button.value);
576
- }
577
- }
578
- }
579
- }
580
- private onUpdateMove() {
581
- let button = this.xr.context.input.getFirstPressedButtonForPointer(this.index);
582
- if (button === undefined) button = 0;
583
- const pressure = this.gamepad?.buttons[button]?.value;
584
- this.emitPointerEvent("pointermove", button, "none", false, null, pressure);
585
- }
586
-
587
-
588
- /** cached spatial pointer init object. We re-use it to not have */
589
- private readonly pointerInit: NEPointerEventInit;
590
- private emitPointerEvent(type: InputEventNames, button: number, buttonName: ButtonName | "none", primary: boolean, source: Event | null = null, pressure?: number) {
591
-
592
- if (!this.emitEvents) {
593
- if (debug && type !== InputEvents.PointerMove) console.warn("Pointer events are disabled for this controller", this.index, type, button);
594
- return;
595
- }
596
-
597
- // Currently we do only want to emit pointer events for NON screen based events
598
- // that means if the input device is spatial (AR touch on a screen should be handled via touchdown etc still)
599
- // Not sure if *this* is enough to determine if the event is spatial or not
600
- if (this.xr.mode === "immersive-vr" || this.xr.isPassThrough) {
601
- this.pointerInit.origin = this;
602
- this.pointerInit.pointerId = this.index * 10 + button;
603
- this.pointerInit.pointerType = this.hand ? "hand" : "controller";
604
- this.pointerInit.button = button;
605
- this.pointerInit.buttonName = buttonName;
606
- this.pointerInit.isPrimary = primary;
607
- this.pointerInit.mode = this.inputSource.targetRayMode;
608
- this.pointerInit.ray = this.ray;
609
- this.pointerInit.device = this.object;
610
- this.pointerInit.pressure = pressure;
611
-
612
- const prevContext = Context.Current;
613
- Context.Current = this.xr.context;
614
- this.xr.context.input.createInputEvent(new NEPointerEvent(type, source, this.pointerInit));
615
- Context.Current = prevContext;
616
- }
617
- }
618
- }
619
-
620
- class InputState {
621
- /** if the button was pressed the last update */
622
- isDown: boolean = false;
623
- /** if the button was released the last update */
624
- isUp: boolean = false;
625
-
626
- pressed: boolean = false;
627
- value: number = 0;
628
- };
629
-
630
- /** Enhanced GamepadButton with `isDown` and `isUp` information */
631
- class NeedleGamepadButton {
632
- touched: boolean = false;
633
- pressed: boolean = false;
634
- value: number = 0;
635
- /** was the button just pressed down the last update */
636
- isDown: boolean = false;
637
- /** was the button just released the last update */
638
- isUp: boolean = false;
639
- }
src/engine/xr/NeedleXRSession.ts DELETED
@@ -1,1247 +0,0 @@
1
- import { Camera, DoubleSide, Mesh, MeshBasicMaterial, Object3D, PerspectiveCamera, PlaneGeometry, PlaneHelper, Quaternion, Vector3, WebXRArrayCamera } from "three";
2
-
3
- import { isDevEnvironment, showBalloonMessage, showBalloonWarning } from "../debug/index.js";
4
- import { Context, FrameEvent } from "../engine_context.js";
5
- import { ContextEvent, ContextRegistry } from "../engine_context_registry.js";
6
- import { isDestroyed } from "../engine_gameobject.js";
7
- import { Gizmos } from "../engine_gizmos.js";
8
- import { registerFrameEventCallback, unregisterFrameEventCallback } from "../engine_lifecycle_functions_internal.js";
9
- import { Mathf } from "../engine_math.js";
10
- import { getTempQuaternion, getTempVector, getWorldPosition, getWorldQuaternion, getWorldScale, setWorldPosition, setWorldQuaternion, setWorldScale } from "../engine_three_utils.js";
11
- import { ICamera, IComponent, INeedleXRSession } from "../engine_types.js";
12
- import { delay, getParam, isDesktop, isQuest } from "../engine_utils.js";
13
- import { flipForwardMatrix, flipForwardQuaternion, ImplictXRRig } from "./internal.js";
14
- import { NeedleXRController } from "./NeedleXRController.js";
15
- import { NeedleXRSync } from "./NeedleXRSync.js";
16
- import { SceneTransition } from "./SceneTransition.js";
17
- import { TemporaryXRContext } from "./TempXRContext.js";
18
- import type { IXRRig } from "./XRRig.js";
19
-
20
- /** NeedleXRSession event argument.
21
- * Use `args.xr` to access the NeedleXRSession */
22
- export type NeedleXREventArgs = { xr: NeedleXRSession }
23
- export type SessionChangedEvt = (args: NeedleXREventArgs) => void;
24
- export type SessionRequestedEvent = (args: { mode: XRSessionMode, init: XRSessionInit }) => void;
25
- export type SessionRequestedEndEvent = (args: { mode: XRSessionMode, init: XRSessionInit, newSession: XRSession | null }) => void;
26
-
27
- /** Result of a XR hit-test
28
- * @property {XRHitTestResult} hit The original XRHitTestResult
29
- * @property {Vector3} position The hit position in world space
30
- * @property {Quaternion} quaternion The hit rotation in world space
31
- */
32
- export type NeedleXRHitTestResult = { hit: XRHitTestResult, position: Vector3, quaternion: Quaternion };
33
-
34
- const debug = getParam("debugwebxr");
35
- const debugFPS = getParam("stats");
36
-
37
- // TODO: move this into the IComponent interface!?
38
- export interface INeedleXRSessionEventReceiver extends Pick<IComponent, "destroyed"> {
39
- get activeAndEnabled(): boolean;
40
- supportsXR?(mode: XRSessionMode): boolean;
41
- /** Called before requesting a XR session */
42
- onBeforeXR?(mode: XRSessionMode, args: XRSessionInit): void;
43
- onEnterXR?(args: NeedleXREventArgs): void;
44
- onUpdateXR?(args: NeedleXREventArgs): void;
45
- onLeaveXR?(args: NeedleXREventArgs): void;
46
- onXRControllerAdded?(args: NeedleXRControllerEventArgs): void;
47
- onXRControllerRemoved?(args: NeedleXRControllerEventArgs): void;
48
- }
49
-
50
- /** Contains a reference to the currently active webxr session and the controller that has changed */
51
- export type NeedleXRControllerEventArgs = NeedleXREventArgs & { controller: NeedleXRController, change: "added" | "removed" }
52
- /** Event Arguments when a controller changed event is invoked (added or removed)
53
- * Access the controller via `args.controller`, the `args.change` property indicates if the controller was added or removed
54
- */
55
- export type ControllerChangedEvt = (args: NeedleXRControllerEventArgs) => void;
56
-
57
-
58
-
59
- function getDOMOverlayElement(domElement: HTMLElement) {
60
- let arOverlayElement: HTMLElement | null = null;
61
- // for react cases we dont have an Engine Element
62
- const element: any = domElement;
63
- if (element.getAROverlayContainer)
64
- arOverlayElement = element.getAROverlayContainer();
65
- else arOverlayElement = domElement;
66
- return arOverlayElement;
67
- }
68
-
69
-
70
-
71
- registerSessionGranted();
72
- function registerSessionGranted() {
73
- if ('xr' in navigator) {
74
- // WebXRViewer (based on Firefox) has a bug where addEventListener
75
- // throws a silent exception and aborts execution entirely.
76
- if (/WebXRViewer\//i.test(navigator.userAgent)) {
77
- console.warn('WebXRViewer does not support addEventListener');
78
- return;
79
- }
80
-
81
- navigator.xr?.addEventListener('sessiongranted', () => {
82
- console.log("Received Session Granted...")
83
- const lastSessionMode = sessionStorage.getItem("needle_xr_session_mode");
84
- const lastSessionInit = sessionStorage.getItem("needle_xr_session_init");
85
- if (lastSessionMode && lastSessionInit) {
86
- console.log("Session Granted: Restore last session")
87
- const init = JSON.parse(lastSessionInit);
88
- NeedleXRSession.start(lastSessionMode as XRSessionMode, init).catch(e => console.warn(e));
89
- }
90
- else {
91
- // if no session was found we start VR by default
92
- NeedleXRSession.start("immersive-vr").catch(e => console.warn("Session Granted failed:", e));
93
- }
94
- });
95
- }
96
- }
97
- function saveSessionInfo(mode: XRSessionMode, init: XRSessionInit) {
98
- sessionStorage.setItem("needle_xr_session_mode", mode);
99
- sessionStorage.setItem("needle_xr_session_init", JSON.stringify(init));
100
- }
101
-
102
- function deleteSessionInfo() {
103
- sessionStorage.removeItem("needle_xr_session_mode");
104
- sessionStorage.removeItem("needle_xr_session_init");
105
- }
106
-
107
- if (isDesktop() && isDevEnvironment()) {
108
- window.addEventListener("keydown", (evt) => {
109
- if (evt.key === "x") {
110
- if (NeedleXRSession.active) {
111
- NeedleXRSession.stop();
112
- }
113
- }
114
- });
115
- }
116
-
117
- if (getParam("simulatewebxrloading")) {
118
- ContextRegistry.registerCallback(ContextEvent.ContextCreationStart, async _cb => {
119
- await delay(3000);
120
- setTimeout(async () => {
121
- const info = await TemporaryXRContext.handoff();
122
- if (info) NeedleXRSession.setSession(info.mode, info.session, info.init, Context.Current);
123
- else
124
- NeedleXRSession.start("immersive-vr")
125
- }, 6000)
126
- });
127
- let triggered = false;
128
- window.addEventListener("click", () => {
129
- if (triggered) return;
130
- triggered = true;
131
- TemporaryXRContext.start("immersive-vr", NeedleXRSession.getDefaultSessionInit("immersive-vr"));
132
- });
133
- }
134
-
135
- /**
136
- * This class manages an XRSession to provide helper methods and events. It provides easy access to the XRInputSources (controllers and hands)
137
- * - Start a XRSession with `NeedleXRSession.start(...)`
138
- * - Stop a XRSession with `NeedleXRSession.stop()`
139
- * - Access a running XRSession with `NeedleXRSession.active`
140
- *
141
- * If a XRSession is active you can use all XR-related event methods on your components to receive XR events e.g. `onEnterXR`, `onUpdateXR`, `onLeaveXR`
142
- * ```ts
143
- * export class MyComponent extends Behaviour {
144
- * // callback invoked whenever the XRSession is started or your component is added to a scene with an active XRSession
145
- * onEnterXR(args: NeedleXREventArgs) {
146
- * console.log("Entered XR");
147
- * // access the NeedleXRSession via args.xr
148
- * }
149
- * // callback invoked whenever a controller is added (or you switch from controller to hand tracking)
150
- * onControllerAdded(args: NeedleXRControllerEventArgs) { }
151
- * }
152
- * ```
153
- *
154
- * ### XRRig
155
- * The XRRig can be accessed via the `rig` property
156
- * Set a custom XRRig via `NeedleXRSession.addRig(...)` or `NeedleXRSession.removeRig(...)`
157
- * By default the active XRRig with the highest priority in the scene is used
158
- */
159
- export class NeedleXRSession implements INeedleXRSession {
160
-
161
- private static _sync: NeedleXRSync | null = null;
162
- static getXRSync(context: Context) {
163
- if (!this._sync) this._sync = new NeedleXRSync(context);
164
- return this._sync;
165
- }
166
-
167
- static get currentSessionRequest(): XRSessionMode | null { return this._currentSessionRequestMode; }
168
- private static _currentSessionRequestMode: XRSessionMode | null = null;
169
-
170
- static get active(): NeedleXRSession | null { return this._activeSession; }
171
- /** The active xr session mode (if any xr session is active) */
172
- static get activeMode() { return this._activeSession?.mode ?? null; }
173
- /** XRSystem via navigator.xr access
174
- * @link https://developer.mozilla.org/en-US/docs/Web/API/XRSystem
175
- */
176
- static get xrSystem(): XRSystem | undefined {
177
- return ('xr' in navigator) ? navigator.xr : undefined;
178
- }
179
- static isXRSupported() { return Promise.all([this.isVRSupported(), this.isARSupported()]).then(res => res.some(e => e)) }
180
- static isVRSupported() { return this.xrSystem?.isSessionSupported('immersive-vr') ?? Promise.resolve(false); }
181
- static isARSupported() { return this.xrSystem?.isSessionSupported('immersive-ar') ?? Promise.resolve(false); }
182
-
183
- private static _currentSessionRequest?: Promise<XRSession>;
184
- private static _activeSession: NeedleXRSession | null;
185
-
186
- static onSessionRequestStart(evt: SessionRequestedEvent) {
187
- this._sessionRequestStartListeners.push(evt);
188
- }
189
- static offSessionRequestStart(evt: SessionRequestedEvent) {
190
- const index = this._sessionRequestStartListeners.indexOf(evt);
191
- if (index >= 0) this._sessionRequestStartListeners.splice(index, 1);
192
- }
193
- private static readonly _sessionRequestStartListeners: SessionRequestedEvent[] = [];
194
-
195
- /** Called after the session request has finished */
196
- static onSessionRequestEnd(evt: SessionRequestedEndEvent) {
197
- this._sessionRequestEndListeners.push(evt);
198
- }
199
- /** Unsubscribe from request end evt */
200
- static offSessionRequestEnd(evt: SessionRequestedEndEvent) {
201
- const index = this._sessionRequestEndListeners.indexOf(evt);
202
- if (index >= 0) this._sessionRequestEndListeners.splice(index, 1);
203
- }
204
- private static readonly _sessionRequestEndListeners: SessionRequestedEndEvent[] = [];
205
-
206
- /** Listen to XR session started */
207
- static onXRStart(evt: SessionChangedEvt) {
208
- this._xrStartListeners.push(evt);
209
- };
210
- /** Unsubscribe from XRSession started events */
211
- static offXRStart(evt: SessionChangedEvt) {
212
- const index = this._xrStartListeners.indexOf(evt);
213
- if (index >= 0) this._xrStartListeners.splice(index, 1);
214
- }
215
- private static readonly _xrStartListeners: SessionChangedEvt[] = [];
216
-
217
- /** Listen to controller added events.
218
- * Events are cleared when starting a new session
219
- **/
220
- static onControllerAdded(evt: ControllerChangedEvt) {
221
- this._controllerAddedListeners.push(evt);
222
- }
223
- /** Unsubscribe from controller added evts */
224
- static offControllerAdded(evt: ControllerChangedEvt) {
225
- const index = this._controllerAddedListeners.indexOf(evt);
226
- if (index >= 0) this._controllerAddedListeners.splice(index, 1);
227
- }
228
- private static readonly _controllerAddedListeners: ControllerChangedEvt[] = [];
229
-
230
- /** Listen to controller removed events
231
- * Events are cleared when starting a new session
232
- **/
233
- static onControllerRemoved(evt: ControllerChangedEvt) {
234
- this._controllerRemovedListeners.push(evt);
235
- }
236
- /** Unsubscribe from controller removed events */
237
- static offControllerRemoved(evt: ControllerChangedEvt) {
238
- const index = this._controllerRemovedListeners.indexOf(evt);
239
- if (index >= 0) this._controllerRemovedListeners.splice(index, 1);
240
- }
241
- private static readonly _controllerRemovedListeners: ControllerChangedEvt[] = [];
242
-
243
- /** If the browser supports offerSession - creating a VR or AR button in the browser navigation bar */
244
- static offerSession(mode: XRSessionMode, init: XRSessionInit | "default", context: Context): boolean {
245
- if ('xr' in navigator && navigator.xr && 'offerSession' in navigator.xr) {
246
- if (typeof navigator.xr.offerSession === "function") {
247
- console.log("WebXR offerSession is available - requesting mode: " + mode);
248
- if (init == "default") {
249
- init = this.getDefaultSessionInit(mode);
250
- }
251
- navigator.xr.offerSession(mode, {
252
- ...init
253
- }).then((session) => {
254
- return NeedleXRSession.setSession(mode, session, init as XRSessionInit, context);
255
- }).catch(_ => {
256
- console.log("XRSession offer rejected (perhaps because another call to offerSession was made or a call to requestSession was made)")
257
- });
258
- }
259
- return true;
260
- }
261
- return false;
262
- }
263
-
264
- /** @returns a new XRSession init object with defaults */
265
- static getDefaultSessionInit(mode: Omit<XRSessionMode, "inline">): XRSessionInit {
266
- switch (mode) {
267
- case "immersive-ar":
268
- return {
269
- optionalFeatures: ['anchors', 'local-floor', 'hand-tracking', 'layers', 'dom-overlay', 'hit-test'],
270
- }
271
- case "immersive-vr":
272
- return {
273
- optionalFeatures: ['local-floor', 'bounded-floor', 'hand-tracking', 'high-fixed-foveation-level', 'layers'],
274
- }
275
- default:
276
- console.warn("No default session init for mode", mode);
277
- return {};
278
- }
279
- }
280
-
281
- /** start a new webXR session (make sure to stop already running sessions before calling this method)
282
- * @param mode The XRSessionMode to start (e.g. `immersive-vr` or `immersive-ar`), docs: https://developer.mozilla.org/en-US/docs/Web/API/XRSessionMode
283
- * @param init The XRSessionInit to use (optional), docs: https://developer.mozilla.org/en-US/docs/Web/API/XRSessionInit
284
- * @param context The Needle Engine context to use
285
- */
286
- static async start(mode: XRSessionMode, init?: XRSessionInit, context?: Context): Promise<NeedleXRSession | null> {
287
-
288
- if (this._currentSessionRequest) {
289
- console.warn("A XRSession is already being requested");
290
- if (debug || isDevEnvironment()) showBalloonWarning("A XRSession is already being requested");
291
- return this._currentSessionRequest.then(() => this._activeSession!);
292
- }
293
-
294
- if (this._activeSession) {
295
- console.error("A XRSession is already running");
296
- return this._activeSession;
297
- }
298
-
299
- // Make sure we have a context
300
- if (!context) context = Context.Current;
301
- if (!context) context = ContextRegistry.All[0] as Context;
302
- if (!context) throw new Error("No Needle Engine Context found");
303
-
304
- // setup session init args, make sure we have default values
305
- if (!init) init = {};
306
- switch (mode) {
307
-
308
- // Setup VR initialization parameters
309
- case "immersive-ar":
310
- {
311
- const supported = await this.xrSystem?.isSessionSupported('immersive-ar')
312
- if (supported !== true) {
313
- console.error(mode + ' is not supported by this browser.');
314
- return null;
315
- }
316
- const defaultInit: XRSessionInit = this.getDefaultSessionInit(mode);
317
- const domOverlayElement = getDOMOverlayElement(context.domElement);
318
- if (domOverlayElement && !isQuest()) { // excluding quest since dom-overlay breaks sessiongranted
319
- defaultInit.domOverlay = { root: domOverlayElement };
320
- defaultInit.optionalFeatures!.push('dom-overlay');
321
- }
322
- init = {
323
- ...defaultInit,
324
- ...init,
325
- }
326
- }
327
- break;
328
-
329
- // Setup AR initialization parameters
330
- case "immersive-vr":
331
- {
332
- const supported = await this.xrSystem?.isSessionSupported('immersive-vr')
333
- if (supported !== true) {
334
- console.error(mode + ' is not supported by this browser.');
335
- return null;
336
- }
337
- const defaultInit: XRSessionInit = this.getDefaultSessionInit(mode);
338
- init = {
339
- ...defaultInit,
340
- ...init,
341
- }
342
- }
343
- break;
344
-
345
- default:
346
- console.warn("No default session init for mode", mode);
347
- break;
348
- }
349
-
350
- // we stop a temporary session here (if any runs)
351
- await TemporaryXRContext.stop();
352
-
353
- const scripts = mode == "immersive-ar" ? context.scripts_immersive_ar : context.scripts_immersive_vr
354
-
355
- if (debug)
356
- console.log("%c" + `Requesting ${mode} session`, "font-weight:bold;", init, scripts);
357
- else
358
- console.log("%c" + `Requesting ${mode} session`, "font-weight:bold;");
359
- for (const script of scripts) {
360
- if (script.onBeforeXR) script.onBeforeXR(mode, init);
361
- }
362
- for (const listener of this._sessionRequestStartListeners) {
363
- listener({ mode, init });
364
- }
365
- if (debug) showBalloonMessage("Requesting " + mode + " session (" + Date.now() + ")");
366
- this._currentSessionRequest = navigator.xr?.requestSession(mode, init);
367
- this._currentSessionRequestMode = mode;
368
- /**@type {XRSystem} */
369
- const newSession = await (this._currentSessionRequest)?.catch(e => {
370
- console.error(e, "Code: " + e.code);
371
- if (e.code === 9) showBalloonWarning("Make sure your device has the required permissions (e.g. camera access)")
372
- console.log("If the specified XR configuration is not supported (e.g. entering AR doesnt work) - make sure you access the website on a secure connection (HTTPS) and your device has the required permissions (e.g. camera access)");
373
- const notSecure = location.protocol === 'http:';
374
- if (notSecure) showBalloonWarning("XR requires a secure connection (HTTPS)");
375
- });
376
- this._currentSessionRequest = undefined;
377
- this._currentSessionRequestMode = null;
378
- for (const listener of this._sessionRequestEndListeners) {
379
- listener({ mode, init, newSession: newSession || null });
380
- }
381
- if (!newSession) {
382
- console.warn("XR Session request was rejected");
383
- return null;
384
- }
385
- return this.setSession(mode, newSession, init, context);
386
- }
387
-
388
- static setSession(mode: XRSessionMode, session: XRSession, init: XRSessionInit, context: Context) {
389
- if (this._activeSession) {
390
- console.error("A XRSession is already running");
391
- return this._activeSession;
392
- }
393
- const scripts = mode == "immersive-ar" ? context.scripts_immersive_ar : context.scripts_immersive_vr;
394
- this._activeSession = new NeedleXRSession(mode, session, context, {
395
- scripts: scripts,
396
- controller_added: this._controllerAddedListeners,
397
- controller_removed: this._controllerRemovedListeners,
398
- init: init
399
- });
400
- session.addEventListener("end", this.onEnd);
401
- if (debug)
402
- console.log("%c" + `Started ${mode} session`, "font-weight:bold;", scripts);
403
- else
404
- console.log("%c" + `Started ${mode} session`, "font-weight:bold;");
405
- return this._activeSession;
406
- }
407
- /** stops the active XR session */
408
- static stop() {
409
- this._activeSession?.end();
410
- }
411
- private static onEnd = () => {
412
- if (debug) console.log("XR Session ended");
413
- this._activeSession = null;
414
- }
415
-
416
-
417
- /** The needle engine context this session was started from */
418
- readonly context: Context;
419
-
420
- get sync(): NeedleXRSync | null {
421
- return NeedleXRSession._sync;
422
- }
423
-
424
- /** Returns true if the xr session is still active */
425
- get running(): boolean { return !this._ended && this.session != null; }
426
-
427
- /**
428
- * @link https://developer.mozilla.org/en-US/docs/Web/API/XRSession
429
- */
430
- readonly session: XRSession;
431
-
432
- /** XR Session Mode: AR or VR */
433
- readonly mode: XRSessionMode;
434
-
435
- /**
436
- * The XRSession interface's read-only interactionMode property describes the best space (according to the user agent) for the application to draw an interactive UI for the current session.
437
- * @link https://developer.mozilla.org/en-US/docs/Web/API/XRSession/interactionMode
438
- */
439
- get interactionMode(): "screen-space" | "world-space" { return this.session["interactionMode"]; }
440
-
441
- /**
442
- * @link https://developer.mozilla.org/en-US/docs/Web/API/XRSession/visibilityState
443
- */
444
- get visibilityState() { return this.session.visibilityState; }
445
-
446
- /**
447
- * @link https://developer.mozilla.org/en-US/docs/Web/API/XRSession/environmentBlendMode
448
- */
449
- get environmentBlendMode() { return this.session.environmentBlendMode; }
450
-
451
- /**
452
- * The current XR frame
453
- * @link https://developer.mozilla.org/en-US/docs/Web/API/XRFrame
454
- */
455
- get frame(): XRFrame { return this.context.xrFrame!; }
456
-
457
- /** The currently active/connected controllers */
458
- readonly controllers: NeedleXRController[] = [];
459
- /** shorthand to query the left controller. Use `controllers` to get access to all connected controllers */
460
- get leftController() { return this.controllers.find(c => c.isLeft); }
461
- /** shorthand to query the right controller. Use `controllers` to get access to all connected controllers */
462
- get rightController() { return this.controllers.find(c => c.isRight); }
463
- /** @returns the given controller if it is connected */
464
- getController(side: XRHandedness) { return this.controllers.find(c => c.side === side) || null; }
465
-
466
- /** Returns true if running in pass through mode in immersive AR */
467
- get isPassThrough() {
468
- if (this.environmentBlendMode !== 'opaque' && this.interactionMode === "world-space") return true;
469
- // since we can not rely on interactionMode check we check the controllers too
470
- // https://linear.app/needle/issue/NE-4057
471
- // the following is a workaround for the issue above
472
- if (this.mode === "immersive-ar" && this.environmentBlendMode !== 'opaque') {
473
- // if we have any tracked pointer controllers we're also in passthrough
474
- if (this.controllers.some(c => c.inputSource.targetRayMode === "tracked-pointer"))
475
- return true;
476
- }
477
- if (isDevEnvironment() && isDesktop() && this.mode === "immersive-ar") {
478
- return true;
479
- }
480
- return false;
481
- }
482
- get isAR() { return this.mode === 'immersive-ar'; }
483
- get isVR() { return this.mode === 'immersive-vr'; }
484
-
485
- get posePosition() { return this._transformPosition; }
486
- get poseOrientation() { return this._transformOrientation; }
487
- /** @returns the context.renderer.xr.getReferenceSpace() result */
488
- get referenceSpace(): XRSpace | null { return this.context.renderer.xr.getReferenceSpace(); }
489
- /** @returns the XRFrame `viewerpose` using the xr `referenceSpace` */
490
- get viewerPose(): XRViewerPose | undefined { return this._viewerPose; }
491
-
492
-
493
- /** @returns `true` if any image is currently being tracked */
494
- /** returns true if images are currently being tracked */
495
- get isTrackingImages() {
496
- if (this.frame && "getImageTrackingResults" in this.frame && typeof this.frame.getImageTrackingResults === "function") {
497
- try {
498
- const trackingResult = this.frame.getImageTrackingResults();
499
- for (const result of trackingResult) {
500
- const state = result.trackingState;
501
- if (state === "tracked") return true;
502
- }
503
- }
504
- catch {
505
- // Looks like we get a NotSupportedException on Android since the method is known
506
- // but the feature is not supported by the session
507
- // TODO Can we check here if we even requested the image-tracking feature instead of catching?
508
- return false;
509
- }
510
- }
511
- return false;
512
- }
513
-
514
-
515
- /** The currently active XR rig */
516
- get rig(): IXRRig | null {
517
- const rig = this._rigs[0] ?? null;
518
- if (rig?.gameObject && isDestroyed(rig.gameObject) || rig?.isActive === false) {
519
- this.updateActiveXRRig();
520
- return this._rigs[0] ?? null;
521
- }
522
- return rig;
523
- }
524
- private _rigScale: number = 1;
525
- private _lastRigScaleUpdate: number = -1;
526
- /** get the XR rig worldscale */
527
- get rigScale() {
528
- if (!this._rigs[0]) return 1;
529
- if (this._lastRigScaleUpdate !== this.context.time.frame) {
530
- this._lastRigScaleUpdate = this.context.time.frame;
531
- this._rigScale = this._rigs[0].gameObject.worldScale.x;
532
- }
533
- return this._rigScale;
534
- }
535
- /** add a rig to the available XR rigs - if it's priority is higher than the currently active rig it will be enabled */
536
- addRig(rig: IXRRig) {
537
- const i = this._rigs.indexOf(rig);
538
- if (i >= 0) return;
539
- if (rig.priority === undefined) rig.priority = 0;
540
- this._rigs.push(rig);
541
- this.updateActiveXRRig();
542
- }
543
- /** Remove a rig from the available XR Rigs */
544
- removeRig(rig: IXRRig) {
545
- const i = this._rigs.indexOf(rig);
546
- if (i === -1) return;
547
- this._rigs.splice(i, 1);
548
- this.updateActiveXRRig();
549
- }
550
- /** Sets a XRRig to be active which will parent the camera to this rig */
551
- setRigActive(rig: IXRRig) {
552
- const i = this._rigs.indexOf(rig);
553
- this._rigs.splice(i, 1);
554
- this._rigs.unshift(rig);
555
- this.updateActiveXRRig();
556
- }
557
- private updateActiveXRRig() {
558
- const previouslyActiveRig = this._rigs[0] ?? null;
559
-
560
- // ensure that the default rig is in the scene
561
- if (this._defaultRig.gameObject.parent !== this.context.scene)
562
- this.context.scene.add(this._defaultRig.gameObject);
563
- // ensure the fallback rig is always active!!!
564
- this._defaultRig.gameObject.visible = true;
565
- // ensure that the default rig is in the list of available rigs
566
- if (!this._rigs.includes(this._defaultRig))
567
- this._rigs.push(this._defaultRig);
568
-
569
- // find the rig with the highest priority and make sure it's at the beginning of the array
570
- let highestPriorityRig: IXRRig = this._rigs[0];
571
- if (highestPriorityRig && highestPriorityRig.priority === undefined) highestPriorityRig.priority = 0;
572
-
573
- for (let i = 1; i < this._rigs.length; i++) {
574
- const rig = this._rigs[i];
575
- if (!rig.isActive) continue;
576
- if (isDestroyed(rig.gameObject)) {
577
- this._rigs.splice(i, 1);
578
- i--;
579
- continue;
580
- }
581
- if (!highestPriorityRig || highestPriorityRig.isActive === false || (rig.priority !== undefined && rig.priority > highestPriorityRig.priority!)) {
582
- highestPriorityRig = rig;
583
- }
584
- }
585
-
586
- // make sure the highest priority rig is at the beginning if it isnt already
587
- if (previouslyActiveRig !== highestPriorityRig) {
588
- const index = this._rigs.indexOf(highestPriorityRig);
589
- if (index >= 0) this._rigs.splice(index, 1);
590
- this._rigs.unshift(highestPriorityRig);
591
- }
592
-
593
- if (debug) {
594
- if (previouslyActiveRig === highestPriorityRig)
595
- console.log("Updated Active XR Rig:", highestPriorityRig, "prev:", previouslyActiveRig);
596
- else console.log("Updated Active XRRig:", highestPriorityRig, " (the same as before)");
597
- }
598
- }
599
- private _rigs: IXRRig[] = [];
600
-
601
-
602
-
603
- private _viewerHitTestSource: XRHitTestSource | null = null;
604
-
605
- /** Returns a XR hit test result (if hit-testing is available) in rig space
606
- * @param source If provided, the hit test will be performed for the given controller
607
- */
608
- getHitTest(source?: NeedleXRController): NeedleXRHitTestResult | null {
609
- if (source) {
610
- return this.getControllerHitTest(source);
611
- }
612
-
613
- if (!this._viewerHitTestSource) return null;
614
- const hitTestSource = this._viewerHitTestSource;
615
- const hitTestResults = this.frame.getHitTestResults(hitTestSource);
616
- if (hitTestResults.length > 0) {
617
- const hit = hitTestResults[0];
618
- return this.convertHitTestResult(hit);
619
- }
620
- return null;
621
- }
622
- private getControllerHitTest(controller: NeedleXRController): NeedleXRHitTestResult | null {
623
- const hitTestSource = controller.hitTestSource;
624
- if (!hitTestSource) return null;
625
- const res = this.frame.getHitTestResultsForTransientInput(hitTestSource);
626
- for (const result of res) {
627
- if (result.inputSource === controller.inputSource) {
628
- for (const hit of result.results) {
629
- return this.convertHitTestResult(hit);
630
- }
631
- }
632
- }
633
- return null;
634
- }
635
- private convertHitTestResult(result: XRHitTestResult): NeedleXRHitTestResult | null {
636
- const referenceSpace = this.context.renderer.xr.getReferenceSpace();
637
- const pose = referenceSpace && result.getPose(referenceSpace);
638
- if (pose) {
639
- const pos = getTempVector(pose.transform.position);
640
- const rot = getTempQuaternion(pose.transform.orientation);
641
- const camera = this.context.mainCamera;
642
- if (camera?.parent !== this._cameraRenderParent) {
643
- pos.applyMatrix4(flipForwardMatrix);
644
- }
645
- if (camera?.parent) {
646
- pos.applyMatrix4(camera.parent.matrixWorld);
647
- rot.multiply(flipForwardQuaternion);
648
- // apply parent quaternion (if parent is moved/rotated)
649
- const parentRotation = getWorldQuaternion(camera.parent);
650
- // ensure that "up" (y+) is pointing away from the wall
651
- parentRotation.premultiply(flipForwardQuaternion);
652
- rot.premultiply(parentRotation);
653
- }
654
- return { hit: result, position: pos, quaternion: rot };
655
- }
656
- return null;
657
- }
658
-
659
-
660
- /** convert a XRRigidTransform from XR session space to threejs / Needle Engine XR space */
661
- convertSpace(transform: XRRigidTransform): { position: Vector3, quaternion: Quaternion } {
662
- const pos = getTempVector(transform.position);
663
- pos.applyMatrix4(flipForwardMatrix);
664
- const rot = getTempQuaternion(transform.orientation);
665
- rot.premultiply(flipForwardQuaternion);
666
- return { position: pos, quaternion: rot };
667
- }
668
-
669
- /** this is the implictly created XR rig */
670
- private readonly _defaultRig: IXRRig;
671
-
672
- /** all scripts that receive some sort of XR update event */
673
- private readonly _xr_scripts: INeedleXRSessionEventReceiver[];
674
- /** scripts that have onUpdateXR event methods */
675
- private readonly _xr_update_scripts: INeedleXRSessionEventReceiver[] = [];
676
- /** scripts that are in the scene but inactive (e.g. disabled parent gameObject) */
677
- private readonly _inactive_scripts: INeedleXRSessionEventReceiver[] = [];
678
- private readonly _controllerAdded: ControllerChangedEvt[];
679
- private readonly _controllerRemoved: ControllerChangedEvt[];
680
- private readonly _originalCameraWorldPosition?: Vector3 | null;
681
- private readonly _originalCameraWorldRotation?: Quaternion | null;
682
- private readonly _originalCameraWorldScale?: Vector3 | null;
683
- private readonly _originalCameraParent?: Object3D | null;
684
- /** we store the main camera reference here each frame to make sure we have a rendering camera
685
- * this e.g. the case when the XR rig with the camera gets disabled (and thus this.context.mainCamera is unassigned)
686
- */
687
- private _mainCamera: ICamera | null = null;
688
-
689
- private constructor(mode: XRSessionMode, session: XRSession, context: Context, extra: {
690
- scripts: INeedleXRSessionEventReceiver[],
691
- controller_added: ControllerChangedEvt[],
692
- controller_removed: ControllerChangedEvt[],
693
- /** the initialization arguments */
694
- init: XRSessionInit,
695
- }) {
696
- saveSessionInfo(mode, extra.init);
697
- this.session = session;
698
- this.mode = mode;
699
- this.context = context;
700
- this.context.renderer.xr.enabled = true;
701
- this.context.renderer.xr.setSession(this.session);
702
- this.context.xr = this;
703
-
704
- this._xr_scripts = [...extra.scripts];
705
- this._xr_update_scripts = this._xr_scripts.filter(e => typeof e.onUpdateXR === "function");
706
- this._controllerAdded = extra.controller_added;
707
- this._controllerRemoved = extra.controller_removed;
708
-
709
- registerFrameEventCallback(this.onBefore, FrameEvent.LateUpdate);
710
- this.context.pre_render_callbacks.push(this.onBeforeRender);
711
- this.context.post_render_callbacks.push(this.onAfterRender);
712
-
713
-
714
- if (extra.init.optionalFeatures?.includes("hit-test") || extra.init.requiredFeatures?.includes("hit-test")) {
715
- session.requestReferenceSpace('viewer').then((referenceSpace) => {
716
- return session.requestHitTestSource?.call(session, { space: referenceSpace })?.then((source) => {
717
- return this._viewerHitTestSource = source;
718
- });
719
- }).catch(e => console.warn(e));
720
- }
721
-
722
- if (this.context.mainCamera) {
723
- this._originalCameraWorldPosition = getWorldPosition(this.context.mainCamera, new Vector3());
724
- this._originalCameraWorldRotation = getWorldQuaternion(this.context.mainCamera, new Quaternion());
725
- this._originalCameraWorldScale = getWorldScale(this.context.mainCamera, new Vector3());
726
- this._originalCameraParent = this.context.mainCamera.parent;
727
- }
728
-
729
- this.context.mainCameraComponent?.applyClearFlags();
730
-
731
- this._defaultRig = new ImplictXRRig();
732
- this.context.scene.add(this._defaultRig.gameObject);
733
- this.addRig(this._defaultRig);
734
-
735
- // register already connected input sources
736
- // this is for when the session is already running (via a temporary xr session)
737
- // and the controllers are already connected
738
- for (const sources of this.session.inputSources) {
739
- this.onInputSourceAdded(sources);
740
- }
741
-
742
- // handle controller and input source changes changes
743
- this.session.addEventListener('end', this.onEnd);
744
- // handle input sources change
745
- this.session.addEventListener("inputsourceschange", (evt: XRInputSourceChangeEvent) => {
746
- // handle removed controllers
747
- for (const removedInputSource of evt.removed) {
748
- this.disconnectInputSource(removedInputSource);
749
- }
750
- for (const newInputSource of evt.added) {
751
- this.onInputSourceAdded(newInputSource);
752
- }
753
- });
754
- }
755
- private onInputSourceAdded = (newInputSource: XRInputSource) => {
756
- // do not create XR controllers for screen input sources
757
- if (newInputSource.targetRayMode === "screen") {
758
- return;
759
- }
760
- let index = 0;
761
- for (let i = 0; i < this.session.inputSources.length; i++) {
762
- if (this.session.inputSources[i] === newInputSource) {
763
- index = i;
764
- break;
765
- }
766
- }
767
- // check if an xr controller for this input source already exists
768
- // in case we have both an event from inputsourceschange and from the construtor initial input sources
769
- if (this.controllers.find(c => c.inputSource === newInputSource)) return;
770
-
771
- const newController = new NeedleXRController(this, newInputSource, index);
772
- this.controllers.push(newController);
773
- this.controllers.sort((a, b) => a.index - b.index);
774
- this._newControllers.push(newController);
775
- this.invokeControllerEvent(newController, this._controllerAdded, "added");
776
-
777
- }
778
-
779
- /** End the XR Session */
780
- end() {
781
- // this can be called by external code to end the session
782
- // the actual cleanup happens in onEnd which subscribes to the session end event
783
- // so users can also just regularly call session.end() and the cleanup will happen automatically
784
- if (this._ended) return;
785
- this.session.end().catch(e => console.warn(e));
786
- }
787
-
788
- private _ended: boolean = false;
789
- private readonly _newControllers: NeedleXRController[] = [];
790
-
791
- private onEnd = (_evt: XRSessionEvent) => {
792
- if (this._ended) return;
793
- this._ended = true;
794
-
795
- if (debug) console.log("XR Session ended");
796
-
797
- deleteSessionInfo();
798
-
799
- this.onAfterRender();
800
- this.revertCustomForward();
801
- this._didStart = false;
802
- this._previousCameraParent = null;
803
-
804
- unregisterFrameEventCallback(this.onBefore, FrameEvent.LateUpdate);
805
- const index = this.context.pre_render_callbacks.indexOf(this.onBeforeRender);
806
- if (index >= 0) this.context.pre_render_callbacks.splice(index, 1);
807
- const index2 = this.context.post_render_callbacks.indexOf(this.onAfterRender);
808
- if (index2 >= 0) this.context.post_render_callbacks.splice(index2, 1);
809
-
810
- this.context.xr = null;
811
- this.context.renderer.xr.enabled = false;
812
- this.context.mainCameraComponent?.applyClearFlags();
813
-
814
- // make sure we disconnect all controllers
815
- for (let i = 0; i < this.controllers.length; i++) {
816
- this.disconnectInputSource(this.controllers[i].inputSource);
817
- }
818
-
819
- // we want to call leave XR for *all* scripts that are still registered
820
- // even if they might already be destroyed e.g. by the WebXR component (it destroys the default controller scripts)
821
- // they should still receive this callback to be properly cleaned up
822
- for (const listener of this._xr_scripts) {
823
- listener?.onLeaveXR?.({ xr: this });
824
- }
825
-
826
- this.sync?.onExitXR(this);
827
-
828
-
829
- if (this.context.mainCamera) {
830
- // if we have a main camera we want to move it back to it's original parent
831
- this._originalCameraParent?.add(this.context.mainCamera);
832
-
833
- if (this._originalCameraWorldPosition) {
834
- setWorldPosition(this.context.mainCamera, this._originalCameraWorldPosition);
835
- }
836
- if (this._originalCameraWorldRotation) {
837
- setWorldQuaternion(this.context.mainCamera, this._originalCameraWorldRotation);
838
- }
839
- if (this._originalCameraWorldScale) {
840
- setWorldScale(this.context.mainCamera, this._originalCameraWorldScale);
841
- }
842
- }
843
-
844
- // mark for size change since DPI might have changed
845
- this.context.requestSizeUpdate();
846
-
847
- this._defaultRig.gameObject.removeFromParent();
848
- };
849
-
850
- /** Disconnects the controller, invokes events and notifies previou controller (if any) */
851
- private disconnectInputSource(inputSource: XRInputSource) {
852
- for (let i = this.controllers.length - 1; i >= 0; i--) {
853
- const oldController = this.controllers[i];
854
- if (oldController.inputSource === inputSource) {
855
- this.controllers.splice(i, 1);
856
- this.invokeControllerEvent(oldController, this._controllerRemoved, "removed");
857
- const args: NeedleXRControllerEventArgs = {
858
- xr: this,
859
- controller: oldController,
860
- change: "removed"
861
- };
862
- for (const script of this._xr_scripts) {
863
- if (script.onXRControllerRemoved) script.onXRControllerRemoved(args);
864
- }
865
- oldController.onDisconnected();
866
- }
867
- }
868
- }
869
-
870
- private _didStart: boolean = false;
871
-
872
- /** Called every frame by the engine */
873
- private onBefore = (context: Context) => {
874
- const frame = context.xrFrame;
875
- if (!frame) return;
876
-
877
- // ensure that XR is always set to a running session
878
- this.context.xr = this;
879
-
880
- // ensure that we always have the correct main camera reference
881
- // we need to store the main camera reference here because the main camera can be disabled and then it's removed from the context
882
- // but in XR we want to ensure we always have a active camera (or at least parent the camera to the active rig)
883
- if (this.context.mainCameraComponent && this.context.mainCameraComponent !== this._mainCamera) {
884
- this._mainCamera = this.context.mainCameraComponent;
885
- }
886
-
887
- if (this.rig?.isActive == false) {
888
- if (debug) console.warn("Latest rig is not active - trying to activate a different rig", this.rig);
889
- this.updateActiveXRRig();
890
- }
891
-
892
- if ((debug || debugFPS) && this.rig) {
893
- const pos = this.rig.gameObject.worldPosition;
894
- const forward = this.rig.gameObject.worldForward;
895
- pos.add(forward.multiplyScalar(1.5));
896
- const upwards = this.rig.gameObject.worldUp;
897
- pos.add(upwards.multiplyScalar(2.5));
898
- Gizmos.DrawLabel(pos, this.context.time.smoothedFps.toFixed(1));
899
- }
900
-
901
- // make sure the camera is parented to the active rig
902
- if (this.rig && this._mainCamera?.gameObject) {
903
- const currentParent = this._mainCamera?.gameObject?.parent;
904
- if (currentParent !== this.rig.gameObject) {
905
- this.rig.gameObject.add(this._mainCamera?.gameObject);
906
- }
907
- }
908
-
909
- this.internalUpdateState();
910
-
911
- // we apply the flip immediately and keep it while in XR so that regular raycasts just work
912
- // otherwise rendering would fool us
913
- this.applyCustomForward();
914
-
915
- const args: NeedleXREventArgs = { xr: this };
916
-
917
- // we want to invoke ENTERXR for NEW component nad EXITXR for REMOVED components
918
- // we want to invoke onControllerAdded to components joining a running XR session if controllers are already connected
919
- //TODO: handle REMOVED components during a session (we want to call leave session events and controller removed events)
920
-
921
- // deferred start because we need an XR frame
922
- if (!this._didStart) {
923
- this._didStart = true;
924
-
925
- for (const listener of NeedleXRSession._xrStartListeners) {
926
- listener(args);
927
- }
928
-
929
- // invoke session listeners start
930
- // we need to make a copy because the array might be modified during the loop (could also use a for loop and iterate backwards perhaps but then order of invocation would be changed OR check if the size has changed...)
931
- const copy = [...this._xr_scripts];
932
- if (debug) console.log("NeedleXRSession start, handle scripts:", copy);
933
- for (const script of copy) {
934
- if (script.destroyed) {
935
- this._script_to_remove.push(script);
936
- continue;
937
- }
938
- if (!script.activeAndEnabled) {
939
- this.markInactive(script);
940
- continue;
941
- }
942
- // if ((script as IComponent).activeAndEnabled === false) continue;
943
- this.invokeCallback_EnterXR(script);
944
- // also invoke all events for currently (already) connected controllers
945
- for (const controller of this.controllers) {
946
- this.invokeCallback_ControllerAdded(script, controller);
947
- }
948
- }
949
- }
950
- else if (this.context.new_scripts_xr.length > 0) {
951
- // invoke start on all new scripts that were added during the session and that support the current mode
952
- const copy = [...this.context.new_scripts_xr];
953
- for (let i = 0; i < copy.length; i++) {
954
- const script = this.context.new_scripts_xr[i] as IComponent & INeedleXRSessionEventReceiver;
955
- if (!script || script.destroyed || script.supportsXR?.(this.mode) == false) {
956
- this.context.new_scripts_xr.splice(i, 1);
957
- continue;
958
- }
959
- if (!script.activeAndEnabled) {
960
- this.context.new_scripts_xr.splice(i, 1);
961
- this.markInactive(script);
962
- continue;
963
- }
964
- // ignore inactive scripts
965
- // if (script.activeAndEnabled === false) continue;
966
- if (this.addScript(script)) {
967
- // invoke onEnterXR on those scripts because they joined a running session
968
- this.invokeCallback_EnterXR(script);
969
- // also invoke all events for currently (already) connected controllers
970
- for (const controller of this.controllers) {
971
- this.invokeCallback_ControllerAdded(script, controller);
972
- }
973
- }
974
- }
975
- }
976
-
977
- // make sure camera layers are correct
978
- // we do this every frame here but I think it would be enough to do it once after the first rendering
979
- // since we want to override the settings in three's WebXRManager
980
- // we also want to continuously sync the culling mask if a user changes it on the main cam while in XR
981
- this.syncCameraCullingMask();
982
-
983
- // update controllers
984
- for (const controller of this.controllers) {
985
- controller.onUpdate(frame);
986
- }
987
-
988
- // handle when new controllers have been added
989
- for (const controller of this._newControllers) {
990
- for (const script of this._xr_scripts) {
991
- if (script.destroyed) {
992
- this._script_to_remove.push(script);
993
- continue;
994
- }
995
- if (script.onXRControllerAdded) script.onXRControllerAdded({ xr: this, controller, change: "added" });
996
- }
997
- }
998
- this._newControllers.length = 0;
999
-
1000
- // invoke update on all scripts
1001
- for (const script of this._xr_update_scripts) {
1002
- if (script.destroyed === true) {
1003
- this._script_to_remove.push(script);
1004
- continue;
1005
- }
1006
- if (script.activeAndEnabled === false) {
1007
- this.markInactive(script);
1008
- continue;
1009
- }
1010
- if (script.onUpdateXR) script.onUpdateXR(args);
1011
- }
1012
-
1013
- // handle inactive scripts
1014
- this.handleInactiveScripts();
1015
-
1016
- // handle removed scripts
1017
- if (this._script_to_remove.length > 0) {
1018
- // make sure we have no duplicates
1019
- const unique = [...new Set(this._script_to_remove)];
1020
- this._script_to_remove.length = 0;
1021
- for (const script of unique) {
1022
- if (!script.destroyed && this.running) {
1023
- script.onLeaveXR?.(args);
1024
- }
1025
- this.removeScript(script);
1026
- }
1027
- }
1028
-
1029
- this.sync?.onUpdate(this);
1030
-
1031
- if (debug) {
1032
- for (const controller of this.controllers) {
1033
- controller.onRenderDebug();
1034
- }
1035
- }
1036
- }
1037
-
1038
- private onBeforeRender = () => {
1039
- if (this.context.mainCamera)
1040
- this.updateFade(this.context.mainCamera);
1041
- }
1042
-
1043
- private onAfterRender = () => {
1044
- this.onUpdateFade_PostRender();
1045
-
1046
- // render spectator view if we're in VR using Link
1047
- if (isDesktop()) {
1048
- const renderer = this.context.renderer;
1049
- if (renderer.xr.isPresenting && this.context.mainCamera) {
1050
- const wasXr = renderer.xr.enabled;
1051
- const previousRenderTarget = renderer.getRenderTarget();
1052
- renderer.xr.enabled = false;
1053
- renderer.setRenderTarget(null);
1054
- renderer.render(this.context.scene, this.context.mainCamera);
1055
- renderer.xr.enabled = wasXr;
1056
- renderer.setRenderTarget(previousRenderTarget);
1057
- }
1058
- }
1059
- }
1060
-
1061
- /** register a new XR script if it hasnt added yet */
1062
- private addScript(script: INeedleXRSessionEventReceiver) {
1063
- if (this._xr_scripts.includes(script)) return false;
1064
- if (debug) console.log("Register new XRScript", script);
1065
- this._xr_scripts.push(script);
1066
- if (typeof script.onUpdateXR === "function") {
1067
- this._xr_update_scripts.push(script);
1068
- }
1069
- return true;
1070
- }
1071
-
1072
- /** mark a script as inactive and invokes callbacks */
1073
- private markInactive(script: INeedleXRSessionEventReceiver) {
1074
- if (this._inactive_scripts.indexOf(script) >= 0) return;
1075
- // inactive scripts should not receive any regular callbacks anymore
1076
- this.removeScript(script, false);
1077
- this._inactive_scripts.push(script);
1078
- // inactive scripts receive callbacks as if the XR session has ended
1079
- for (const ctrl of this.controllers) this.invokeCallback_ControllerRemoved(script, ctrl);
1080
- this.invokeCallback_LeaveXR(script);
1081
- }
1082
- private handleInactiveScripts() {
1083
- if (this._inactive_scripts.length > 0) {
1084
- for (let i = this._inactive_scripts.length - 1; i >= 0; i--) {
1085
- const script = this._inactive_scripts[i];
1086
- if (script.activeAndEnabled) {
1087
- this._inactive_scripts.splice(i, 1);
1088
- this.addScript(script);
1089
- this.invokeCallback_EnterXR(script);
1090
- for (const ctrl of this.controllers) this.invokeCallback_ControllerAdded(script, ctrl);
1091
- }
1092
- }
1093
- }
1094
- }
1095
-
1096
- private readonly _script_to_remove: INeedleXRSessionEventReceiver[] = [];
1097
-
1098
- private removeScript(script: INeedleXRSessionEventReceiver, removeCompletely: boolean = true) {
1099
- if (debug) console.log("Remove XRScript", script);
1100
- const index = this._xr_scripts.indexOf(script);
1101
- if (index >= 0) this._xr_scripts.splice(index, 1);
1102
- const index2 = this._xr_update_scripts.indexOf(script);
1103
- if (index2 >= 0) this._xr_update_scripts.splice(index2, 1);
1104
- if (removeCompletely) {
1105
- const index3 = this._inactive_scripts.indexOf(script);
1106
- if (index3 >= 0) this._inactive_scripts.splice(index3, 1);
1107
- }
1108
- }
1109
-
1110
- private invokeCallback_EnterXR(script: INeedleXRSessionEventReceiver) {
1111
- if (script.onEnterXR) {
1112
- script.onEnterXR({ xr: this });
1113
- }
1114
- }
1115
- private invokeCallback_ControllerAdded(script: INeedleXRSessionEventReceiver, controller: NeedleXRController) {
1116
- if (script.onXRControllerAdded) {
1117
- script.onXRControllerAdded({ xr: this, controller, change: "added" });
1118
- }
1119
- }
1120
- private invokeCallback_ControllerRemoved(script: INeedleXRSessionEventReceiver, controller: NeedleXRController) {
1121
- if (script.onXRControllerRemoved) {
1122
- script.onXRControllerRemoved({ xr: this, controller, change: "removed" });
1123
- }
1124
- }
1125
- private invokeCallback_LeaveXR(script: INeedleXRSessionEventReceiver) {
1126
- if (script.onLeaveXR && !script.destroyed) {
1127
- script.onLeaveXR({ xr: this });
1128
- }
1129
- }
1130
-
1131
- private syncCameraCullingMask() {
1132
- // when we set unity layers objects will only be rendered on one eye
1133
- // we set layers to sync raycasting and have a similar behaviour to unity
1134
- const cam = this.context.xrCamera;
1135
- const cull = this.context.mainCameraComponent?.cullingMask;
1136
- if (cam && cull !== undefined) {
1137
- for (const c of cam.cameras) {
1138
- c.layers.mask = cull;
1139
- }
1140
- cam.layers.mask = cull;
1141
- }
1142
- else if (cam) {
1143
- for (const c of cam.cameras) {
1144
- c.layers.enableAll();
1145
- }
1146
- cam.layers.enableAll();
1147
- }
1148
- }
1149
-
1150
- private invokeControllerEvent(controller: NeedleXRController, listeners: ControllerChangedEvt[], change: "added" | "removed") {
1151
- for (let i = listeners.length - 1; i >= 0; i--) {
1152
- const listener = listeners[i];
1153
- if (!listener) continue;
1154
- try {
1155
- listener({
1156
- xr: this,
1157
- controller,
1158
- change
1159
- });
1160
- }
1161
- catch (e) {
1162
- console.error(e);
1163
- }
1164
- }
1165
- }
1166
-
1167
-
1168
- private _camera!: Object3D;
1169
- private readonly _cameraRenderParent: Object3D = new Object3D().rotateY(Math.PI);
1170
- private _previousCameraParent!: Object3D | null;
1171
- private readonly _customforward: boolean = true;
1172
- private originalCameraNearPlane?: number;
1173
- /** This is used to have the XR system camera look into threejs Z forward direction (instead of -z) */
1174
- private applyCustomForward() {
1175
- if (this.context.mainCamera && this._customforward) {
1176
- this._camera = this.context.mainCamera;
1177
- if (this._camera.parent !== this._cameraRenderParent) {
1178
- this._previousCameraParent = this._camera.parent;
1179
- this._previousCameraParent?.add(this._cameraRenderParent);
1180
- }
1181
- this._cameraRenderParent.name = "XR Camera Render Parent";
1182
- this._cameraRenderParent.add(this._camera);
1183
-
1184
- let minNearPlane = .02;
1185
- if (this.rig) {
1186
- const rigWorldScale = getWorldScale(this.rig.gameObject);
1187
- minNearPlane *= rigWorldScale.x;
1188
- }
1189
- if (this._camera instanceof PerspectiveCamera && this._camera.near > minNearPlane) {
1190
- this.originalCameraNearPlane = this._camera.near;
1191
- this._camera.near = minNearPlane;
1192
- }
1193
- }
1194
- }
1195
- private revertCustomForward() {
1196
- if (this._camera && this._previousCameraParent) {
1197
- this._previousCameraParent.add(this._camera);
1198
- }
1199
- this._previousCameraParent = null;
1200
-
1201
- if (this._camera instanceof PerspectiveCamera && this.originalCameraNearPlane != undefined) {
1202
- this._camera.near = this.originalCameraNearPlane;
1203
- }
1204
- }
1205
-
1206
-
1207
- private _viewerPose?: XRViewerPose;
1208
- private readonly _transformOrientation = new Quaternion();
1209
- private readonly _transformPosition = new Vector3();
1210
-
1211
- private internalUpdateState() {
1212
- const referenceSpace = this.context.renderer.xr.getReferenceSpace();
1213
- if (!referenceSpace) {
1214
- this._viewerPose = undefined;
1215
- return;
1216
- }
1217
- this._viewerPose = this.frame.getViewerPose(referenceSpace);
1218
- if (this._viewerPose) {
1219
- const transform: XRRigidTransform = this._viewerPose.transform;
1220
- this._transformPosition.set(transform.position.x, transform.position.y, transform.position.z);
1221
- this._transformOrientation.set(transform.orientation.x, transform.orientation.y, transform.orientation.z, transform.orientation.w);
1222
- }
1223
- }
1224
-
1225
- // TODO: for scene transitions (e.g. SceneSwitcher) where creating the scene might take a few moments we might want more control over when/how this fading occurs and how long the scene stays black
1226
- private transition?: SceneTransition;
1227
-
1228
- /** Call to fade rendering to black for a short moment (the returned promise will be resolved when fully black)
1229
- * This can be used to mask scene transitions or teleportation
1230
- * @returns a promise that is resolved when the screen is fully black
1231
- * @example `fadeTransition().then(() => { <fully_black> })`
1232
- */
1233
- fadeTransition() {
1234
- if (!this.transition) this.transition = new SceneTransition();
1235
- return this.transition.fadeTransition();
1236
- }
1237
-
1238
- /** e.g. FadeToBlack */
1239
- private updateFade(camera: Camera) {
1240
- if (this.transition && camera instanceof PerspectiveCamera)
1241
- this.transition.update(camera, this.context.time.deltaTime);
1242
- }
1243
-
1244
- private onUpdateFade_PostRender() {
1245
- this.transition?.remove();
1246
- }
1247
- }
src/engine/xr/NeedleXRSync.ts DELETED
@@ -1,221 +0,0 @@
1
- import type { Context } from "../engine_context.js";
2
- import { RoomEvents, UserJoinedOrLeftRoomModel } from "../engine_networking.js";
3
- import { getParam } from "../engine_utils.js";
4
- import { NeedleXRController } from "./NeedleXRController.js";
5
- import { NeedleXRSession } from "./NeedleXRSession.js";
6
-
7
- const debug = getParam("debugwebxr");
8
-
9
-
10
- declare type XRControllerType = "hand" | "controller";
11
-
12
- declare type XRControllerState = {
13
- // adding a guid so it's saved on the server, ideally we have a "room lifetime" store that doesnt save state forever on disc but just until the room is disposed (we have to add support for this in the networking backend tho)
14
- guid: string;
15
- index: number;
16
- handedness: XRHandedness;
17
- isTracking: boolean;
18
- type: XRControllerType;
19
- }
20
-
21
- class XRUserState {
22
-
23
- readonly controllerStates: XRControllerState[] = [];
24
-
25
- readonly userId: string;
26
- readonly context: Context;
27
-
28
- private readonly userStateEvtName: string;
29
-
30
- constructor(userId: string, context: Context) {
31
- this.userId = userId;
32
- this.context = context;
33
- this.userStateEvtName = "xr-sync-user-state-" + userId;
34
- this.context.connection.beginListen(this.userStateEvtName, this.onReceivedControllerState);
35
- }
36
-
37
- dispose() {
38
- this.context.connection.stopListen(this.userStateEvtName, this.onReceivedControllerState);
39
- }
40
-
41
- onReceivedControllerState = (state: XRControllerState) => {
42
- if (debug) console.log(`XRSync: Received change for ${this.userId}: ${state.type} ${state.handedness}; tracked=${state.isTracking}`);
43
-
44
- let found = false;
45
- for (let i = 0; i < this.controllerStates.length; i++) {
46
- const ctrl = this.controllerStates[i];
47
- if (ctrl.index === state.index) {
48
- this.controllerStates[i] = state;
49
- found = true;
50
- break;
51
- }
52
- }
53
- if (!found) {
54
- this.controllerStates.push(state);
55
- }
56
- }
57
-
58
- update(session: NeedleXRSession) {
59
- if (this.context.connection.isConnected == false) return;
60
-
61
- for (let i = this.controllerStates.length - 1; i >= 0; i--) {
62
- const state = this.controllerStates[i];
63
- let foundController = false;
64
- for (let i = 0; i < session.controllers.length; i++) {
65
- const ctrl = session.controllers[i];
66
- if (ctrl.index === state.index) {
67
- foundController = true;
68
- }
69
- }
70
- if (!foundController) {
71
- // controller was removed
72
- if (debug) console.log(`XRSync: ${state.type} ${state.handedness} removed`, state.index);
73
- this.controllerStates.splice(i, 1);
74
- this.sendControllerRemoved(state);
75
- }
76
- }
77
-
78
- for (const ctrl of session.controllers) {
79
- this.updateControllerStates(ctrl);
80
- }
81
- }
82
-
83
- onExitXR(_session: NeedleXRSession) {
84
- for (const state of this.controllerStates) {
85
- this.sendControllerRemoved(state);
86
- }
87
- this.controllerStates.length = 0;
88
- }
89
-
90
- private sendControllerRemoved(state: XRControllerState) {
91
- state.isTracking = false;
92
- state.guid = "";
93
- this.context.connection.send(this.userStateEvtName, state);
94
- this.context.connection.sendDeleteRemoteState(state.guid);
95
- }
96
-
97
- private updateControllerStates(ctrl: NeedleXRController) {
98
-
99
- // this.context.connection.send(this.userStateEvtName, {});
100
- const existing = this.controllerStates.find(x => x.index === ctrl.index);
101
- if (existing) {
102
- let hasChanged = false;
103
- hasChanged ||= existing.isTracking != ctrl.isTracking;
104
- if (hasChanged) {
105
- existing.isTracking = ctrl.isTracking;
106
- this.context.connection.send(this.userStateEvtName, existing);
107
- }
108
- }
109
- else {
110
- const state: XRControllerState = {
111
- guid: this.userId + "-" + ctrl.index,
112
- isTracking: ctrl.isTracking,
113
- handedness: ctrl.side,
114
- index: ctrl.index,
115
- type: ctrl.hand ? "hand" : "controller"
116
- }
117
- this.controllerStates.push(state);
118
- this.context.connection.send(this.userStateEvtName, state);
119
- if (debug) console.log(`XRSync: ${state.type} ${state.handedness} added`, state.index);
120
- }
121
- }
122
-
123
-
124
- }
125
-
126
- export class NeedleXRSync {
127
-
128
- hasState(userId: string | null | undefined) {
129
- if (!userId) return false;
130
- return this._states.has(userId);
131
- }
132
-
133
- /** Is the left controller or hand tracked */
134
- isTracking(userId: string | null | undefined, handedness: XRHandedness): boolean | undefined {
135
- if (!userId) return undefined;
136
- const user = this._states.get(userId);
137
- if (!user) return undefined;
138
- const ctrl = user.controllerStates.find(x => x.handedness === handedness);
139
- return ctrl?.isTracking || false;
140
- }
141
-
142
- /** Is it hand tracking or a controller */
143
- getDeviceType(userId: string, handedness: XRHandedness): XRControllerType | undefined | "unknown" {
144
- if (!userId) return undefined;
145
- const user = this._states.get(userId);
146
- if (!user) return undefined;
147
- const ctrl = user.controllerStates.find(x => x.handedness === handedness);
148
- return ctrl?.type || "unknown";
149
- }
150
-
151
- private readonly context: Context;
152
-
153
- constructor(context: Context) {
154
- this.context = context;
155
- this.context.connection.beginListen(RoomEvents.JoinedRoom, this.onJoinedRoom);
156
- this.context.connection.beginListen(RoomEvents.LeftRoom, this.onLeftRoom)
157
- this.context.connection.beginListen(RoomEvents.UserJoinedRoom, this.onOtherUserJoinedRoom);
158
- this.context.connection.beginListen(RoomEvents.UserLeftRoom, this.onOtherUserLeftRoom);
159
- }
160
- destroy() {
161
- this.context.connection.stopListen(RoomEvents.JoinedRoom, this.onJoinedRoom);
162
- this.context.connection.stopListen(RoomEvents.LeftRoom, this.onLeftRoom)
163
- this.context.connection.stopListen(RoomEvents.UserJoinedRoom, this.onOtherUserJoinedRoom);
164
- this.context.connection.stopListen(RoomEvents.UserLeftRoom, this.onOtherUserLeftRoom);
165
- }
166
-
167
- private onJoinedRoom = () => {
168
- if (this.context.connection.connectionId) {
169
- if (!this._states.has(this.context.connection.connectionId)) {
170
- if (debug) console.log("XRSync: Local user joined room", this.context.connection.connectionId);
171
- this._states.set(this.context.connection.connectionId, new XRUserState(this.context.connection.connectionId, this.context));
172
- }
173
- for (const user of this.context.connection.usersInRoom()) {
174
- if (!this._states.has(user)) {
175
- this._states.set(user, new XRUserState(user, this.context));
176
- }
177
- }
178
- }
179
- }
180
- private onLeftRoom = () => {
181
- if (this.context.connection.connectionId) {
182
- if (!this._states.has(this.context.connection.connectionId)) {
183
- const state = this._states.get(this.context.connection.connectionId);
184
- state?.dispose();
185
- this._states.delete(this.context.connection.connectionId);
186
- }
187
- }
188
- }
189
- private onOtherUserJoinedRoom = (evt: UserJoinedOrLeftRoomModel) => {
190
- const userId = evt.userId;
191
- if (!this._states.has(userId)) {
192
- if (debug) console.log("XRSync: Remote user joined room", userId);
193
- this._states.set(userId, new XRUserState(userId, this.context));
194
- }
195
- }
196
- private onOtherUserLeftRoom = (evt: UserJoinedOrLeftRoomModel) => {
197
- const userId = evt.userId;
198
- if (!this._states.has(userId)) {
199
- const state = this._states.get(userId);
200
- state?.dispose();
201
- this._states.delete(userId);
202
- }
203
- }
204
-
205
- private _states: Map<string, XRUserState> = new Map();
206
-
207
- onUpdate(session: NeedleXRSession) {
208
- if (this.context.connection.isConnected && this.context.connection.connectionId) {
209
- const localState = this._states.get(this.context.connection.connectionId);
210
- localState?.update(session);
211
- }
212
- }
213
-
214
- onExitXR(session: NeedleXRSession) {
215
- if (this.context.connection.isConnected && this.context.connection.connectionId) {
216
- const localState = this._states.get(this.context.connection.connectionId);
217
- localState?.onExitXR(session);
218
- }
219
- }
220
-
221
- }
src/engine-components/NestedGltf.ts CHANGED
@@ -1,9 +1,9 @@
1
+ import { getParam } from "../engine/engine_utils.js";
2
+ import { Behaviour } from "../engine-components/Component.js";
1
3
  import { AssetReference, type ProgressCallback } from "../engine/engine_addressables.js";
4
+ import { serializable } from "../engine/engine_serialization_decorator.js";
5
+ import { InstantiateIdProvider } from "../engine/engine_networking_instantiate.js";
2
6
  import { InstantiateOptions } from "../engine/engine_gameobject.js";
3
- import { InstantiateIdProvider } from "../engine/engine_networking_instantiate.js";
4
- import { serializable } from "../engine/engine_serialization_decorator.js";
5
- import { getParam } from "../engine/engine_utils.js";
6
- import { Behaviour } from "../engine-components/Component.js";
7
7
 
8
8
  const debug = getParam("debugnestedgltf");
9
9
 
src/engine-components/Networking.ts CHANGED
@@ -1,6 +1,6 @@
1
+ import { serializable } from "../engine/engine_serialization.js";
1
2
  import type { INetworkingWebsocketUrlProvider } from "../engine/engine_networking.js";
2
3
  import { isLocalNetwork } from "../engine/engine_networking_utils.js";
3
- import { serializable } from "../engine/engine_serialization.js";
4
4
  import { getParam } from "../engine/engine_utils.js";
5
5
  import { Behaviour } from "./Component.js";
6
6
 
src/engine-components/js-extensions/Object3D.ts CHANGED
@@ -1,23 +1,24 @@
1
+ import { applyPrototypeExtensions, registerPrototypeExtensions } from "./ExtensionUtils.js";
1
2
  import { Object3D, Quaternion, Vector3 } from "three";
2
- import { TransformControlsGizmo } from "three/examples/jsm/controls/TransformControls.js";
3
-
4
- import { addNewComponent, getComponent, getComponentInChildren, getComponentInParent, getComponents, getComponentsInChildren, getComponentsInParent, getOrAddComponent, moveComponentInstance, removeComponent } from "../../engine/engine_components.js";
5
- import { destroy,isActiveSelf, setActive } from "../../engine/engine_gameobject.js";
3
+ import type { Constructor, ConstructorConcrete, IComponent, IComponent as Component } from "../../engine/engine_types.js";
4
+ import { moveComponentInstance, addNewComponent, getComponent, getComponentInChildren, getComponentInParent, getComponents, getComponentsInChildren, getComponentsInParent, getOrAddComponent, removeComponent } from "../../engine/engine_components.js";
5
+ import { isActiveSelf, setActive, destroy } from "../../engine/engine_gameobject.js";
6
6
  import {
7
- getTempVector,
7
+ setWorldPosition,
8
8
  getWorldPosition,
9
+ setWorldQuaternion,
9
10
  getWorldQuaternion,
10
- getWorldRotation,
11
11
  getWorldScale,
12
- setWorldPosition,
13
- setWorldQuaternion,
12
+ setWorldScale,
14
13
  setWorldRotation,
15
- setWorldScale}
14
+ getWorldRotation,
15
+ getTempVector
16
+ }
16
17
  from "../../engine/engine_three_utils.js";
17
- import type { Constructor, ConstructorConcrete, IComponent as Component,IComponent } from "../../engine/engine_types.js";
18
- import { applyPrototypeExtensions, registerPrototypeExtensions } from "./ExtensionUtils.js";
19
18
 
19
+ import { TransformControlsGizmo } from "three/examples/jsm/controls/TransformControls.js";
20
20
 
21
+
21
22
  // used to decorate cloned object3D objects with the same added components defined above
22
23
  export function apply(object: Object3D) {
23
24
  if (object && object.isObject3D === true) {
src/engine-components/OffsetConstraint.ts CHANGED
@@ -1,8 +1,7 @@
1
- import { Euler, Plane,Quaternion, Vector3 } from "three";
2
-
1
+ import { Behaviour, GameObject } from "./Component.js";
2
+ import * as utils from "./../engine/engine_three_utils.js";
3
+ import { Quaternion, Euler, Vector3, Plane } from "three";
3
4
  import { serializable } from "../engine/engine_serialization_decorator.js";
4
- import * as utils from "./../engine/engine_three_utils.js";
5
- import { Behaviour, GameObject } from "./Component.js";
6
5
 
7
6
  export class OffsetConstraint extends Behaviour {
8
7
 
src/engine-components/utils/OpenURL.ts CHANGED
@@ -1,9 +1,9 @@
1
1
 
2
+ import { type IPointerClickHandler, PointerEventData } from "../ui/index.js";
3
+ import { Behaviour } from "../Component.js";
4
+ import { serializable } from "../../engine/engine_serialization.js";
2
5
  import { isDevEnvironment, showBalloonMessage } from "../../engine/debug/index.js";
3
- import { serializable } from "../../engine/engine_serialization.js";
4
- import { isiOS,isSafari } from "../../engine/engine_utils.js";
5
- import { Behaviour } from "../Component.js";
6
- import { type IPointerClickHandler, PointerEventData } from "../ui/index.js";
6
+ import { isSafari } from "../../engine/engine_utils.js";
7
7
  import { ObjectRaycaster, Raycaster } from "../ui/Raycaster.js";
8
8
  import { tryGetUIComponent } from "../ui/Utils.js";
9
9
 
@@ -34,6 +34,7 @@
34
34
 
35
35
  if (isDevEnvironment()) showBalloonMessage("Open URL: " + this.url)
36
36
 
37
+
37
38
  switch (this.mode) {
38
39
  case OpenURLMode.NewTab:
39
40
  if (isSafari()) {
@@ -43,12 +44,10 @@
43
44
  globalThis.open(this.url, "_blank");
44
45
  break;
45
46
  case OpenURLMode.SameTab:
46
- // TODO: test if "same tab" now also works on iOS
47
- if (isSafari() && isiOS()) {
47
+ if (isSafari()) {
48
48
  globalThis.open(this.url, "_top");
49
49
  }
50
- else
51
- globalThis.open(this.url, "_self");
50
+ else globalThis.open(this.url, "_self");
52
51
  break;
53
52
  case OpenURLMode.NewWindow:
54
53
  if (isSafari()) {
@@ -59,10 +58,19 @@
59
58
 
60
59
  }
61
60
  }
61
+
62
62
  start(): void {
63
63
  const raycaster = this.gameObject.getComponentInParent(ObjectRaycaster);
64
64
  if (!raycaster) this.gameObject.addNewComponent(ObjectRaycaster);
65
65
  }
66
+
67
+ onEnable(): void {
68
+ if (isSafari()) window.addEventListener("touchend", this._safariNewTabWorkaround);
69
+ }
70
+ onDisable(): void {
71
+ if (isSafari()) window.removeEventListener("touchend", this._safariNewTabWorkaround);
72
+ }
73
+
66
74
  onPointerEnter(args) {
67
75
  if (!args.used && this.clickable)
68
76
  this.context.input.setCursorPointer();
@@ -75,6 +83,30 @@
75
83
  if (this.clickable && !args.used && this.url?.length)
76
84
  this.open();
77
85
  }
86
+
87
+ private _safariNewTabWorkaround = () => {
88
+ if (!this.clickable || !this.url?.length) return;
89
+ // we only need this workaround for opening a new tab
90
+ if (this.mode === OpenURLMode.SameTab) return;
91
+ // When we process the click directly in the browser event we can open a new tab
92
+ // by emitting a link attribute and calling onClick
93
+ const raycaster = this.gameObject.getComponentInParent(Raycaster);
94
+ if (raycaster) {
95
+ const hits = raycaster.performRaycast();
96
+ if (!hits) return;
97
+ for (const hit of hits) {
98
+ if (hit.object === this.gameObject || tryGetUIComponent(hit.object)?.gameObject === this.gameObject) {
99
+ this._validateUrl();
100
+ var a = document.createElement('a') as HTMLAnchorElement;
101
+ a.setAttribute("target", "_blank");
102
+ a.setAttribute("href", this.url);
103
+ a.click();
104
+ break;
105
+ }
106
+ }
107
+ }
108
+ }
109
+
78
110
  private _validateUrl() {
79
111
  if (!this.url) return;
80
112
  if (this.url.startsWith("www.")) {
src/engine-components/OrbitControls.ts CHANGED
@@ -1,21 +1,21 @@
1
- import { Box3, Box3Helper, GridHelper, Mesh, Object3D, PerspectiveCamera, Ray,ShadowMaterial, Vector2, Vector3 } from "three";
2
- import { OrbitControls as ThreeOrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
3
- import { GroundedSkybox } from "three/examples/jsm/objects/GroundedSkybox.js";
4
-
5
- import { setCameraController, useForAutoFit } from "../engine/engine_camera.js";
6
- import { Gizmos } from "../engine/engine_gizmos.js";
7
- import { Mathf } from "../engine/engine_math.js";
1
+ import { Behaviour, GameObject } from "./Component.js";
2
+ import { Camera } from "./Camera.js";
3
+ import { LookAtConstraint } from "./LookAtConstraint.js";
4
+ import { getWorldDirection, getWorldPosition, getWorldRotation, setWorldRotation } from "../engine/engine_three_utils.js";
8
5
  import { RaycastOptions } from "../engine/engine_physics.js";
9
6
  import { serializable } from "../engine/engine_serialization_decorator.js";
10
- import { getWorldDirection, getWorldPosition, getWorldRotation, setWorldRotation } from "../engine/engine_three_utils.js";
7
+ import { getParam, isMobileDevice } from "../engine/engine_utils.js";
8
+
9
+ import { Box3, Object3D, PerspectiveCamera, Vector2, Vector3, Box3Helper, GridHelper, Mesh, ShadowMaterial, Ray } from "three";
10
+ import { OrbitControls as ThreeOrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
11
+ import { type AfterHandleInputEvent, EventSystem, EventSystemEvents } from "./ui/EventSystem.js";
11
12
  import type { ICameraController } from "../engine/engine_types.js";
12
- import { getParam, isMobileDevice } from "../engine/engine_utils.js";
13
- import { Camera } from "./Camera.js";
14
- import { Behaviour, GameObject } from "./Component.js";
15
- import { LookAtConstraint } from "./LookAtConstraint.js";
13
+ import { setCameraController, useForAutoFit } from "../engine/engine_camera.js";
16
14
  import { SyncedTransform } from "./SyncedTransform.js";
17
- import { type AfterHandleInputEvent, EventSystem, EventSystemEvents } from "./ui/EventSystem.js";
18
15
  import { tryGetUIComponent } from "./ui/Utils.js";
16
+ import { GroundProjectedSkybox } from "three/examples/jsm/objects/GroundProjectedSkybox.js";
17
+ import { Mathf } from "../engine/engine_math.js";
18
+ import { Gizmos } from "../engine/engine_gizmos.js";
19
19
 
20
20
 
21
21
  const debug = getParam("debugorbit");
@@ -373,7 +373,7 @@
373
373
  this._controls.enableZoom = false;
374
374
  }
375
375
  }
376
-
376
+ //@ts-ignore
377
377
  // this._controls.zoomToCursor = this.zoomToCursor;
378
378
  if (!this.context.isInXR) {
379
379
  if (!freeCam && this.lookAtConstraint?.locked) this.setLookTargetFromConstraint(0, this.lookAtConstraint01);
@@ -542,7 +542,7 @@
542
542
  if (obj instanceof Box3Helper) allowExpanding = false;
543
543
  if (obj instanceof GridHelper) allowExpanding = false;
544
544
  // ignore GroundProjectedEnv
545
- if (obj instanceof GroundedSkybox) allowExpanding = false;
545
+ if (obj instanceof GroundProjectedSkybox) allowExpanding = false;
546
546
  // // Ignore shadow catcher geometry
547
547
  if ((obj as Mesh).material instanceof ShadowMaterial) allowExpanding = false;
548
548
  // ONLY fit meshes
src/engine-components/ui/Outline.ts CHANGED
@@ -1,8 +1,7 @@
1
- import { Color, Vector2 } from "three"
2
-
1
+ import { RGBAColor } from "../js-extensions/index.js";
3
2
  import { serializable } from "../../engine/engine_serialization.js";
4
3
  import { Behaviour } from "../Component.js";
5
- import { RGBAColor } from "../js-extensions/index.js";
4
+ import { Color, Vector2 } from "three"
6
5
 
7
6
  export class Outline extends Behaviour {
8
7
 
src/engine-components/ParticleSystem.ts CHANGED
@@ -1,31 +1,32 @@
1
+ import { Behaviour, GameObject } from "./Component.js";
1
2
  import * as THREE from "three";
2
- import { AxesHelper, BackSide, BufferGeometry, Color, FrontSide, Material, Matrix4, Mesh, MeshBasicMaterial, MeshStandardMaterial, Object3D, OneMinusDstAlphaFactor, PlaneGeometry, Quaternion, Sprite, SpriteMaterial, Vector3, Vector4 } from "three";
3
- import type { BatchedRenderer, Behavior, BehaviorPlugin, BillBoardSettings, BurstParameters, ColorGenerator, EmissionState, EmitSubParticleSystem, EmitterShape, FunctionColorGenerator, FunctionJSON, FunctionValueGenerator, IntervalValue, MeshSettings, Particle, ParticleEmitter, ParticleSystemParameters, PointEmitter, RecordState, RotationGenerator, SizeOverLife, TrailSettings, ValueGenerator, VFXBatchSettings } from "three.quarks";
4
- import { BatchedParticleRenderer, ConstantColor, ConstantValue, ParticleSystem as _ParticleSystem, RenderMode, TrailBatch, TrailParticle } from "three.quarks";
3
+ import { MainModule, EmissionModule, ShapeModule, ParticleSystemShapeType, MinMaxCurve, MinMaxGradient, ColorOverLifetimeModule, SizeOverLifetimeModule, NoiseModule, ParticleSystemSimulationSpace, ParticleBurst, type IParticleSystem, ParticleSystemRenderMode, TrailModule, VelocityOverLifetimeModule, TextureSheetAnimationModule, RotationOverLifetimeModule, LimitVelocityOverLifetimeModule, RotationBySpeedModule, InheritVelocityModule, SizeBySpeedModule, ColorBySpeedModule, ParticleSystemScalingMode } from "./ParticleSystemModules.js"
4
+ import { getParam } from "../engine/engine_utils.js";
5
5
 
6
- import { isDevEnvironment, showBalloonWarning } from "../engine/debug/index.js";
7
- import { Gizmos } from "../engine/engine_gizmos.js";
8
- import { Mathf } from "../engine/engine_math.js";
9
6
  // https://github.dev/creativelifeform/three-nebula
10
7
  // import System, { Emitter, Position, Life, SpriteRenderer, Particle, Body, MeshRenderer, } from 'three-nebula.js';
8
+
11
9
  import { serializable } from "../engine/engine_serialization.js";
10
+ import { RGBAColor } from "./js-extensions/RGBAColor.js";
11
+ import { AxesHelper, BufferGeometry, Color, Material, Matrix4, Mesh, MeshStandardMaterial, Object3D, OneMinusDstAlphaFactor, PlaneGeometry, Quaternion, Sprite, SpriteMaterial, Vector3, Vector4 } from "three";
12
+ import { getWorldPosition, getWorldQuaternion, getWorldScale, setWorldScale } from "../engine/engine_three_utils.js";
12
13
  import { assign } from "../engine/engine_serialization_core.js";
14
+ import { ParticleSystem as _ParticleSystem, ConstantValue, ConstantColor, BatchedParticleRenderer, TrailBatch, TrailParticle, RenderMode } from "three.quarks";
15
+ import type { BatchedRenderer, Behavior, BehaviorPlugin, BillBoardSettings, BurstParameters, ColorGenerator, EmissionState, EmitSubParticleSystem, EmitterShape, FunctionColorGenerator, FunctionJSON, FunctionValueGenerator, IntervalValue, MeshSettings, Particle, ParticleEmitter, ParticleSystemParameters, PointEmitter, RecordState, RotationGenerator, SizeOverLife, TrailSettings, VFXBatchSettings, ValueGenerator } from "three.quarks";
16
+ import { createFlatTexture } from "../engine/engine_shaders.js";
17
+ import { Mathf } from "../engine/engine_math.js";
13
18
  import { Context } from "../engine/engine_setup.js";
14
- import { createFlatTexture } from "../engine/engine_shaders.js";
15
- import { getWorldPosition, getWorldQuaternion, getWorldScale, setWorldScale } from "../engine/engine_three_utils.js";
16
- import { getParam } from "../engine/engine_utils.js";
19
+ import { ParticleSubEmitter } from "./ParticleSystemSubEmitter.js";
17
20
  import { NEEDLE_progressive } from "../engine/extensions/NEEDLE_progressive.js";
18
- import { Behaviour, GameObject } from "./Component.js";
19
- import { RGBAColor } from "./js-extensions/RGBAColor.js";
20
- import { ColorBySpeedModule, ColorOverLifetimeModule, EmissionModule, InheritVelocityModule, type IParticleSystem, LimitVelocityOverLifetimeModule, MainModule, MinMaxCurve, MinMaxGradient, NoiseModule, ParticleBurst, ParticleSystemRenderMode, ParticleSystemScalingMode, ParticleSystemShapeType, ParticleSystemSimulationSpace, RotationBySpeedModule, RotationOverLifetimeModule, ShapeModule, SizeBySpeedModule, SizeOverLifetimeModule, TextureSheetAnimationModule, TrailModule, VelocityOverLifetimeModule } from "./ParticleSystemModules.js"
21
- import { ParticleSubEmitter } from "./ParticleSystemSubEmitter.js";
21
+ import { Gizmos } from "../engine/engine_gizmos.js";
22
+ import { isDevEnvironment, showBalloonWarning } from "../engine/debug/index.js";
22
23
 
23
24
  const debug = getParam("debugparticles");
24
25
  const suppressProgressiveLoading = getParam("noprogressive");
25
26
  const debugProgressiveLoading = getParam("debugprogressive");
26
27
 
27
28
 
28
- export type { Particle as QParticle, Behavior as QParticleBehaviour } from "three.quarks"
29
+ export type { Behavior as QParticleBehaviour, Particle as QParticle } from "three.quarks"
29
30
 
30
31
 
31
32
 
@@ -80,17 +81,23 @@
80
81
  return res;
81
82
  }
82
83
 
84
+ private static _havePatchedQuarkShaders = false;
85
+
83
86
  getMaterial(trailEnabled: boolean = false) {
84
- const material = (trailEnabled === true && this.trailMaterial) ? this.trailMaterial : this.particleMaterial;
85
87
 
86
- if (material) {
87
- if (trailEnabled) {
88
- // the particle material for trails must be DoubleSide or BackSide (since otherwise the trail is invisible)
89
- if (material.side === FrontSide)
90
- material.side = BackSide;
88
+ if (!ParticleSystemRenderer._havePatchedQuarkShaders) {
89
+ ParticleSystemRenderer._havePatchedQuarkShaders = true;
90
+
91
+ // HACK patch three.quarks fo three152+, see https://github.com/Alchemist0823/three.quarks/issues/56#issuecomment-1560825038
92
+ const _rebuild = TrailBatch.prototype.rebuildMaterial;
93
+ TrailBatch.prototype.rebuildMaterial = function () {
94
+ _rebuild.call(this);
95
+ this.material.defines.MAP_UV = "uv";
91
96
  }
92
97
  }
93
98
 
99
+ const material = (trailEnabled === true && this.trailMaterial) ? this.trailMaterial : this.particleMaterial;
100
+
94
101
  // progressive load on start
95
102
  // TODO: figure out how to do this before particle system rendering so we only load textures for visible materials
96
103
  if (material && !suppressProgressiveLoading && material["_didRequestTextureLOD"] === undefined) {
@@ -391,7 +398,7 @@
391
398
  let size = particle.size;
392
399
  if (size <= 0 && !this.system.trails.sizeAffectsWidth) {
393
400
  // Not sure where we get to 100* from, tested in SOC trong com
394
- size = 20 * this.system.trails.widthOverTrail.evaluate(.5, trailParticle[$trailWidthRandom]);
401
+ size = 100 * this.system.trails.widthOverTrail.evaluate(.5, trailParticle[$trailWidthRandom]);
395
402
  }
396
403
  state.size = this.system.trails.getWidth(size, age01, pos01, trailParticle[$trailWidthRandom]);
397
404
  state.color.copy(particle.color);
@@ -423,7 +430,8 @@
423
430
  initialize(particle: Particle): void {
424
431
  const simulationSpeed = this.system.main.simulationSpeed;
425
432
 
426
- particle.startSpeed = this.system.main.startSpeed.evaluate(Math.random(), Math.random());
433
+ const factor = 1;
434
+ particle.startSpeed = this.system.main.startSpeed.evaluate(Math.random(), Math.random()) * factor;
427
435
  particle.velocity.copy(this.system.shape.getDirection(particle.position)).multiplyScalar(particle.startSpeed);
428
436
  if (this.system.inheritVelocity?.enabled) {
429
437
  this.system.inheritVelocity.applyInitial(particle.velocity);
@@ -608,7 +616,8 @@
608
616
  if (mat && mat["map"]) {
609
617
  const original = mat["map"]! as THREE.Texture;
610
618
  // cache the last original one so we're not creating tons of clones
611
- if (this.clonedTexture.original !== original || !this.clonedTexture.clone) {
619
+ if (this.clonedTexture.original !== original || !this.clonedTexture.clone)
620
+ {
612
621
  const tex = original.clone();
613
622
  tex.premultiplyAlpha = false;
614
623
  tex.colorSpace = THREE.LinearSRGBColorSpace;
@@ -747,7 +756,7 @@
747
756
  readonly limitVelocityOverLifetime!: LimitVelocityOverLifetimeModule;
748
757
 
749
758
  @serializable(InheritVelocityModule)
750
- inheritVelocity!: InheritVelocityModule;
759
+ readonly inheritVelocity!: InheritVelocityModule;
751
760
 
752
761
  @serializable(ColorBySpeedModule)
753
762
  readonly colorBySpeed!: ColorBySpeedModule;
@@ -926,8 +935,6 @@
926
935
  }
927
936
 
928
937
  awake(): void {
929
- this._worldPositionFrame = -1;
930
-
931
938
  this._renderer = this.gameObject.getComponent(ParticleSystemRenderer) as ParticleSystemRenderer;
932
939
 
933
940
  if (!this.main) {
@@ -961,12 +968,6 @@
961
968
  const emitter = this._particleSystem.emitter;
962
969
  this.context.scene.add(emitter);
963
970
 
964
- if (this.inheritVelocity.system && this.inheritVelocity.system !== this) {
965
- this.inheritVelocity = this.inheritVelocity.clone();
966
- }
967
- this.inheritVelocity.awake(this);
968
-
969
-
970
971
  if (debug) {
971
972
  console.log(this);
972
973
  this.gameObject.add(new AxesHelper(1))
@@ -1109,7 +1110,6 @@
1109
1110
  this._interface.update();
1110
1111
  this.shape.update(this, this.context, this.main.simulationSpace, this.gameObject);
1111
1112
  this.noise.update(this.context);
1112
-
1113
1113
  this.inheritVelocity?.update(this.context);
1114
1114
  this.velocityOverLifetime.update(this);
1115
1115
  }
src/engine-components/ParticleSystemModules.ts CHANGED
@@ -1,15 +1,14 @@
1
- import { createNoise4D, type NoiseFunction4D } from 'simplex-noise';
2
- import { Euler, Matrix4, Object3D, Quaternion, Vector2, Vector3, Vector4 } from "three";
3
- import type { EmitterShape, Particle, ShapeJSON } from "three.quarks";
4
-
5
- import { Gizmos } from "../engine/engine_gizmos.js";
1
+ import { Matrix4, Object3D, Quaternion, Vector3, Vector2, Euler, Vector4 } from "three";
6
2
  import { Mathf } from "../engine/engine_math.js";
7
3
  import { serializable } from "../engine/engine_serialization.js";
4
+ import { RGBAColor } from "./js-extensions/RGBAColor.js";
5
+ import { AnimationCurve } from "./AnimationCurve.js";
6
+ import type { Vec2, Vec3 } from "../engine/engine_types.js";
8
7
  import { Context } from "../engine/engine_setup.js";
9
- import type { Vec2, Vec3 } from "../engine/engine_types.js";
8
+ import type { EmitterShape, Particle, ShapeJSON } from "three.quarks";
9
+ import { createNoise4D, type NoiseFunction4D } from 'simplex-noise';
10
+ import { Gizmos } from "../engine/engine_gizmos.js";
10
11
  import { getParam } from "../engine/engine_utils.js";
11
- import { AnimationCurve } from "./AnimationCurve.js";
12
- import { RGBAColor } from "./js-extensions/RGBAColor.js";
13
12
 
14
13
  const debug = getParam("debugparticles");
15
14
 
@@ -180,19 +179,6 @@
180
179
  @serializable()
181
180
  curveMultiplier?: number;
182
181
 
183
- clone() {
184
- const clone = new MinMaxCurve();
185
- clone.mode = this.mode;
186
- clone.constant = this.constant;
187
- clone.constantMin = this.constantMin;
188
- clone.constantMax = this.constantMax;
189
- clone.curve = this.curve?.clone();
190
- clone.curveMin = this.curveMin?.clone();
191
- clone.curveMax = this.curveMax?.clone();
192
- clone.curveMultiplier = this.curveMultiplier;
193
- return clone;
194
- }
195
-
196
182
  evaluate(t01: number, lerpFactor?: number): number {
197
183
  const t = lerpFactor === undefined ? Math.random() : lerpFactor;
198
184
  switch (this.mode) {
@@ -613,7 +599,6 @@
613
599
  }
614
600
  getPosition(): void {
615
601
  this._vector.set(0, 0, 0);
616
-
617
602
  const pos = this._temp.copy(this.position);
618
603
  const isWorldSpace = this._space === ParticleSystemSimulationSpace.World;
619
604
  if (isWorldSpace) {
@@ -756,7 +741,7 @@
756
741
  vec.z = z;
757
742
  }
758
743
 
759
- private randomCirclePoint(pos: Vec3, radius: number, thickness: number, arg: number, vec: Vec3) {
744
+ private randomCirclePoint(pos:Vec3, radius:number, thickness:number, arg:number, vec:Vec3){
760
745
  const u = Math.random();
761
746
  const theta = 2 * Math.PI * u * (arg / 360);
762
747
  const r = Mathf.lerp(1, 1 - (Math.pow(1 - Math.random(), Math.PI)), thickness) * (radius);
@@ -987,7 +972,7 @@
987
972
  @serializable()
988
973
  worldSpace: boolean = false;
989
974
 
990
- getWidth(size: number, _life01: number, pos01: number, t: number) {
975
+ getWidth(size: number, _life01: number, pos01: number, t : number) {
991
976
  const res = this.widthOverTrail.evaluate(pos01, t);
992
977
  size *= res;
993
978
  return size;
@@ -1399,54 +1384,22 @@
1399
1384
  @serializable()
1400
1385
  mode!: ParticleSystemInheritVelocityMode;
1401
1386
 
1402
- clone() {
1403
- const ni = new InheritVelocityModule();
1404
- ni.enabled = this.enabled;
1405
- ni.curve = this.curve?.clone();
1406
- ni.curveMultiplier = this.curveMultiplier;
1407
- ni.mode = this.mode;
1408
- return ni;
1409
- }
1410
-
1411
1387
  system!: IParticleSystem;
1388
+ private _lastWorldPosition!: Vector3;
1389
+ private _velocity: Vector3 = new Vector3();
1390
+ private _temp: Vector3 = new Vector3();
1412
1391
 
1413
- private get _lastWorldPosition() {
1414
- if (!this.system['_iv_lastWorldPosition']) {
1415
- this.system['_iv_lastWorldPosition'] = new Vector3();
1416
- }
1417
- return this.system['_iv_lastWorldPosition'];
1418
- }
1419
- private get _velocity() {
1420
- if (!this.system['_iv_velocity']) {
1421
- this.system['_iv_velocity'] = new Vector3();
1422
- }
1423
- return this.system['_iv_velocity'];
1424
- }
1425
-
1426
- private readonly _temp: Vector3 = new Vector3();
1427
- private _firstUpdate: boolean = true;
1428
-
1429
- awake(system: IParticleSystem) {
1430
- this.system = system;
1431
- this.reset();
1432
- }
1433
-
1434
- reset() {
1435
- this._firstUpdate = true;
1436
- }
1437
-
1438
1392
  update(_context: Context) {
1439
1393
  if (!this.enabled) return;
1440
1394
  if (this.system.worldspace === false) return;
1441
- if (this._firstUpdate) {
1442
- this._firstUpdate = false;
1443
- this._velocity.set(0, 0, 0);
1444
- this._lastWorldPosition.copy(this.system.worldPos);
1445
- }
1446
- else if (this._lastWorldPosition) {
1395
+ if (this._lastWorldPosition) {
1447
1396
  this._velocity.copy(this.system.worldPos).sub(this._lastWorldPosition).multiplyScalar(1 / this.system.deltaTime);
1448
1397
  this._lastWorldPosition.copy(this.system.worldPos);
1449
1398
  }
1399
+ else {
1400
+ this._velocity.set(0, 0, 0);
1401
+ this._lastWorldPosition = this.system.worldPos.clone();
1402
+ }
1450
1403
  }
1451
1404
 
1452
1405
  // TODO: make work for subsystems
@@ -1460,10 +1413,8 @@
1460
1413
  }
1461
1414
  }
1462
1415
 
1463
- private _frames = 0;
1464
1416
  applyCurrent(vel: Vector3, t01: number, lerpFactor: number) {
1465
1417
  if (!this.enabled) return;
1466
- if (!this.system) return;
1467
1418
  if (this.system.worldspace === false) return;
1468
1419
  if (this.mode === ParticleSystemInheritVelocityMode.Current) {
1469
1420
  const factor = this.curve.evaluate(t01, lerpFactor);
src/engine-components/ParticleSystemSubEmitter.ts CHANGED
@@ -1,9 +1,8 @@
1
- import { Matrix4,Quaternion, Vector3 } from "three";
2
- import { type Behavior, type EmissionState, type Particle, type ParticleSystem } from "three.quarks";
3
-
1
+ import { type Behavior, type Particle, type EmissionState, type ParticleSystem } from "three.quarks";
2
+ import { Vector3, Quaternion, Matrix4 } from "three";
3
+ import type { IParticleSystem } from "./ParticleSystemModules.js";
4
4
  import { CircularBuffer } from "../engine/engine_utils.js";
5
5
  import { $particleLife, SubEmitterType } from "./ParticleSystem.js";
6
- import type { IParticleSystem } from "./ParticleSystemModules.js";
7
6
 
8
7
  const VECTOR_ONE = new Vector3(1, 1, 1);
9
8
  const VECTOR_Z = new Vector3(0, 0, 1);
src/engine-components/postprocessing/Effects/Pixelation.ts CHANGED
@@ -1,9 +1,8 @@
1
+ import { registerCustomEffectType } from "../VolumeProfile.js";
2
+ import { type EffectProviderResult, PostProcessingEffect } from "../PostProcessingEffect.js";
1
3
  import { PixelationEffect as PixelationEffectPP } from "postprocessing";
2
-
4
+ import { VolumeParameter } from "../VolumeParameter.js";
3
5
  import { serializable } from "../../../engine/engine_serialization.js";
4
- import { type EffectProviderResult, PostProcessingEffect } from "../PostProcessingEffect.js";
5
- import { VolumeParameter } from "../VolumeParameter.js";
6
- import { registerCustomEffectType } from "../VolumeProfile.js";
7
6
 
8
7
  export class PixelationEffect extends PostProcessingEffect {
9
8
  get typeName(): string {
src/engine-components/timeline/PlayableDirector.ts CHANGED
@@ -1,17 +1,16 @@
1
+ import { Animator } from '../Animator.js';
2
+ import { Behaviour, GameObject } from '../Component.js';
1
3
  import * as THREE from 'three';
2
- import { Object3D, Quaternion, Vector3 } from 'three';
3
-
4
- import { FrameEvent } from '../../engine/engine_context.js';
5
- import { isLocalNetwork } from '../../engine/engine_networking_utils.js';
6
- import type { GuidsMap } from '../../engine/engine_types.js';
7
- import { deepClone, delay, getParam } from '../../engine/engine_utils.js';
8
- import { Animator } from '../Animator.js';
9
4
  import { AudioListener } from '../AudioListener.js';
10
5
  import { AudioSource } from '../AudioSource.js';
11
- import { Behaviour, GameObject } from '../Component.js';
12
6
  import { SignalReceiver } from './SignalAsset.js';
13
7
  import * as Models from "./TimelineModels.js";
14
8
  import * as Tracks from "./TimelineTracks.js";
9
+ import { deepClone, delay, getParam } from '../../engine/engine_utils.js';
10
+ import type { GuidsMap } from '../../engine/engine_types.js';
11
+ import { Object3D, Quaternion, Vector3 } from 'three';
12
+ import { isLocalNetwork } from '../../engine/engine_networking_utils.js';
13
+ import { FrameEvent } from '../../engine/engine_context.js';
15
14
 
16
15
  const debug = getParam("debugtimeline");
17
16
 
@@ -165,9 +164,9 @@
165
164
  if (!this.isValid()) return;
166
165
  const pauseChanged = this._isPaused == true;
167
166
  this._isPaused = false;
167
+ if (pauseChanged) this.invokePauseChangedMethodsOnTracks();
168
168
  if (this._isPlaying) return;
169
169
  this._isPlaying = true;
170
- if (pauseChanged) this.invokePauseChangedMethodsOnTracks();
171
170
  if (this.waitForAudio) {
172
171
  // Make sure audio tracks have loaded at the current time
173
172
  const promises: Array<Promise<any>> = [];
@@ -519,7 +518,7 @@
519
518
  const clipModel = track.clips[i];
520
519
  const animModel = clipModel.asset as Models.AnimationClipModel;
521
520
  if (!animModel) {
522
- console.error(`Timeline ${this.name}: clip #${i} on track \"${track.name}\" has no animation data`);
521
+ console.error("MISSING anim model?", "clip#" + i, clipModel, track, this.playableAsset, this.name);
523
522
  continue;
524
523
  }
525
524
  // console.log(clipModel, track);
src/engine-components/PlayerColor.ts CHANGED
@@ -1,45 +1,40 @@
1
- import * as THREE from "three";
2
-
3
- import { WaitForSeconds } from "../engine/engine_coroutine.js";
4
1
  import { RoomEvents } from "../engine/engine_networking.js";
5
- import { PlayerState } from "../engine-components-experimental/networking/PlayerSync.js";
6
2
  import { Behaviour, GameObject } from "./Component.js";
3
+ import * as THREE from "three";
7
4
  import { AvatarMarker } from "./webxr/WebXRAvatar.js";
5
+ import { WaitForSeconds } from "../engine/engine_coroutine.js";
8
6
 
9
7
 
10
8
  export class PlayerColor extends Behaviour {
11
9
 
10
+ awake(): void {
11
+ // console.log("AWAKE", this.name);
12
+ this.context.connection.beginListen(RoomEvents.JoinedRoom, this.tryAssignColor.bind(this));
13
+ }
14
+
12
15
  private _didAssignPlayerColor: boolean = false;
13
16
 
14
17
  onEnable(): void {
15
- this.context.connection.beginListen(RoomEvents.JoinedRoom, this.tryAssignColor);
18
+ // console.log("ENABLE", this.name);
16
19
  if (!this._didAssignPlayerColor)
17
20
  this.startCoroutine(this.waitForConnection());
18
21
  }
19
- onDisable(): void {
20
- this.context.connection.stopListen(RoomEvents.JoinedRoom, this.tryAssignColor);
21
- }
22
22
 
23
23
  private *waitForConnection() {
24
- while (!this.destroyed && this.activeAndEnabled) {
24
+ while (!this.destroyed && this.enabled) {
25
25
  yield WaitForSeconds(.2);
26
26
  if (this.tryAssignColor()) break;
27
27
  }
28
+ // console.log("STOP WAITING", this.name, this.destroyed);
28
29
  }
29
30
 
30
- private tryAssignColor = () => {
31
- const marker = GameObject.getComponentInParent(this.gameObject, PlayerState);
32
- if (marker && marker.owner) {
31
+ private tryAssignColor(): boolean {
32
+ const marker = GameObject.getComponentInParent(this.gameObject, AvatarMarker);
33
+ if (marker && marker.connectionId) {
33
34
  this._didAssignPlayerColor = true;
34
- this.assignUserColor(marker.owner);
35
+ this.assignUserColor(marker.connectionId);
35
36
  return true;
36
37
  }
37
- const avatar = GameObject.getComponentInParent(this.gameObject, AvatarMarker);
38
- if (avatar?.connectionId) {
39
- this._didAssignPlayerColor = true;
40
- this.assignUserColor(avatar.connectionId);
41
- return true;
42
- }
43
38
  return false;
44
39
  }
45
40
 
src/engine-components-experimental/networking/PlayerSync.ts CHANGED
@@ -1,69 +1,39 @@
1
- import { Object3D } from "three";
2
-
1
+ import { Behaviour, Component, GameObject } from "../../engine-components/Component.js";
3
2
  import { AssetReference } from "../../engine/engine_addressables.js";
3
+ import { serializable } from "../../engine/engine_serialization_decorator.js";
4
+ import { syncField } from "../../engine/engine_networking_auto.js"
4
5
  import { RoomEvents } from "../../engine/engine_networking.js";
5
- import { syncField } from "../../engine/engine_networking_auto.js"
6
6
  import { syncDestroy } from "../../engine/engine_networking_instantiate.js";
7
- import { serializable } from "../../engine/engine_serialization_decorator.js";
8
- import { IGameObject } from "../../engine/engine_types.js";
9
- import { delay, getParam } from "../../engine/engine_utils.js";
10
- import { Behaviour, Component, GameObject } from "../../engine-components/Component.js";
7
+ import { getParam } from "../../engine/engine_utils.js";
8
+
9
+ import { Object3D } from "three";
11
10
  import { EventList } from "../../engine-components/EventList.js";
12
11
 
13
12
 
14
13
  const debug = getParam("debugplayersync");
15
14
 
16
15
  export class PlayerSync extends Behaviour {
17
-
18
- /** when enabled PlayerSync will automatically load and instantiate the assigned asset when joining a networked room */
19
- @serializable()
20
- autoSync: boolean = true;
21
-
22
- /** This asset will be loaded and instantiated when PlayerSync becomes active and joins a networked room */
23
16
  @serializable(AssetReference)
24
17
  asset?: AssetReference;
25
18
 
26
- /** Event called when */
27
19
  @serializable(EventList)
28
20
  onPlayerSpawned?: EventList;
29
21
 
30
-
31
- private _localInstance?: Promise<IGameObject>;
32
-
33
22
  awake(): void {
34
23
  this.watchTabVisible();
35
- if (!this.onPlayerSpawned) this.onPlayerSpawned = new EventList();
36
24
  }
37
25
 
38
26
  onEnable(): void {
39
27
  this.context.connection.beginListen(RoomEvents.RoomStateSent, this.onJoinedRoom);
40
- this.context.connection.beginListen(RoomEvents.JoinedRoom, this.onJoinedRoom);
41
- if (this.context.connection.isInRoom) {
42
- this.onJoinedRoom();
43
- }
44
28
  }
45
29
  onDisable(): void {
46
- this.context.connection.stopListen(RoomEvents.RoomStateSent, this.onJoinedRoom);
47
- this.context.connection.stopListen(RoomEvents.JoinedRoom, this.onJoinedRoom);
30
+ this.context.connection.stopListen(RoomEvents.RoomStateSent, this.onJoinedRoom)
48
31
  }
49
32
 
50
- private onJoinedRoom = () => {
51
- if (debug) console.log("PlayerSync.joinedRoom. autoSync is set to " + this.autoSync);
52
- if (this.autoSync) this.getInstance();
53
- }
33
+ private onJoinedRoom = async (_model) => {
34
+ if (debug) console.log("PlayerSync.onUserJoined", _model);
54
35
 
55
- async getInstance() {
56
- if (this._localInstance) return this._localInstance;
57
-
58
- if (debug) console.log("PlayerSync.createInstance", this.asset?.uri);
59
-
60
- if (!this.asset?.asset && !this.asset?.uri) {
61
- console.error("PlayerSync: can not create an instance because \"asset\" is not set!");
62
- return null;
63
- }
64
-
65
- this._localInstance = this.asset?.instantiateSynced({ parent: this.gameObject }, true) as Promise<IGameObject>;
66
- const instance = await this._localInstance;
36
+ const instance = await this.asset?.instantiateSynced({ parent: this.gameObject }, true);
67
37
  if (instance) {
68
38
  const pl = GameObject.getComponent(instance, PlayerState);
69
39
  if (pl) {
@@ -71,29 +41,15 @@
71
41
  this.onPlayerSpawned?.invoke(instance);
72
42
  }
73
43
  else {
74
- this._localInstance = undefined;
75
44
  console.error("<strong>Failed finding PlayerState on " + this.asset?.uri + "</strong>: please make sure the asset has a PlayerState component!");
76
45
  GameObject.destroySynced(instance);
77
46
  }
78
47
  }
79
- else {
80
- this._localInstance = undefined;
48
+ else{
81
49
  console.warn("PlayerSync: failed instantiating asset!")
82
50
  }
83
-
84
- return this._localInstance;
85
51
  }
86
52
 
87
- destroyInstance() {
88
- this._localInstance?.then(go => {
89
- if (debug) console.log("PlayerSync.destroyInstance", go);
90
- return GameObject.destroySynced(go);
91
- });
92
- this._localInstance = undefined;
93
- }
94
-
95
-
96
-
97
53
  private watchTabVisible() {
98
54
  window.addEventListener("visibilitychange", _ => {
99
55
  if (document.visibilityState === "visible") {
@@ -134,22 +90,19 @@
134
90
  return PlayerState._local;
135
91
  }
136
92
 
137
- static getFor(obj: Object3D | Component) {
93
+ //** use to check if a component or gameobject is part of a instance owned by the local player */
94
+ static isLocalPlayer(obj: Object3D | Component): boolean {
138
95
  if (obj instanceof Object3D) {
139
- return GameObject.getComponentInParent(obj, PlayerState);
96
+ const state = GameObject.getComponentInParent(obj, PlayerState);
97
+ return state?.isLocalPlayer ?? false;
140
98
  }
141
99
  else if (obj instanceof Component) {
142
- return GameObject.getComponentInParent(obj.gameObject, PlayerState);
100
+ const state = GameObject.getComponentInParent(obj.gameObject, PlayerState);
101
+ return state?.isLocalPlayer ?? false;
143
102
  }
144
- return undefined;
103
+ return false;
145
104
  }
146
105
 
147
- //** use to check if a component or gameobject is part of a instance owned by the local player */
148
- static isLocalPlayer(obj: Object3D | Component): boolean {
149
- const state = PlayerState.getFor(obj);
150
- return state?.isLocalPlayer ?? false;
151
- }
152
-
153
106
  // static Callback
154
107
  private static _callbacks: { [key: string]: PlayerStateEventCallback[] } = {};
155
108
  /**
@@ -199,13 +152,13 @@
199
152
  }
200
153
 
201
154
  // call local events
202
- if (!this.hasOwner) {
155
+ if(!this.hasOwner) {
203
156
  this.hasOwner = true;
204
157
  this.onFirstOwnerChangeEvent?.invoke(detail);
205
158
  }
206
159
 
207
160
  this.onOwnerChangeEvent?.invoke(detail);
208
-
161
+
209
162
  // call remote events
210
163
  if (this.owner === this.context.connection.connectionId) {
211
164
  PlayerState._local.push(this);
@@ -235,60 +188,20 @@
235
188
  }
236
189
 
237
190
 
238
- async start() {
239
- if (debug) console.log("PLAYERSTATE.START, owner: " + this.owner, this.context.connection.usersInRoom([]))
240
-
241
- // generate number from owner
242
- // if (this.owner) {
243
- // // string to number
244
- // let num = 0;
245
- // for (let i = 0; i < this.owner.length; i++) {
246
- // num += this.owner.charCodeAt(i);
247
- // }
248
- // console.log(num)
249
- // num = num / 1000
250
- // this.gameObject.position.y = num;
251
- // }
252
-
191
+ start() {
253
192
  // If a player is spawned but not in the room anymore we want to destroy it
254
193
  // this might happen in a case where all users get disconnected at once and the server
255
194
  // still has the syncInstantiate messages that are sent to all clients
256
- if (this.owner) {
257
- // a slight delay is necessary right now because the syncInstantiate call might has created this object already with the owner assigned but the user has not yet joined the room
258
- if (!this.context.connection.isInRoom) await delay(300);
259
- if (this.context.connection.userIsInRoom(this.owner) == false) {
260
- if (debug) console.log(`PlayerSync.start → doDestroy \"${this.name}\" because user \"${this.owner}\" is not in room anymore...`, "Currently in room:", ...this.context.connection.usersInRoom())
261
- this.doDestroy();
262
- }
195
+ if (this.owner && !this.context.connection.userIsInRoom(this.owner)) {
196
+ if (debug) console.log("PlayerSync.start → doDestroy because user is not in room anymore...", this)
197
+ this.doDestroy();
198
+ return;
263
199
  }
264
- else if (!this.owner) {
265
- if (debug) console.warn("PlayerState.start → owner is undefined!", this.name);
266
- // we can delete it here immediately because it is not synced anymore or the owner has left the room
267
- // we could also do this in a timeout and check if the owner is still not assigned after a second (but that would be a hack)
268
- setTimeout(() => {
269
- if (!this.destroyed && !this.owner) {
270
- if (debug) console.warn(`PlayerState.start → owner is still undefined: destroying \"${this.name}\" instance now`);
271
- this.doDestroy();
272
- }
273
- else console.log("PlayerState.start → owner is assigned", this.owner);
274
- }, 2000);
275
- }
276
200
  }
277
201
 
278
- // onEnable() {
279
- // if (debug) this.startCoroutine(this.debugRoutine());
280
- // }
281
-
282
- // *debugRoutine() {
283
- // while (!this.destroyed && this.activeAndEnabled) {
284
- // Gizmos.DrawLabel(this.gameObject.worldPosition, this.owner ?? "no owner");
285
- // yield;
286
- // }
287
- // }
288
-
289
202
  /** this tells the server that this client has been destroyed and the networking message for the instantiate will be removed */
290
203
  doDestroy() {
291
- if (debug) console.log("PlayerSync.doDestroy → syncDestroy", this.name);
204
+ if (debug) console.log("PlayerSync.doDestroy → syncDestroy", this);
292
205
  syncDestroy(this.gameObject, this.context.connection);
293
206
  }
294
207
 
src/engine-components/ui/PointerEvents.ts CHANGED
@@ -1,171 +1,102 @@
1
+ import { GameObject } from "../Component.js";
2
+ import { Input, NEPointerEvent } from "../../engine/engine_input.js";
1
3
  import { Face, Object3D, Vector3 } from "three";
2
4
 
3
- import { Input, InputEventNames, NEPointerEvent } from "../../engine/engine_input.js";
4
- import { GamepadButtonName, MouseButtonName } from "../../engine/engine_types.js";
5
- import { GameObject } from "../Component.js";
6
-
7
5
  export interface IInputEventArgs {
8
6
  get used(): boolean;
9
- use(): void;
10
- stopImmediatePropagation?(): void;
7
+ Use(): void;
8
+ StopPropagation?(): void;
11
9
  }
12
10
 
13
- /** This pointer event data object is passed to all event receivers that are currently active
14
- * It contains hit information if an object was hovered or clicked
15
- * If the event is received in onPointerDown or onPointerMove, you can call `setPointerCapture` to receive onPointerMove events even when the pointer has left the object until you call `releasePointerCapture` or when the pointerUp event happens
16
- * You can get additional information about the event or event source via the `event` property (of type `NEPointerEvent`)
17
- */
18
11
  export class PointerEventData implements IInputEventArgs {
19
12
 
20
- /** the original event */
21
- readonly event: NEPointerEvent;
13
+ // TODO: should we make this a getter and return the input used state instead? -> this.context.getPointerUsed(this.pointerId);
14
+ used: boolean = false;
22
15
 
23
- /** the index of the used device
24
- * mouse and touch are always 0, controller is the gamepad index or XRController index
25
- */
26
- get deviceIndex() { return this.event.deviceIndex; }
27
-
28
- /** a combination of the pointerId + button to uniquely identify the exact input (e.g. LeftController:Button0 = 0, RightController:Button1 = 101) */
29
- get pointerId() { return this.event.pointerId; }
30
-
31
- /**
32
- * mouse button 0 === LEFT, 1 === MIDDLE, 2 === RIGHT
33
- * */
34
- readonly button: number;
35
- readonly buttonName: MouseButtonName | GamepadButtonName | undefined;
36
- get pressure(): number { return this.event.pressure; }
37
-
38
- private _used: boolean = false;
39
- /** true when `use()` has been called */
40
- get used(): boolean {
41
- return this._used;
42
- }
43
-
44
- /** mark this event to be used */
45
16
  use() {
46
- if (this._used) return;
47
- this._used = true;
17
+ this.used = true;
48
18
  if (this.pointerId !== undefined)
49
19
  this.input.setPointerUsed(this.pointerId);
50
20
  }
51
21
 
52
- private _propagationStopped: boolean = false;
53
- get propagationStopped() {
54
- return this._propagationStopped;
22
+ stopPropagation() {
23
+ this._event?.stopImmediatePropagation();
55
24
  }
56
25
 
57
- /** Call this method to stop immediate propagation on the `event` object.
58
- * WARNING: this is currently equivalent to stopImmediatePropagation
59
- */
60
- stopPropagation() {
61
- // we currently don't have a distinction between stopPropagation and stopImmediatePropagation
62
- this._propagationStopped = true;
63
- this.event.stopImmediatePropagation();
26
+ /**@deprecated use use() */
27
+ Use() {
28
+ this.use();
64
29
  }
65
- /** Call this method to stop immediate propagation on the `event` object.
66
- */
67
- stopImmediatePropagation() {
68
- this._propagationStopped = true;
69
- this.event.stopImmediatePropagation();
70
- }
71
30
 
72
- /**@ignore internal flag, pointer captured (we dont want to see it in intellisense) */
73
- z__pointer_ctured: boolean = false;
74
- /** Call this method in `onPointerDown` or `onPointerMove` to receive onPointerMove events for this pointerId even when the pointer has left the object until you call `releasePointerCapture` or when the pointerUp event happens
75
- */
76
- setPointerCapture() {
77
- this.z__pointer_ctured = true;
31
+ /**@deprecated use stopPropagation() */
32
+ StopPropagation() {
33
+ this._event?.stopImmediatePropagation();
78
34
  }
79
- /**@ignore internal flag, pointer capture released */
80
- z__pointer_cture_rleased: boolean = false;
81
- /** call this method in `onPointerDown` or `onPointerMove` to stop receiving onPointerMove events */
82
- releasePointerCapture() {
83
- this.z__pointer_cture_rleased = true;
84
- }
85
35
 
86
-
87
36
  /** Who initiated this event */
88
37
  inputSource: Input | any;
89
38
 
90
- /** Returns the input target ray mode e.g. "screen" for 2D mouse and touch events */
91
- get mode(): XRTargetRayMode { return this.event.mode; }
92
-
93
39
  /** The object this event hit or interacted with */
94
40
  object!: THREE.Object3D;
95
41
  /** The world position of this event */
96
42
  point?: Vector3;
97
- /** The object-space normal of this event */
43
+ /** The world normal of this event */
98
44
  normal?: Vector3;
99
- /** */
100
45
  face?: Face | null;
101
- /** The distance of the hit point from the origin */
102
46
  distance?: number;
103
- /** The instance ID of an object hit by a raycast (if a instanced object was hit) */
104
47
  instanceId?: number;
105
48
 
49
+ pointerId: number | undefined;
106
50
  isDown: boolean | undefined;
107
51
  isUp: boolean | undefined;
108
52
  isPressed: boolean | undefined;
109
- isClick: boolean | undefined;
110
- isDoubleClick: boolean | undefined;
53
+ isClicked: boolean | undefined;
111
54
 
55
+ /** mouse button 0 === LEFT, 1 === MIDDLE, 2 === RIGHT */
56
+ readonly button: number | string;
112
57
 
113
58
  private input: Input;
114
59
 
115
- constructor(input: Input, event: NEPointerEvent) {
116
- this.event = event;
60
+ private _event?: NEPointerEvent;
61
+ get event() { return this._event; }
62
+
63
+ constructor(input: Input, event?: NEPointerEvent) {
64
+ this._event = event;
117
65
  this.input = input;
118
- this.button = event.button;
66
+ this.button = event?.button ?? 0;
119
67
  }
120
68
 
121
69
  clone() {
122
- const clone = new PointerEventData(this.input, this.event);
70
+ const clone = new PointerEventData(this.input, this._event);
123
71
  Object.assign(clone, this);
124
72
  return clone;
125
73
  }
126
-
127
- /**@deprecated use use() */
128
- Use() {
129
- this.use();
130
- }
131
-
132
- /**@deprecated use stopPropagation() */
133
- StopPropagation() {
134
- this.event.stopImmediatePropagation();
135
- }
136
74
  }
137
75
 
138
76
  export interface IPointerDownHandler {
139
- /** Called when a button is started to being pressed on an object (or a child object) */
140
77
  onPointerDown?(args: PointerEventData);
141
78
  }
142
79
 
143
80
  export interface IPointerUpHandler {
144
- /** Called when a button is released (which was previously pressed in `onPointerDown`) */
145
81
  onPointerUp?(args: PointerEventData);
146
82
  }
147
83
 
148
84
  export interface IPointerEnterHandler {
149
- /** Called when a pointer (mouse, touch, xr controller) starts pointing on/hovering an object (or a child object) */
150
85
  onPointerEnter?(args: PointerEventData);
151
86
  }
152
87
 
153
88
  export interface IPointerMoveHandler {
154
- /** Called when a pointer (mouse, touch, xr controller) is moving over an object (or a child object) */
155
89
  onPointerMove?(args: PointerEventData);
156
90
  }
157
91
 
158
92
  export interface IPointerExitHandler {
159
- /** Called when a pointer (mouse, touch, xr controller) exists an object (it was hovering the object before but now it's not anymore) */
160
93
  onPointerExit?(args: PointerEventData);
161
94
  }
162
95
 
163
96
  export interface IPointerClickHandler {
164
- /** Called when an object (or any child object) is clicked (needs a EventSystem in the scene) */
165
97
  onPointerClick?(args: PointerEventData);
166
98
  }
167
99
 
168
- /** Implement on your component to receive input events via the `EventSystem` component */
169
100
  export interface IPointerEventHandler extends IPointerDownHandler,
170
101
  IPointerUpHandler, IPointerEnterHandler, IPointerMoveHandler, IPointerExitHandler, IPointerClickHandler { }
171
102
 
@@ -175,30 +106,11 @@
175
106
  * @internal tests if the object has any PointerEventComponent used by the EventSystem
176
107
  * This is used to skip raycasting on objects that have no components that use pointer events
177
108
  */
178
- export function hasPointerEventComponent(obj: Object3D, event?: InputEventNames | null) {
109
+ export function hasPointerEventComponent(obj: Object3D) {
179
110
  const res = GameObject.foreachComponent(obj, comp => {
180
- // ignore disabled components
181
- if (!comp.enabled) return undefined;
182
-
183
111
  const handler = comp as IPointerEventHandler;
184
- // if a specific event is passed in, we only check for that event
185
- if (event) {
186
- switch (event) {
187
- case "pointerdown":
188
- if (handler.onPointerDown) return true;
189
- break;
190
- case "pointerup":
191
- if (handler.onPointerUp || handler.onPointerClick) return true;
192
- break;
193
- case "pointermove":
194
- if (handler.onPointerEnter || handler.onPointerExit || handler.onPointerMove) return true;
195
- break;
196
- }
197
- }
198
- else {
199
- if (handler.onPointerDown || handler.onPointerUp || handler.onPointerEnter || handler.onPointerExit || handler.onPointerClick)
200
- return true;
201
- }
112
+ if (handler.onPointerDown || handler.onPointerUp || handler.onPointerEnter || handler.onPointerExit || handler.onPointerClick)
113
+ return true;
202
114
  // undefined means continue
203
115
  return undefined;
204
116
  }, false);
src/engine-components/postprocessing/PostProcessingEffect.ts CHANGED
@@ -1,11 +1,10 @@
1
+ import { serializable } from "../../engine/engine_serialization.js";
1
2
  import { Effect, Pass } from "postprocessing";
2
-
3
+ import { VolumeParameter } from "./VolumeParameter.js";
4
+ import { Component } from "../Component.js";
5
+ import type { ISerializable, SerializationContext } from "../../engine/engine_serialization_core.js";
3
6
  import type { EditorModification, IEditorModification } from "../../engine/engine_editor-sync.js";
4
- import { serializable } from "../../engine/engine_serialization.js";
5
- import type { ISerializable, SerializationContext } from "../../engine/engine_serialization_core.js";
6
7
  import { getParam } from "../../engine/engine_utils.js";
7
- import { Component } from "../Component.js";
8
- import { VolumeParameter } from "./VolumeParameter.js";
9
8
 
10
9
  const debug = getParam("debugpost");
11
10
 
src/engine-components/postprocessing/PostProcessingHandler.ts CHANGED
@@ -1,13 +1,12 @@
1
- import { N8AOPostPass } from "n8ao";
2
- import { BloomEffect, BrightnessContrastEffect, ChromaticAberrationEffect, DepthDownsamplingPass, DepthOfFieldEffect, Effect, EffectComposer, EffectPass, HueSaturationEffect, NormalPass, Pass, PixelationEffect, RenderPass, SelectiveBloomEffect, SSAOEffect, VignetteEffect } from "postprocessing";
3
1
  import { HalfFloatType } from "three";
4
-
5
- import { showBalloonWarning } from "../../engine/debug/index.js";
6
2
  import { Context } from "../../engine/engine_setup.js";
7
- import type { Constructor } from "../../engine/engine_types.js";
8
3
  import { getParam, isMobileDevice } from "../../engine/engine_utils.js";
4
+ import { BloomEffect, BrightnessContrastEffect, ChromaticAberrationEffect, DepthDownsamplingPass, DepthOfFieldEffect, Effect, EffectComposer, EffectPass, HueSaturationEffect, NormalPass, Pass, PixelationEffect, RenderPass, SelectiveBloomEffect, SSAOEffect, VignetteEffect } from "postprocessing";
5
+ import { showBalloonWarning } from "../../engine/debug/index.js";
9
6
  import { Camera } from "../Camera.js";
10
7
  import { PostProcessingEffect } from "./PostProcessingEffect.js";
8
+ import type { Constructor } from "../../engine/engine_types.js";
9
+ import { N8AOPostPass } from "n8ao";
11
10
 
12
11
  const debug = getParam("debugpost");
13
12
 
src/engine-components-experimental/Presentation.ts CHANGED
@@ -1,5 +1,5 @@
1
+ import { Behaviour } from "../engine-components/Component.js";
1
2
  import type { KeyCode } from "../engine/engine_input.js";
2
- import { Behaviour } from "../engine-components/Component.js";
3
3
 
4
4
  export class PresentationMode extends Behaviour {
5
5
 
src/engine-components/ui/Raycaster.ts CHANGED
@@ -1,17 +1,11 @@
1
- import { SkinnedMesh } from "three";
2
-
3
- import { IRaycastOptions, RaycastOptions } from "../../engine/engine_physics.js";
4
1
  import { serializable } from "../../engine/engine_serialization.js";
5
- import { NeedleXRSession } from "../../engine/engine_xr.js";
6
- import { Behaviour } from "../Component.js";
2
+ import { RaycastOptions } from "../../engine/engine_physics.js";
3
+ import { Behaviour, Component } from "../Component.js";
7
4
  import { EventSystem } from "./EventSystem.js";
5
+ import { SkinnedMesh } from "three";
8
6
 
9
7
 
10
- /** Derive from this class to create your own custom Raycaster
11
- * If you override awake, onEnable or onDisable, be sure to call the base class methods
12
- * Implement `performRaycast` to perform your custom raycasting logic
13
- */
14
- export abstract class Raycaster extends Behaviour {
8
+ export class Raycaster extends Behaviour {
15
9
  awake(): void {
16
10
  EventSystem.createIfNoneExists(this.context);
17
11
  }
@@ -24,7 +18,9 @@
24
18
  EventSystem.get(this.context)?.unregister(this);
25
19
  }
26
20
 
27
- abstract performRaycast(_opts?: IRaycastOptions | RaycastOptions | null): THREE.Intersection[] | null;
21
+ performRaycast(_opts: RaycastOptions | null = null): THREE.Intersection[] | null {
22
+ return null;
23
+ }
28
24
  }
29
25
 
30
26
 
@@ -39,7 +35,7 @@
39
35
  this.targets = [this.gameObject];
40
36
  }
41
37
 
42
- performRaycast(opts: IRaycastOptions | RaycastOptions | null = null): THREE.Intersection[] | null {
38
+ performRaycast(opts: RaycastOptions | null = null): THREE.Intersection[] | null {
43
39
  if (!this.targets) return null;
44
40
  opts ??= new RaycastOptions();
45
41
  opts.targets = this.targets;
@@ -74,19 +70,4 @@
74
70
  }
75
71
  }
76
72
 
77
- export class SpatialGrabRaycaster extends Raycaster {
78
- performRaycast(_opts?: IRaycastOptions | RaycastOptions | null): THREE.Intersection[] | null {
79
- // ensure we're in XR, otherwise return
80
- if (!NeedleXRSession.active) return null;
81
- if (!_opts?.ray) return null;
82
73
 
83
- const rayOrigin = _opts.ray.origin;
84
- const radius = 0.01;
85
-
86
- // TODO if needed, check if the input source is a XR controller or hand
87
- // draw gizmo around ray origin
88
- // Gizmos.DrawSphere(rayOrigin, radius, 0x00ff0022);
89
-
90
- return this.context.physics.sphereOverlap(rayOrigin, radius);
91
- }
92
- }
src/engine-components/ui/RaycastUtils.ts CHANGED
@@ -1,9 +1,8 @@
1
- import { Object3D } from "three";
2
-
3
1
  import { foreachComponent } from "../../engine/engine_gameobject.js";
4
2
  import { type IComponent } from "../../engine/engine_types.js";
5
3
  import { $shadowDomOwner } from "./BaseUIComponent.js";
6
4
  import { type ICanvasGroup, type IGraphic } from "./Interfaces.js";
5
+ import { Object3D } from "three";
7
6
 
8
7
 
9
8
  export class UIRaycastUtils {
src/engine-components/ui/RectTransform.ts CHANGED
@@ -1,14 +1,13 @@
1
- import { Matrix4, Object3D, Quaternion, Vector2, Vector3 } from "three";
2
1
  import * as ThreeMeshUI from 'three-mesh-ui'
2
+ import { BaseUIComponent } from "./BaseUIComponent.js";
3
3
  import { type DocumentedOptions as ThreeMeshUIEveryOptions } from "three-mesh-ui/build/types/core/elements/MeshUIBaseElement.js";
4
-
5
- import { foreachComponentEnumerator } from "../../engine/engine_gameobject.js";
6
4
  import { serializable } from "../../engine/engine_serialization_decorator.js";
5
+ import { Matrix4, Object3D, Quaternion, Vector2, Vector3 } from "three";
7
6
  import { getParam } from "../../engine/engine_utils.js";
7
+ import { onChange } from "./Utils.js";
8
+ import { foreachComponentEnumerator } from "../../engine/engine_gameobject.js";
9
+ import { type ICanvas, type IRectTransform, type IRectTransformChangedReceiver } from "./Interfaces.js";
8
10
  import { GameObject } from '../Component.js';
9
- import { BaseUIComponent } from "./BaseUIComponent.js";
10
- import { type ICanvas, type IRectTransform, type IRectTransformChangedReceiver } from "./Interfaces.js";
11
- import { onChange } from "./Utils.js";
12
11
 
13
12
  const debug = getParam("debugui");
14
13
  const debugLayout = getParam("debuguilayout");
src/engine-components/ReflectionProbe.ts CHANGED
@@ -1,11 +1,10 @@
1
+ import { Behaviour } from "./Component.js";
1
2
  import { EquirectangularReflectionMapping, Material, Object3D, SRGBColorSpace, Texture, Vector3 } from "three";
2
-
3
3
  import { serializable } from "../engine/engine_serialization.js";
4
4
  import { Context } from "../engine/engine_setup.js";
5
5
  import type { IRenderer } from "../engine/engine_types.js";
6
+ import { BoxHelperComponent } from "./BoxHelperComponent.js";
6
7
  import { getParam } from "../engine/engine_utils.js";
7
- import { BoxHelperComponent } from "./BoxHelperComponent.js";
8
- import { Behaviour } from "./Component.js";
9
8
 
10
9
  export const debug = getParam("debugreflectionprobe");
11
10
  const disable = getParam("noreflectionprobe");
src/engine/codegen/register_types.ts CHANGED
@@ -1,5 +1,4 @@
1
- /* eslint-disable */
2
- import { TypeStore } from "./../engine_typestore.js"
1
+ import { TypeStore } from "./../engine_typestore.js"
3
2
 
4
3
  // Import types
5
4
  import { __Ignore } from "../../engine-components/codegen/components.js";
@@ -14,11 +13,11 @@
14
13
  import { Animator } from "../../engine-components/Animator.js";
15
14
  import { AnimatorController } from "../../engine-components/AnimatorController.js";
16
15
  import { Antialiasing } from "../../engine-components/postprocessing/Effects/Antialiasing.js";
16
+ import { AttachedObject } from "../../engine-components/webxr/WebXRController.js";
17
17
  import { AudioExtension } from "../../engine-components/export/usdz/extensions/behavior/AudioExtension.js";
18
18
  import { AudioListener } from "../../engine-components/AudioListener.js";
19
19
  import { AudioSource } from "../../engine-components/AudioSource.js";
20
20
  import { AudioTrackHandler } from "../../engine-components/timeline/TimelineTracks.js";
21
- import { Avatar } from "../../engine-components/webxr/Avatar.js";
22
21
  import { Avatar_Brain_LookAt } from "../../engine-components/avatar/Avatar_Brain_LookAt.js";
23
22
  import { Avatar_MouthShapes } from "../../engine-components/avatar/Avatar_MouthShapes.js";
24
23
  import { Avatar_MustacheShake } from "../../engine-components/avatar/Avatar_MustacheShake.js";
@@ -33,6 +32,7 @@
33
32
  import { BasicIKConstraint } from "../../engine-components/BasicIKConstraint.js";
34
33
  import { BehaviorExtension } from "../../engine-components/export/usdz/extensions/behavior/Behaviour.js";
35
34
  import { BehaviorModel } from "../../engine-components/export/usdz/extensions/behavior/BehavioursBuilder.js";
35
+ import { Behaviour } from "../../engine-components/Component.js";
36
36
  import { Bloom } from "../../engine-components/postprocessing/Effects/Bloom.js";
37
37
  import { BoxCollider } from "../../engine-components/Collider.js";
38
38
  import { BoxGizmo } from "../../engine-components/Gizmos.js";
@@ -53,6 +53,7 @@
53
53
  import { ColorAdjustments } from "../../engine-components/postprocessing/Effects/ColorAdjustments.js";
54
54
  import { ColorBySpeedModule } from "../../engine-components/ParticleSystemModules.js";
55
55
  import { ColorOverLifetimeModule } from "../../engine-components/ParticleSystemModules.js";
56
+ import { Component } from "../../engine-components/Component.js";
56
57
  import { ContactShadows } from "../../engine-components/ContactShadows.js";
57
58
  import { ControlTrackHandler } from "../../engine-components/timeline/TimelineTracks.js";
58
59
  import { CustomBranding } from "../../engine-components/export/usdz/USDZExporter.js";
@@ -89,6 +90,7 @@
89
90
  import { Image } from "../../engine-components/ui/Image.js";
90
91
  import { InheritVelocityModule } from "../../engine-components/ParticleSystemModules.js";
91
92
  import { InputField } from "../../engine-components/ui/InputField.js";
93
+ import { Interactable } from "../../engine-components/Interactable.js";
92
94
  import { Light } from "../../engine-components/Light.js";
93
95
  import { LimitVelocityOverLifetimeModule } from "../../engine-components/ParticleSystemModules.js";
94
96
  import { LODGroup } from "../../engine-components/LODGroup.js";
@@ -102,7 +104,6 @@
102
104
  import { MeshRenderer } from "../../engine-components/Renderer.js";
103
105
  import { MinMaxCurve } from "../../engine-components/ParticleSystemModules.js";
104
106
  import { MinMaxGradient } from "../../engine-components/ParticleSystemModules.js";
105
- import { NeedleWebXRHtmlElement } from "../../engine-components/webxr/WebXRButtons.js";
106
107
  import { NestedGltf } from "../../engine-components/NestedGltf.js";
107
108
  import { Networking } from "../../engine-components/Networking.js";
108
109
  import { NoiseModule } from "../../engine-components/ParticleSystemModules.js";
@@ -129,6 +130,7 @@
129
130
  import { PreliminaryTrigger } from "../../engine-components/export/usdz/extensions/behavior/BehaviourComponents.js";
130
131
  import { PresentationMode } from "../../engine-components-experimental/Presentation.js";
131
132
  import { RawImage } from "../../engine-components/ui/Image.js";
133
+ import { Raycaster } from "../../engine-components/ui/Raycaster.js";
132
134
  import { Rect } from "../../engine-components/ui/RectTransform.js";
133
135
  import { RectTransform } from "../../engine-components/ui/RectTransform.js";
134
136
  import { ReflectionProbe } from "../../engine-components/ReflectionProbe.js";
@@ -156,7 +158,6 @@
156
158
  import { SizeOverLifetimeModule } from "../../engine-components/ParticleSystemModules.js";
157
159
  import { SkinnedMeshRenderer } from "../../engine-components/Renderer.js";
158
160
  import { SmoothFollow } from "../../engine-components/SmoothFollow.js";
159
- import { SpatialGrabRaycaster } from "../../engine-components/ui/Raycaster.js";
160
161
  import { SpatialHtml } from "../../engine-components/ui/SpatialHtml.js";
161
162
  import { SpatialTrigger } from "../../engine-components/SpatialTrigger.js";
162
163
  import { SpatialTriggerReceiver } from "../../engine-components/SpatialTrigger.js";
@@ -171,7 +172,7 @@
171
172
  import { SyncedRoom } from "../../engine-components/SyncedRoom.js";
172
173
  import { SyncedTransform } from "../../engine-components/SyncedTransform.js";
173
174
  import { TapGestureTrigger } from "../../engine-components/export/usdz/extensions/behavior/BehaviourComponents.js";
174
- import { TeleportTarget } from "../../engine-components/webxr/TeleportTarget.js";
175
+ import { TeleportTarget } from "../../engine-components/webxr/WebXRController.js";
175
176
  import { TestRunner } from "../../engine-components/TestRunner.js";
176
177
  import { TestSimulateUserData } from "../../engine-components/TestRunner.js";
177
178
  import { Text } from "../../engine-components/ui/Text.js";
@@ -201,19 +202,23 @@
201
202
  import { Volume } from "../../engine-components/postprocessing/Volume.js";
202
203
  import { VolumeParameter } from "../../engine-components/postprocessing/VolumeParameter.js";
203
204
  import { VolumeProfile } from "../../engine-components/postprocessing/VolumeProfile.js";
205
+ import { VRUserState } from "../../engine-components/webxr/WebXRSync.js";
206
+ import { WebAR } from "../../engine-components/webxr/WebXR.js";
204
207
  import { WebARCameraBackground } from "../../engine-components/webxr/WebARCameraBackground.js";
205
208
  import { WebARSessionRoot } from "../../engine-components/webxr/WebARSessionRoot.js";
206
209
  import { WebXR } from "../../engine-components/webxr/WebXR.js";
210
+ import { WebXRAvatar } from "../../engine-components/webxr/WebXRAvatar.js";
211
+ import { WebXRController } from "../../engine-components/webxr/WebXRController.js";
207
212
  import { WebXRImageTracking } from "../../engine-components/webxr/WebXRImageTracking.js";
208
213
  import { WebXRImageTrackingModel } from "../../engine-components/webxr/WebXRImageTracking.js";
209
214
  import { WebXRPlaneTracking } from "../../engine-components/webxr/WebXRPlaneTracking.js";
215
+ import { WebXRSync } from "../../engine-components/webxr/WebXRSync.js";
210
216
  import { WebXRTrackedImage } from "../../engine-components/webxr/WebXRImageTracking.js";
211
- import { XRControllerFollow } from "../../engine-components/webxr/controllers/XRControllerFollow.js";
212
- import { XRControllerModel } from "../../engine-components/webxr/controllers/XRControllerModel.js";
213
- import { XRControllerMovement } from "../../engine-components/webxr/controllers/XRControllerMovement.js";
214
- import { XRFlag } from "../../engine-components/webxr/XRFlag.js";
217
+ import { XRFlag } from "../../engine-components/XRFlag.js";
218
+ import { XRGrabModel } from "../../engine-components/webxr/WebXRGrabRendering.js";
219
+ import { XRGrabRendering } from "../../engine-components/webxr/WebXRGrabRendering.js";
215
220
  import { XRRig } from "../../engine-components/webxr/WebXRRig.js";
216
- import { XRState } from "../../engine-components/webxr/XRFlag.js";
221
+ import { XRState } from "../../engine-components/XRFlag.js";
217
222
 
218
223
  // Register types
219
224
  TypeStore.add("__Ignore", __Ignore);
@@ -228,11 +233,11 @@
228
233
  TypeStore.add("Animator", Animator);
229
234
  TypeStore.add("AnimatorController", AnimatorController);
230
235
  TypeStore.add("Antialiasing", Antialiasing);
236
+ TypeStore.add("AttachedObject", AttachedObject);
231
237
  TypeStore.add("AudioExtension", AudioExtension);
232
238
  TypeStore.add("AudioListener", AudioListener);
233
239
  TypeStore.add("AudioSource", AudioSource);
234
240
  TypeStore.add("AudioTrackHandler", AudioTrackHandler);
235
- TypeStore.add("Avatar", Avatar);
236
241
  TypeStore.add("Avatar_Brain_LookAt", Avatar_Brain_LookAt);
237
242
  TypeStore.add("Avatar_MouthShapes", Avatar_MouthShapes);
238
243
  TypeStore.add("Avatar_MustacheShake", Avatar_MustacheShake);
@@ -247,6 +252,7 @@
247
252
  TypeStore.add("BasicIKConstraint", BasicIKConstraint);
248
253
  TypeStore.add("BehaviorExtension", BehaviorExtension);
249
254
  TypeStore.add("BehaviorModel", BehaviorModel);
255
+ TypeStore.add("Behaviour", Behaviour);
250
256
  TypeStore.add("Bloom", Bloom);
251
257
  TypeStore.add("BoxCollider", BoxCollider);
252
258
  TypeStore.add("BoxGizmo", BoxGizmo);
@@ -267,6 +273,7 @@
267
273
  TypeStore.add("ColorAdjustments", ColorAdjustments);
268
274
  TypeStore.add("ColorBySpeedModule", ColorBySpeedModule);
269
275
  TypeStore.add("ColorOverLifetimeModule", ColorOverLifetimeModule);
276
+ TypeStore.add("Component", Component);
270
277
  TypeStore.add("ContactShadows", ContactShadows);
271
278
  TypeStore.add("ControlTrackHandler", ControlTrackHandler);
272
279
  TypeStore.add("CustomBranding", CustomBranding);
@@ -303,6 +310,7 @@
303
310
  TypeStore.add("Image", Image);
304
311
  TypeStore.add("InheritVelocityModule", InheritVelocityModule);
305
312
  TypeStore.add("InputField", InputField);
313
+ TypeStore.add("Interactable", Interactable);
306
314
  TypeStore.add("Light", Light);
307
315
  TypeStore.add("LimitVelocityOverLifetimeModule", LimitVelocityOverLifetimeModule);
308
316
  TypeStore.add("LODGroup", LODGroup);
@@ -316,7 +324,6 @@
316
324
  TypeStore.add("MeshRenderer", MeshRenderer);
317
325
  TypeStore.add("MinMaxCurve", MinMaxCurve);
318
326
  TypeStore.add("MinMaxGradient", MinMaxGradient);
319
- TypeStore.add("NeedleWebXRHtmlElement", NeedleWebXRHtmlElement);
320
327
  TypeStore.add("NestedGltf", NestedGltf);
321
328
  TypeStore.add("Networking", Networking);
322
329
  TypeStore.add("NoiseModule", NoiseModule);
@@ -343,6 +350,7 @@
343
350
  TypeStore.add("PreliminaryTrigger", PreliminaryTrigger);
344
351
  TypeStore.add("PresentationMode", PresentationMode);
345
352
  TypeStore.add("RawImage", RawImage);
353
+ TypeStore.add("Raycaster", Raycaster);
346
354
  TypeStore.add("Rect", Rect);
347
355
  TypeStore.add("RectTransform", RectTransform);
348
356
  TypeStore.add("ReflectionProbe", ReflectionProbe);
@@ -370,7 +378,6 @@
370
378
  TypeStore.add("SizeOverLifetimeModule", SizeOverLifetimeModule);
371
379
  TypeStore.add("SkinnedMeshRenderer", SkinnedMeshRenderer);
372
380
  TypeStore.add("SmoothFollow", SmoothFollow);
373
- TypeStore.add("SpatialGrabRaycaster", SpatialGrabRaycaster);
374
381
  TypeStore.add("SpatialHtml", SpatialHtml);
375
382
  TypeStore.add("SpatialTrigger", SpatialTrigger);
376
383
  TypeStore.add("SpatialTriggerReceiver", SpatialTriggerReceiver);
@@ -415,16 +422,20 @@
415
422
  TypeStore.add("Volume", Volume);
416
423
  TypeStore.add("VolumeParameter", VolumeParameter);
417
424
  TypeStore.add("VolumeProfile", VolumeProfile);
425
+ TypeStore.add("VRUserState", VRUserState);
426
+ TypeStore.add("WebAR", WebAR);
418
427
  TypeStore.add("WebARCameraBackground", WebARCameraBackground);
419
428
  TypeStore.add("WebARSessionRoot", WebARSessionRoot);
420
429
  TypeStore.add("WebXR", WebXR);
430
+ TypeStore.add("WebXRAvatar", WebXRAvatar);
431
+ TypeStore.add("WebXRController", WebXRController);
421
432
  TypeStore.add("WebXRImageTracking", WebXRImageTracking);
422
433
  TypeStore.add("WebXRImageTrackingModel", WebXRImageTrackingModel);
423
434
  TypeStore.add("WebXRPlaneTracking", WebXRPlaneTracking);
435
+ TypeStore.add("WebXRSync", WebXRSync);
424
436
  TypeStore.add("WebXRTrackedImage", WebXRTrackedImage);
425
- TypeStore.add("XRControllerFollow", XRControllerFollow);
426
- TypeStore.add("XRControllerModel", XRControllerModel);
427
- TypeStore.add("XRControllerMovement", XRControllerMovement);
428
437
  TypeStore.add("XRFlag", XRFlag);
438
+ TypeStore.add("XRGrabModel", XRGrabModel);
439
+ TypeStore.add("XRGrabRendering", XRGrabRendering);
429
440
  TypeStore.add("XRRig", XRRig);
430
441
  TypeStore.add("XRState", XRState);
src/engine-components/Renderer.ts CHANGED
@@ -1,22 +1,21 @@
1
+ import { Behaviour, GameObject } from "./Component.js";
1
2
  import * as THREE from "three";
3
+ // import { RendererCustomShader } from "./RendererCustomShader.js";
4
+ import { RendererLightmap } from "./RendererLightmap.js";
5
+ import { Context, FrameEvent } from "../engine/engine_setup.js";
6
+ import { getParam } from "../engine/engine_utils.js";
7
+ import { serializable } from "../engine/engine_serialization_decorator.js";
2
8
  import { AxesHelper, Material, Matrix4, Mesh, Object3D, SkinnedMesh, Texture, Vector4 } from "three";
3
-
9
+ import { NEEDLE_render_objects } from "../engine/extensions/NEEDLE_render_objects.js";
10
+ import { NEEDLE_progressive } from "../engine/extensions/NEEDLE_progressive.js";
11
+ import { $instancingAutoUpdateBounds, $instancingRenderer, InstancingUtil, NEED_UPDATE_INSTANCE_KEY } from "../engine/engine_instancing.js";
12
+ import type { IRenderer, ISharedMaterials } from "../engine/engine_types.js";
13
+ import { ReflectionProbe } from "./ReflectionProbe.js";
14
+ import { setCustomVisibility } from "../engine/js-extensions/Layers.js";
15
+ import { isLocalNetwork } from "../engine/engine_networking_utils.js";
4
16
  import { showBalloonWarning } from "../engine/debug/index.js";
5
17
  import { Gizmos } from "../engine/engine_gizmos.js";
6
- import { $instancingAutoUpdateBounds, $instancingRenderer, InstancingUtil, NEED_UPDATE_INSTANCE_KEY } from "../engine/engine_instancing.js";
7
- import { isLocalNetwork } from "../engine/engine_networking_utils.js";
8
- import { serializable } from "../engine/engine_serialization_decorator.js";
9
- import { Context, FrameEvent } from "../engine/engine_setup.js";
10
18
  import { getTempVector } from "../engine/engine_three_utils.js";
11
- import type { IRenderer, ISharedMaterials } from "../engine/engine_types.js";
12
- import { getParam } from "../engine/engine_utils.js";
13
- import { NEEDLE_progressive } from "../engine/extensions/NEEDLE_progressive.js";
14
- import { NEEDLE_render_objects } from "../engine/extensions/NEEDLE_render_objects.js";
15
- import { setCustomVisibility } from "../engine/js-extensions/Layers.js";
16
- import { Behaviour, GameObject } from "./Component.js";
17
- import { ReflectionProbe } from "./ReflectionProbe.js";
18
- // import { RendererCustomShader } from "./RendererCustomShader.js";
19
- import { RendererLightmap } from "./RendererLightmap.js";
20
19
 
21
20
  // for staying compatible with old code
22
21
  export { InstancingUtil } from "../engine/engine_instancing.js";
@@ -254,11 +253,11 @@
254
253
  return undefined;
255
254
  }
256
255
 
257
- get sharedMaterial(): Material {
256
+ get sharedMaterial(): THREE.Material {
258
257
  return this.sharedMaterials[0];
259
258
  }
260
259
 
261
- set sharedMaterial(mat: Material) {
260
+ set sharedMaterial(mat: THREE.Material) {
262
261
  const cur = this.sharedMaterials[0];
263
262
  if (cur === mat) return;
264
263
  this.sharedMaterials[0] = mat;
@@ -266,12 +265,12 @@
266
265
  }
267
266
 
268
267
  /**@deprecated please use sharedMaterial */
269
- get material(): Material {
268
+ get material(): THREE.Material {
270
269
  return this.sharedMaterials[0];
271
270
  }
272
271
 
273
272
  /**@deprecated please use sharedMaterial */
274
- set material(mat: Material) {
273
+ set material(mat: THREE.Material) {
275
274
  this.sharedMaterial = mat;
276
275
  }
277
276
 
@@ -456,10 +455,12 @@
456
455
 
457
456
  private _isInstancingEnabled: boolean = false;
458
457
  private handles: InstanceHandle[] | null | undefined = undefined;
458
+ private prevLayers: number[] | null | undefined = undefined;
459
459
 
460
460
  private clearInstancingState() {
461
461
  this._isInstancingEnabled = false;
462
462
  this.handles = undefined;
463
+ this.prevLayers = undefined;
463
464
  }
464
465
 
465
466
  setInstancingEnabled(enabled: boolean): boolean {
@@ -605,7 +606,11 @@
605
606
  if (this._isInstancingEnabled && this.handles) {
606
607
  for (let i = 0; i < this.handles.length; i++) {
607
608
  const handle = this.handles[i];
608
- setCustomVisibility(handle.object, false);
609
+ if (!this.prevLayers) this.prevLayers = [];
610
+ const layer = handle.object.layers.mask;
611
+ if (i >= this.prevLayers.length) this.prevLayers.push(layer);
612
+ else this.prevLayers[i] = layer;
613
+ handle.object.layers.disableAll();
609
614
  }
610
615
  }
611
616
 
@@ -672,10 +677,10 @@
672
677
  }
673
678
 
674
679
  onAfterRender() {
675
- if (this._isInstancingEnabled && this.handles) {
680
+ if (this._isInstancingEnabled && this.handles && this.prevLayers && this.prevLayers.length >= this.handles.length) {
676
681
  for (let i = 0; i < this.handles.length; i++) {
677
682
  const handle = this.handles[i];
678
- setCustomVisibility(handle.object, true);
683
+ handle.object.layers.mask = this.prevLayers[i];
679
684
  }
680
685
  }
681
686
 
@@ -994,8 +999,8 @@
994
999
  this.inst = new THREE.InstancedMesh(geo, material, count);
995
1000
  this.inst[$instancingAutoUpdateBounds] = true;
996
1001
  this.inst.count = 0;
1002
+ this.inst.layers.set(2);
997
1003
  this.inst.visible = true;
998
- this.context.scene.add(this.inst);
999
1004
 
1000
1005
  // Not handled by RawShaderMaterial, so we need to set the define explicitly.
1001
1006
  // Edge case: theoretically some users of the material could use it in an
@@ -1009,25 +1014,26 @@
1009
1014
  material.defines["USE_INSTANCING"] = true;
1010
1015
  material.needsUpdate = true;
1011
1016
  }
1012
-
1017
+
1018
+ // this.inst.castShadow = true;
1019
+ // this.inst.receiveShadow = true;
1020
+ this.context.scene.add(this.inst);
1013
1021
  context.pre_render_callbacks.push(this.onBeforeRender);
1014
- context.post_render_callbacks.push(this.onAfterRender);
1022
+ // console.log(this.inst);
1023
+ // this.context.pre_render_callbacks.push(this.onPreRender.bind(this));
1024
+
1025
+ // setInterval(() => {
1026
+ // this.inst.visible = !this.inst.visible;
1027
+ // }, 500);
1015
1028
  }
1016
1029
 
1017
1030
  private onBeforeRender = () => {
1018
- // ensure the instanced mesh is rendered / has correct layers
1019
- this.inst.layers.enableAll();
1020
-
1021
1031
  if (this._needUpdateBounds && this.inst[$instancingAutoUpdateBounds] === true) {
1022
1032
  if (debugInstancing)
1023
1033
  console.log("Update instancing bounds", this.name, this.inst.matrixWorldNeedsUpdate);
1024
1034
  this.updateBounds();
1025
1035
  }
1026
1036
  }
1027
- private onAfterRender = () => {
1028
- // hide the instanced mesh again when its not being rendered (for raycasting we still use the original object)
1029
- this.inst.layers.disableAll();
1030
- }
1031
1037
 
1032
1038
  private randomColor() {
1033
1039
  return new THREE.Color(Math.random(), Math.random(), Math.random());
@@ -1070,7 +1076,7 @@
1070
1076
  if (this.inst.count > 0)
1071
1077
  this.inst.visible = true;
1072
1078
 
1073
- if (debugInstancing) console.log("Added", this.name, this.inst.count);
1079
+ // console.log("Added", this.name, this.inst.count, this.handles);
1074
1080
  }
1075
1081
 
1076
1082
  remove(handle: InstanceHandle) {
@@ -1110,7 +1116,6 @@
1110
1116
  this.inst.visible = false;
1111
1117
 
1112
1118
  this.inst.instanceMatrix.needsUpdate = true;
1113
- if (debugInstancing) console.log("Removed", this.name, this.inst.count);
1114
1119
  }
1115
1120
 
1116
1121
  updateInstance(mat: THREE.Matrix4, index: number) {
src/engine-components/RendererLightmap.ts CHANGED
@@ -1,5 +1,4 @@
1
- import { Material, Mesh, ShaderMaterial, Texture, Vector4,type WebGLProgramParametersWithUniforms } from "three";
2
-
1
+ import { Material, Mesh, type Shader, ShaderMaterial, Texture, Vector4 } from "three";
3
2
  import type { Context, OnRenderCallback } from "../engine/engine_setup.js";
4
3
  import { getParam } from "../engine/engine_utils.js";
5
4
 
@@ -100,7 +99,7 @@
100
99
  }
101
100
  }
102
101
 
103
- private onBeforeCompile = (shader: WebGLProgramParametersWithUniforms, _) => {
102
+ private onBeforeCompile = (shader: Shader, _) => {
104
103
  if (debug) console.log("Lightmaps, before compile", shader)
105
104
  //@ts-ignore
106
105
  shader.lightMapUv = "uv1";
src/engine-components/js-extensions/RGBAColor.ts CHANGED
@@ -1,7 +1,6 @@
1
+ import { Mathf } from "../../engine/engine_math.js";
1
2
  import { Color } from "three";
2
3
 
3
- import { Mathf } from "../../engine/engine_math.js";
4
-
5
4
  export class RGBAColor extends Color {
6
5
  alpha: number = 1;
7
6
 
src/engine-components/RigidBody.ts CHANGED
@@ -1,14 +1,13 @@
1
+ import { Behaviour } from "./Component.js";
1
2
  import * as THREE from 'three'
3
+ import { getWorldPosition } from "../engine/engine_three_utils.js";
4
+ import { serializable } from "../engine/engine_serialization_decorator.js";
5
+ import { Watch } from "../engine/engine_utils.js";
2
6
  import { Matrix4, Object3D, Vector3 } from "three";
3
-
7
+ import type { IRigidbody, Vec3 } from "../engine/engine_types.js";
4
8
  import { CollisionDetectionMode, RigidbodyConstraints } from "../engine/engine_physics.types.js";
5
- import { serializable } from "../engine/engine_serialization_decorator.js";
9
+ import { validate } from "../engine/engine_util_decorator.js";
6
10
  import { Context, FrameEvent } from "../engine/engine_setup.js";
7
- import { getWorldPosition } from "../engine/engine_three_utils.js";
8
- import type { IRigidbody, Vec3 } from "../engine/engine_types.js";
9
- import { validate } from "../engine/engine_util_decorator.js";
10
- import { Watch } from "../engine/engine_utils.js";
11
- import { Behaviour } from "./Component.js";
12
11
 
13
12
  class TransformWatch {
14
13
 
src/engine-components/SceneSwitcher.ts CHANGED
@@ -1,12 +1,11 @@
1
- import { Object3D } from "three";
2
-
3
1
  import { AssetReference } from "../engine/engine_addressables.js";
4
- import { registerObservableAttribute } from "../engine/engine_element_extras.js";
5
2
  import { InputEvents } from "../engine/engine_input.js";
6
3
  import { isLocalNetwork } from "../engine/engine_networking_utils.js";
4
+ import { delay, getParam, setParamWithoutReload } from "../engine/engine_utils.js";
7
5
  import { serializable } from "../engine/engine_serialization.js";
8
- import { delay, getParam, setParamWithoutReload } from "../engine/engine_utils.js";
9
6
  import { Behaviour, GameObject } from "./Component.js";
7
+ import { registerObservableAttribute } from "../engine/engine_element_extras.js";
8
+ import { Object3D } from "three";
10
9
 
11
10
  const debug = getParam("debugsceneswitcher");
12
11
 
@@ -126,9 +125,9 @@
126
125
 
127
126
  async onEnable() {
128
127
  globalThis.addEventListener("popstate", this.onPopState);
129
- this.context.input.addEventListener(InputEvents.KeyDown, this.onInputKeyDown);
130
- this.context.input.addEventListener(InputEvents.PointerMove, this.onInputPointerMove);
131
- this.context.input.addEventListener(InputEvents.PointerUp, this.onInputPointerUp);
128
+ this.context.input.addEventListener(InputEvents.KeyDown, this.onKeyDown);
129
+ this.context.input.addEventListener(InputEvents.PointerMove, this.onPointerMove);
130
+ this.context.input.addEventListener(InputEvents.PointerUp, this.onPointerUp);
132
131
 
133
132
  if (!this._engineElementOverserver) {
134
133
  this._engineElementOverserver = new MutationObserver((mutations) => {
@@ -173,9 +172,9 @@
173
172
 
174
173
  onDisable(): void {
175
174
  globalThis.removeEventListener("popstate", this.onPopState);
176
- this.context.input.removeEventListener(InputEvents.KeyDown, this.onInputKeyDown);
177
- this.context.input.removeEventListener(InputEvents.PointerMove, this.onInputPointerMove);
178
- this.context.input.removeEventListener(InputEvents.PointerUp, this.onInputPointerUp);
175
+ this.context.input.removeEventListener(InputEvents.KeyDown, this.onKeyDown);
176
+ this.context.input.removeEventListener(InputEvents.PointerMove, this.onPointerMove);
177
+ this.context.input.removeEventListener(InputEvents.PointerUp, this.onPointerUp);
179
178
  this._preloadScheduler?.stop();
180
179
  }
181
180
 
@@ -203,7 +202,7 @@
203
202
 
204
203
  private normalizedSwipeThresholdX = 0.1;
205
204
  private _didSwipe: boolean = false;
206
- private onInputPointerMove = (e: any) => {
205
+ private onPointerMove = (e: any) => {
207
206
  if (!this.useSwipe) return;
208
207
  if (!this._didSwipe && e.button === 0 && e.pointerType === "touch" && this.context.input.getPointerPressedCount() === 1) {
209
208
  const delta = this.context.input.getPointerPositionDelta(e.button);
@@ -221,13 +220,13 @@
221
220
  }
222
221
  }
223
222
 
224
- private onInputPointerUp = (e: any) => {
223
+ private onPointerUp = (e: any) => {
225
224
  if (e.button === 0) {
226
225
  this._didSwipe = false;
227
226
  }
228
227
  };
229
228
 
230
- private onInputKeyDown = (e: any) => {
229
+ private onKeyDown = (e: any) => {
231
230
  if (!this.useKeyboard) return;
232
231
  if (!this.scenes) return;
233
232
  const key = e.key.toLowerCase();
src/engine/xr/SceneTransition.ts DELETED
@@ -1,77 +0,0 @@
1
- import { Camera, DoubleSide, Mesh, MeshBasicMaterial, PlaneGeometry } from "three";
2
-
3
- import { Mathf } from "../engine_math.js";
4
-
5
- export class SceneTransition {
6
-
7
- private readonly _fadeToColorQuad: Mesh;
8
- private readonly _fadeToColorMaterial: MeshBasicMaterial;
9
-
10
- constructor() {
11
- this._fadeToColorMaterial = new MeshBasicMaterial({
12
- color: 0x000000,
13
- transparent: true,
14
- depthTest: false,
15
- fog: false,
16
- side: DoubleSide,
17
- });
18
- this._fadeToColorQuad = new Mesh(new PlaneGeometry(10, 10), this._fadeToColorMaterial);
19
- }
20
-
21
- dispose() {
22
- this._fadeToColorQuad.geometry.dispose();
23
- this._fadeToColorMaterial.dispose();
24
- }
25
-
26
- update(camera: Camera, dt: number) {
27
- const quad = this._fadeToColorQuad;
28
- const mat = this._fadeToColorMaterial;
29
-
30
- // make sure the quad is in the scene
31
- if (quad.parent !== camera && mat.opacity > 0) {
32
- camera.add(quad);
33
- }
34
- else if (mat.opacity === 0) {
35
- quad.removeFromParent();
36
- }
37
- quad.layers.set(2);
38
- quad.material = this._fadeToColorMaterial!;
39
- quad.position.z = -1;
40
- // perform the fade
41
- const fadeValue = this._requestedFadeValue;
42
- mat.opacity = Mathf.lerp(mat.opacity, fadeValue, dt / .03);
43
-
44
- // check if we're close enough to the desired value:
45
- if (Math.abs(mat.opacity - fadeValue) <= .01) {
46
- if (this._transitionResolve) {
47
- this._transitionResolve();
48
- this._transitionResolve = null;
49
- this._transitionPromise = null;
50
- this._requestedFadeValue = 0;
51
- }
52
- }
53
- }
54
- remove() {
55
- this._fadeToColorQuad.removeFromParent();
56
- }
57
-
58
- /** Call to fade rendering to black for a short moment (the returned promise will be resolved when fully black)
59
- * This can be used to mask scene transitions or teleportation
60
- * @returns a promise that is resolved when the screen is fully black
61
- * @example `fadeTransition().then(() => { <fully_black> })`
62
- */
63
- fadeTransition() {
64
- if (this._transitionPromise) return this._transitionPromise;
65
- this._requestedFadeValue = 1;
66
- const promise = new Promise<void>(resolve => {
67
- this._transitionResolve = resolve;
68
- });
69
- this._transitionPromise = promise;
70
- return promise;
71
- }
72
-
73
-
74
- private _requestedFadeValue: number = 0;
75
- private _transitionPromise: Promise<void> | null = null;
76
- private _transitionResolve: (() => void) | null = null;
77
- }
src/engine-schemes/schemes.ts CHANGED
@@ -1,8 +1,7 @@
1
1
 
2
2
  import * as flatbuffers from "flatbuffers"
3
-
3
+ import { Transform } from "./transform.js";
4
4
  import { SyncedTransformModel } from "./synced-transform-model.js";
5
- import { Transform } from "./transform.js";
6
5
 
7
6
  // registry
8
7
  export const binaryIdentifierCasts : {[key:string] : (bin:flatbuffers.ByteBuffer) => object} = {};
src/engine-components/ScreenCapture.ts CHANGED
@@ -1,12 +1,12 @@
1
+ import { Behaviour, GameObject } from "./Component.js";
2
+ import { AspectMode, VideoPlayer } from "./VideoPlayer.js";
3
+ import { serializable } from "../engine/engine_serialization.js";
4
+ import type { IPointerClickHandler, PointerEventData } from "./ui/PointerEvents.js";
5
+ import { AudioSource } from "./AudioSource.js";
6
+ import { delay, getParam } from "../engine/engine_utils.js";
1
7
  import { showBalloonWarning } from "../engine/debug/index.js";
8
+ import { NetworkedStreams, disposeStream, StreamReceivedEvent, StreamEndedEvent, PeerHandle, NetworkedStreamEvents } from "../engine/engine_networking_streams.js";
2
9
  import { RoomEvents } from "../engine/engine_networking.js";
3
- import { disposeStream, NetworkedStreamEvents,NetworkedStreams, PeerHandle, StreamEndedEvent, StreamReceivedEvent } from "../engine/engine_networking_streams.js";
4
- import { serializable } from "../engine/engine_serialization.js";
5
- import { delay, getParam } from "../engine/engine_utils.js";
6
- import { AudioSource } from "./AudioSource.js";
7
- import { Behaviour, GameObject } from "./Component.js";
8
- import type { IPointerClickHandler, PointerEventData } from "./ui/PointerEvents.js";
9
- import { AspectMode, VideoPlayer } from "./VideoPlayer.js";
10
10
 
11
11
  const debug = getParam("debugscreensharing");
12
12
 
@@ -146,7 +146,6 @@
146
146
  delay(1000).then(() => {
147
147
  if (this.enabled && this.autoConnect && !this.isReceiving && !this.isSending && this.context.connection.isInRoom)
148
148
  this.share()
149
- return 0;
150
149
  });
151
150
  }
152
151
  }
@@ -182,7 +181,7 @@
182
181
  if (this._activeShareRequest) return this._activeShareRequest;
183
182
  this._activeShareRequest = this.internalShare(opts);
184
183
  return this._activeShareRequest.then(() =>{
185
- return this._activeShareRequest = null;
184
+ this._activeShareRequest = null;
186
185
  })
187
186
  }
188
187
 
src/engine-components/postprocessing/Effects/ScreenspaceAmbientOcclusion.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import { BlendFunction, DepthDownsamplingPass, NormalPass, SSAOEffect } from "postprocessing";
2
2
  import { Color, PerspectiveCamera } from "three";
3
-
4
3
  import { serializable } from "../../../engine/engine_serialization.js";
5
4
  import { type EffectProviderResult, PostProcessingEffect } from "../PostProcessingEffect.js";
6
5
  import { VolumeParameter } from "../VolumeParameter.js";
src/engine-components/postprocessing/Effects/ScreenspaceAmbientOcclusionN8.ts CHANGED
@@ -1,11 +1,10 @@
1
- import { N8AOPostPass } from "n8ao";
2
1
  import { Color, NeverDepth, PerspectiveCamera } from "three";
3
-
4
2
  import { serializable } from "../../../engine/engine_serialization.js";
5
- import { validate } from "../../../engine/engine_util_decorator.js";
6
3
  import { type EffectProviderResult, PostProcessingEffect } from "../PostProcessingEffect.js";
7
4
  import { VolumeParameter } from "../VolumeParameter.js";
8
5
  import { registerCustomEffectType } from "../VolumeProfile.js";
6
+ import { N8AOPostPass } from "n8ao";
7
+ import { validate } from "../../../engine/engine_util_decorator.js";
9
8
 
10
9
  // https://github.com/N8python/n8ao
11
10
 
src/engine-components/ShadowCatcher.ts CHANGED
@@ -1,9 +1,8 @@
1
- import { AdditiveBlending, Material, Mesh, MeshBasicMaterial, MeshStandardMaterial,ShadowMaterial } from "three";
2
-
3
- import { ObjectUtils, PrimitiveType } from "../engine/engine_create_objects.js";
4
- import { serializable } from "../engine/engine_serialization_decorator.js";
5
1
  import { Behaviour, GameObject } from "./Component.js";
6
2
  import { RGBAColor } from "./js-extensions/RGBAColor.js";
3
+ import { ShadowMaterial, AdditiveBlending, Material, MeshBasicMaterial, Mesh, MeshStandardMaterial } from "three";
4
+ import { serializable } from "../engine/engine_serialization_decorator.js";
5
+ import { ObjectUtils, PrimitiveType } from "../engine/engine_create_objects.js";
7
6
  import { Renderer } from "./Renderer.js";
8
7
 
9
8
  enum ShadowMode {
src/engine-components/timeline/SignalAsset.ts CHANGED
@@ -1,7 +1,7 @@
1
+ import { EventList } from "../EventList.js";
2
+ import { Behaviour } from "../Component.js";
1
3
  import { serializable } from "../../engine/engine_serialization_decorator.js";
2
4
  import { getParam } from "../../engine/engine_utils.js";
3
- import { Behaviour } from "../Component.js";
4
- import { EventList } from "../EventList.js";
5
5
 
6
6
  const debug = getParam("debugsignals")
7
7
 
src/engine-components/Skybox.ts CHANGED
@@ -1,16 +1,15 @@
1
+ import { serializable } from "../engine/engine_serialization_decorator.js";
2
+ import { Behaviour, GameObject } from "./Component.js";
3
+ import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader.js';
4
+ import { EXRLoader } from "three/examples/jsm/loaders/EXRLoader.js";
1
5
  import { EquirectangularRefractionMapping, NeverDepth, SRGBColorSpace, sRGBEncoding, Texture, TextureLoader } from "three"
2
- import { EXRLoader } from "three/examples/jsm/loaders/EXRLoader.js";
3
- import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader.js';
4
-
5
- import { disposeObjectResources, setDisposable } from "../engine/engine_assetdatabase.js";
6
+ import { syncField } from "../engine/engine_networking_auto.js";
7
+ import { Camera, ClearFlags } from "./Camera.js";
8
+ import { PromiseAllWithErrors, addAttributeChangeCallback, getParam, removeAttributeChangeCallback } from "../engine/engine_utils.js";
6
9
  import { ContextRegistry } from "../engine/engine_context_registry.js";
7
10
  import { registerObservableAttribute } from "../engine/engine_element_extras.js";
8
- import { syncField } from "../engine/engine_networking_auto.js";
9
- import { serializable } from "../engine/engine_serialization_decorator.js";
10
11
  import { type IContext } from "../engine/engine_types.js";
11
- import { addAttributeChangeCallback, getParam, PromiseAllWithErrors, removeAttributeChangeCallback } from "../engine/engine_utils.js";
12
- import { Camera, ClearFlags } from "./Camera.js";
13
- import { Behaviour, GameObject } from "./Component.js";
12
+ import { disposeObjectResources, setDisposable } from "../engine/engine_assetdatabase.js";
14
13
 
15
14
  const debug = getParam("debugskybox");
16
15
 
@@ -93,7 +92,7 @@
93
92
  const entry = cache.shift();
94
93
  if (entry) { disposeCachedTexture(entry.texture); }
95
94
  }
96
- texture.then(t => { return setDisposable(t, false) });
95
+ texture.then(t => setDisposable(t, false));
97
96
  cache.push({ src, texture });
98
97
  }
99
98
 
src/engine-components/SmoothFollow.ts CHANGED
@@ -1,12 +1,11 @@
1
+ import { Camera } from "./Camera.js";
2
+ import { Behaviour, GameObject } from "./Component.js";
1
3
  import * as THREE from "three";
2
- import { Object3D } from "three";
3
-
4
4
  import { Mathf } from "../engine/engine_math.js";
5
- import { Axes } from "../engine/engine_physics.types.js";
6
5
  import { serializable } from "../engine/engine_serialization_decorator.js";
6
+ import { Object3D } from "three";
7
7
  import { getWorldPosition, getWorldQuaternion } from "../engine/engine_three_utils.js";
8
- import { Camera } from "./Camera.js";
9
- import { Behaviour, GameObject } from "./Component.js";
8
+ import { Axes } from "../engine/engine_physics.types.js";
10
9
 
11
10
  export class SmoothFollow extends Behaviour {
12
11
 
src/engine-components/ui/SpatialHtml.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  import * as THREE from 'three'
2
2
  import { HTMLMesh } from 'three/examples/jsm/interactive/HTMLMesh.js';
3
3
  import { InteractiveGroup } from 'three/examples/jsm/interactive/InteractiveGroup.js';
4
-
5
4
  import { getWorldEuler, getWorldRotation, setWorldRotationXYZ } from '../../engine/engine_three_utils.js';
6
5
  import { Behaviour } from '../Component.js';
7
6
 
src/engine-components/SpatialTrigger.ts CHANGED
@@ -1,10 +1,9 @@
1
1
  import { BoxHelper, Layers } from "three";
2
-
2
+ import { Behaviour, GameObject } from "./Component.js";
3
+ import { BoxHelperComponent } from "./BoxHelperComponent.js"
4
+ import { EventList } from "./EventList.js";
3
5
  import { serializable } from "../engine/engine_serialization_decorator.js";
4
6
  import { getParam } from "../engine/engine_utils.js";
5
- import { BoxHelperComponent } from "./BoxHelperComponent.js"
6
- import { Behaviour, GameObject } from "./Component.js";
7
- import { EventList } from "./EventList.js";
8
7
 
9
8
  const debug = getParam("debugspatialtrigger");
10
9
 
src/engine-components/SpectatorCamera.ts CHANGED
@@ -1,21 +1,21 @@
1
+ import { Behaviour, Component, GameObject } from "./Component.js";
2
+ import { Camera } from "./Camera.js";
1
3
  import * as THREE from "three";
4
+ import { OrbitControls } from "./OrbitControls.js";
5
+ import { WebXR, WebXREvent } from "./webxr/WebXR.js";
6
+ import { AvatarMarker } from "./webxr/WebXRAvatar.js";
7
+ import { XRStateFlag } from "./XRFlag.js";
8
+ import { SmoothFollow } from "./SmoothFollow.js";
2
9
  import { Object3D } from "three";
3
-
4
10
  import { InputEvents } from "../engine/engine_input.js";
11
+ import { Context } from "../engine/engine_setup.js";
12
+ import { getParam } from "../engine/engine_utils.js";
13
+ import { PlayerView, ViewDevice } from "../engine/engine_playerview.js";
14
+ import { RaycastOptions } from "../engine/engine_physics.js";
5
15
  import { RoomEvents } from "../engine/engine_networking.js";
16
+ import type { ICamera } from "../engine/engine_types.js";
6
17
  import type { IModel } from "../engine/engine_networking_types.js";
7
- import { RaycastOptions } from "../engine/engine_physics.js";
8
- import { PlayerView, ViewDevice } from "../engine/engine_playerview.js";
9
18
  import { serializable } from "../engine/engine_serialization.js";
10
- import { Context } from "../engine/engine_setup.js";
11
- import type { ICamera } from "../engine/engine_types.js";
12
- import { getParam } from "../engine/engine_utils.js";
13
- import { Camera } from "./Camera.js";
14
- import { Behaviour, Component, GameObject } from "./Component.js";
15
- import { OrbitControls } from "./OrbitControls.js";
16
- import { SmoothFollow } from "./SmoothFollow.js";
17
- import { AvatarMarker } from "./webxr/WebXRAvatar.js";
18
- import { XRStateFlag } from "./webxr/XRFlag.js";
19
19
 
20
20
 
21
21
  export enum SpectatorMode {
@@ -145,11 +145,23 @@
145
145
  if (!this._handler && this.cam)
146
146
  this._handler = new SpectatorHandler(this.context, this.cam, this);
147
147
 
148
+
149
+ this.eventSub_WebXRRequestStartEvent = this.onXRSessionRequestStart.bind(this);
150
+ this.eventSub_WebXRStartEvent = this.onXRSessionStart.bind(this);
151
+ this.eventSub_WebXREndEvent = this.onXRSessionEnded.bind(this);
152
+
153
+ WebXR.addEventListener(WebXREvent.RequestVRSession, this.eventSub_WebXRRequestStartEvent);
154
+ WebXR.addEventListener(WebXREvent.XRStarted, this.eventSub_WebXRStartEvent);
155
+ WebXR.addEventListener(WebXREvent.XRStopped, this.eventSub_WebXREndEvent);
156
+
148
157
  this.orbit = GameObject.getComponent(this.context.mainCamera, OrbitControls);
149
158
  }
150
159
 
151
160
  onDestroy(): void {
152
161
  this.stopSpectating();
162
+ WebXR.removeEventListener(WebXREvent.RequestVRSession, this.eventSub_WebXRStartEvent);
163
+ WebXR.removeEventListener(WebXREvent.XRStarted, this.eventSub_WebXRStartEvent);
164
+ WebXR.removeEventListener(WebXREvent.XRStopped, this.eventSub_WebXREndEvent);
153
165
  this._handler?.destroy();
154
166
  this._networking?.destroy();
155
167
  }
@@ -161,13 +173,13 @@
161
173
  return standalone && !isHololens;
162
174
  }
163
175
 
164
- onBeforeXR(_evt) {
176
+ private onXRSessionRequestStart(_evt) {
165
177
  if (!this.isSupportedPlatform()) return;
166
178
  GameObject.setActive(this.gameObject, true);
167
179
  }
168
180
 
169
181
 
170
- onEnterXR(_evt) {
182
+ private onXRSessionStart(_evt) {
171
183
  if (!this.isSupportedPlatform()) return;
172
184
  if (debug) console.log(this.context.mainCamera);
173
185
  if (this.context.mainCamera) {
@@ -175,7 +187,7 @@
175
187
  }
176
188
  }
177
189
 
178
- onLeaveXR(_evt) {
190
+ private onXRSessionEnded(_evt) {
179
191
  this.context.removeCamera(this.cam as ICamera);
180
192
  GameObject.setActive(this.gameObject, false);
181
193
  if (this.orbit) this.orbit.enabled = true;
@@ -212,16 +224,14 @@
212
224
  const previousRenderTarget = renderer.getRenderTarget();
213
225
  let oldFramebuffer: WebGLFramebuffer | null = null;
214
226
 
215
- const webglState = renderer.state as THREE.WebGLState & { bindXRFramebuffer?: Function };
216
-
217
227
  // seems that in some cases, renderer.getRenderTarget returns null
218
228
  // even when we're rendering to a headset.
219
229
  if (!previousRenderTarget) {
220
- if (!renderer.state.bindFramebuffer || !webglState.bindXRFramebuffer)
230
+ if (!renderer.state.bindFramebuffer || !renderer.state.bindXRFramebuffer)
221
231
  return;
222
232
 
223
233
  oldFramebuffer = renderer["_framebuffer"];
224
- webglState.bindXRFramebuffer(null);
234
+ renderer.state.bindXRFramebuffer(null);
225
235
  }
226
236
 
227
237
  this.setAvatarFlagsBeforeRender();
@@ -269,8 +279,8 @@
269
279
 
270
280
  if (previousRenderTarget)
271
281
  renderer.setRenderTarget(previousRenderTarget);
272
- else if (webglState.bindXRFramebuffer)
273
- webglState.bindXRFramebuffer(oldFramebuffer);
282
+ else
283
+ renderer.state.bindXRFramebuffer(oldFramebuffer);
274
284
 
275
285
  this.resetAvatarFlags();
276
286
  }
@@ -279,7 +289,7 @@
279
289
  const isFirstPersonMode = this._mode === SpectatorMode.FirstPerson;
280
290
 
281
291
  for (const av of AvatarMarker.instances) {
282
- if (av.avatar && "isLocalAvatar" in av.avatar && "flags" in av.avatar) {
292
+ if (av.avatar && "isLocalAvatar" in av.avatar) {
283
293
  let mask = XRStateFlag.All;
284
294
  if (this.isSpectatingSelf)
285
295
  mask = isFirstPersonMode && av.avatar.isLocalAvatar ? XRStateFlag.FirstPerson : XRStateFlag.ThirdPerson;
@@ -298,7 +308,7 @@
298
308
  const flags = av.avatar.flags;
299
309
  if (!flags) continue;
300
310
  for (const flag of flags) {
301
- if ("isLocalAvatar" in av.avatar && av.avatar?.isLocalAvatar) {
311
+ if (av.avatar?.isLocalAvatar) {
302
312
  flag.UpdateVisible(XRStateFlag.FirstPerson);
303
313
  }
304
314
  else {
src/engine-components/SpriteRenderer.ts CHANGED
@@ -1,10 +1,9 @@
1
+ import { Behaviour } from "./Component.js";
1
2
  import * as THREE from "three";
3
+ import { serializable, serializeable } from "../engine/engine_serialization_decorator.js";
2
4
  import { Material, NearestFilter, Texture } from "three";
3
-
4
- import { serializable, serializeable } from "../engine/engine_serialization_decorator.js";
5
+ import { RGBAColor } from "./js-extensions/RGBAColor.js";
5
6
  import { getParam } from "../engine/engine_utils.js";
6
- import { Behaviour } from "./Component.js";
7
- import { RGBAColor } from "./js-extensions/RGBAColor.js";
8
7
 
9
8
  const debug = getParam("debugspriterenderer");
10
9
  const showWireframe = getParam("wireframe");
src/engine-components/SyncedCamera.ts CHANGED
@@ -1,20 +1,20 @@
1
- import { Builder } from "flatbuffers";
2
- import { Object3D } from "three";
3
-
4
- import { isDevEnvironment } from "../engine/debug/index.js";
5
- import { AssetReference } from "../engine/engine_addressables.js";
6
- import { InstantiateOptions } from "../engine/engine_gameobject.js";
7
- import { InstancingUtil } from "../engine/engine_instancing.js";
8
1
  import { NetworkConnection } from "../engine/engine_networking.js";
9
- import { ViewDevice } from "../engine/engine_playerview.js";
10
- import { serializable } from "../engine/engine_serialization_decorator.js";
2
+ import { Behaviour, GameObject } from "./Component.js";
3
+ import { Camera } from "./Camera.js";
11
4
  import * as utils from "../engine/engine_three_utils.js"
12
- import { registerBinaryType } from "../engine-schemes/schemes.js";
5
+ import { WebXR } from "./webxr/WebXR.js";
6
+ import { Builder } from "flatbuffers";
13
7
  import { SyncedCameraModel } from "../engine-schemes/synced-camera-model.js";
14
8
  import { Vec3 } from "../engine-schemes/vec3.js";
15
- import { Camera } from "./Camera.js";
16
- import { Behaviour, GameObject } from "./Component.js";
9
+ import { registerBinaryType } from "../engine-schemes/schemes.js";
10
+ import { InstancingUtil } from "../engine/engine_instancing.js";
11
+ import { serializable } from "../engine/engine_serialization_decorator.js";
12
+ import { Object3D } from "three";
17
13
  import { AvatarMarker } from "./webxr/WebXRAvatar.js";
14
+ import { AssetReference } from "../engine/engine_addressables.js";
15
+ import { ViewDevice } from "../engine/engine_playerview.js";
16
+ import { InstantiateOptions } from "../engine/engine_gameobject.js";
17
+ import { isDevEnvironment } from "../engine/debug/index.js";
18
18
 
19
19
  const SyncedCameraModelIdentifier = "SCAM";
20
20
  registerBinaryType(SyncedCameraModelIdentifier, SyncedCameraModel.getRootAsSyncedCameraModel);
@@ -130,7 +130,7 @@
130
130
  }
131
131
  }
132
132
 
133
- if (this.context.isInXR) return;
133
+ if (WebXR.IsInWebXR) return;
134
134
 
135
135
  const cam = this.context.mainCamera
136
136
  if (cam === null) {
src/engine-components/SyncedRoom.ts CHANGED
@@ -1,7 +1,7 @@
1
+ import { Behaviour } from "./Component.js";
2
+ import * as utils from "../engine/engine_utils.js"
1
3
  import { serializable } from "../engine/engine_serialization_decorator.js";
2
- import * as utils from "../engine/engine_utils.js"
3
4
  import { getParam } from "../engine/engine_utils.js";
4
- import { Behaviour } from "./Component.js";
5
5
 
6
6
  const viewParamName = "view";
7
7
  const debug = utils.getParam("debugsyncedroom");
src/engine-components/SyncedTransform.ts CHANGED
@@ -1,17 +1,15 @@
1
- import * as flatbuffers from "flatbuffers";
2
1
  import * as THREE from 'three'
3
-
4
- import { InstancingUtil } from "../engine/engine_instancing.js";
5
- import { onUpdate } from '../engine/engine_lifecycle_api.js';
6
2
  import { OwnershipModel, RoomEvents } from "../engine/engine_networking.js"
3
+ import { Behaviour, GameObject } from "./Component.js";
4
+ import { Rigidbody } from "./RigidBody.js";
5
+ import * as utils from "../engine/engine_utils.js"
7
6
  import { sendDestroyed } from '../engine/engine_networking_instantiate.js';
8
- import { setWorldEuler } from '../engine/engine_three_utils.js';
9
- import * as utils from "../engine/engine_utils.js"
10
- import { registerBinaryType } from '../engine-schemes/schemes.js';
7
+ import { InstancingUtil } from "../engine/engine_instancing.js";
11
8
  import { SyncedTransformModel } from '../engine-schemes/synced-transform-model.js';
9
+ import * as flatbuffers from "flatbuffers";
12
10
  import { Transform } from '../engine-schemes/transform.js';
13
- import { Behaviour, GameObject } from "./Component.js";
14
- import { Rigidbody } from "./RigidBody.js";
11
+ import { registerBinaryType } from '../engine-schemes/schemes.js';
12
+ import { setWorldEuler } from '../engine/engine_three_utils.js';
15
13
 
16
14
  const debug = utils.getParam("debugsync");
17
15
  export const SyncedTransformIdentifier = "STRS";
@@ -37,19 +35,8 @@
37
35
  }
38
36
 
39
37
 
40
- let FAST_ACTIVE_SYNCTRANSFORMS = 0;
41
- let FAST_INTERVAL = 0;
42
- onUpdate((ctx) => {
43
- const isRunningOnGlitch = ctx.connection.currentServerUrl?.includes("glitch");
44
- const threshold = isRunningOnGlitch ? 10 : 40;
45
- FAST_INTERVAL = Math.floor(FAST_ACTIVE_SYNCTRANSFORMS / threshold);
46
- FAST_ACTIVE_SYNCTRANSFORMS = 0;
47
- if(debug && FAST_INTERVAL > 0) console.log("Sync Transform Fast Interval", FAST_INTERVAL);
48
- })
49
-
50
38
  export class SyncedTransform extends Behaviour {
51
39
 
52
-
53
40
  // public autoOwnership: boolean = true;
54
41
  public overridePhysics: boolean = true
55
42
  public interpolatePosition: boolean = true;
@@ -70,7 +57,6 @@
70
57
  private _receivedFastUpdate: boolean = false;
71
58
  private _shouldRequestOwnership: boolean = false;
72
59
 
73
- /** Request ownership of an object - you need to be connected to a room */
74
60
  public requestOwnership() {
75
61
  if (debug)
76
62
  console.log("Request ownership");
@@ -306,12 +292,8 @@
306
292
 
307
293
  const updateInterval = 10;
308
294
  const fastUpdate = this.rb || this.fastMode;
309
-
310
295
  if (this._needsUpdate && (updateInterval <= 0 || updateInterval > 0 && this.context.time.frameCount % updateInterval === 0 || fastUpdate)) {
311
296
 
312
- FAST_ACTIVE_SYNCTRANSFORMS++;
313
- if (fastUpdate && FAST_INTERVAL > 0 && this.context.time.frameCount % FAST_INTERVAL !== 0) return;
314
-
315
297
  if (debug)
316
298
  console.log("send update", this.context.connection.connectionId, this.guid, this.gameObject.name, this.gameObject.guid);
317
299
 
src/engine-components/webxr/TeleportTarget.ts DELETED
@@ -1,9 +0,0 @@
1
- import { Behaviour } from "../Component.js";
2
-
3
- /** This component is just used as a marker on objects for WebXR teleportation
4
- * The default XRControllerMovement script checks if this component is on the object you are pointing at and if so it will teleport to that location if triggered
5
- * If the component is not present it won't teleport
6
- */
7
- export class TeleportTarget extends Behaviour {
8
-
9
- }
src/engine/xr/TempXRContext.ts DELETED
@@ -1,183 +0,0 @@
1
- import { AxesHelper, Camera, Color, DirectionalLight, GridHelper, Mesh, MeshBasicMaterial, MeshStandardMaterial, PerspectiveCamera, Scene, WebGLRenderer } from "three";
2
-
3
- import { ObjectUtils, PrimitiveType } from "../engine_create_objects.js";
4
- import { Mathf } from "../engine_math.js";
5
- import { delay } from "../engine_utils.js";
6
-
7
- declare type SessionInfo = { session: XRSession, mode: XRSessionMode, init: XRSessionInit };
8
-
9
- /** Create with static `start`- used to start an XR session while waiting for session granted */
10
- export class TemporaryXRContext {
11
-
12
- private static _active: TemporaryXRContext | null = null;
13
- static get active() {
14
- return this._active;
15
- }
16
-
17
- private static _requestInFlight = false;
18
-
19
- static async start(mode: XRSessionMode, init: XRSessionInit) {
20
- if (this._active) {
21
- console.error("Cannot start a new XR session while one is already active");
22
- return null;
23
- }
24
- if (this._requestInFlight) {
25
- console.error("Cannot start a new XR session while a request is already in flight");
26
- return null;
27
- }
28
-
29
- if ('xr' in navigator && navigator.xr) {
30
- if (!init) {
31
- console.error("XRSessionInit must be provided");
32
- return null;
33
- }
34
- this._requestInFlight = true;
35
- const session = await navigator.xr.requestSession(mode, init);
36
- session.addEventListener("end", () => {
37
- this._active = null;
38
- });
39
- if (!this._requestInFlight) {
40
- session.end();
41
- return null;
42
- }
43
- this._requestInFlight = false;
44
- this._active = new TemporaryXRContext(mode, init, session);
45
- return this._active;
46
- }
47
-
48
- return null;
49
- }
50
-
51
- static async handoff(): Promise<SessionInfo | null> {
52
- if (this._active) {
53
- return this._active.handoff();
54
- }
55
- return null;
56
- }
57
-
58
- static async stop() {
59
- this._requestInFlight = false;
60
- if (this._active) {
61
- await this._active.end();
62
- await delay(100);
63
- }
64
- this._active = null;
65
- }
66
-
67
- private readonly _session: XRSession | null;
68
- private readonly _mode: XRSessionMode;
69
- private readonly _init: XRSessionInit;
70
-
71
- private readonly _renderer: WebGLRenderer;
72
- private readonly _camera: Camera;
73
- private readonly _scene: Scene;
74
-
75
- private constructor(mode: XRSessionMode, init: XRSessionInit, session: XRSession) {
76
- this._mode = mode;
77
- this._init = init;
78
- this._session = session;
79
- this._session.addEventListener("end", this.onEnd);
80
-
81
- this._renderer = new WebGLRenderer({ alpha: true });
82
- this._renderer.setAnimationLoop(this.onFrame);
83
- this._renderer.xr.setSession(session);
84
- this._renderer.xr.enabled = true;
85
- this._camera = new PerspectiveCamera();
86
- this._scene = new Scene();
87
- this._scene.add(this._camera);
88
- this.setupScene();
89
- }
90
-
91
- end() {
92
- if (!this._session) return Promise.resolve();
93
- return this._session.end();
94
- }
95
-
96
- /** returns the session and session info and stops the temporary rendering */
97
- async handoff() {
98
- if (!this._session) throw new Error("Cannot handoff a session that has already ended");
99
- const info: SessionInfo = {
100
- session: this._session,
101
- mode: this._mode,
102
- init: this._init
103
- };
104
- await this.onBeforeHandoff();
105
- // calling onEnd here directly because we dont end the session
106
- this.onEnd();
107
- // set the session to null because we dont want this class to accidentaly end the session
108
- //@ts-ignore
109
- this._session = null;
110
- return info;
111
- }
112
-
113
- private onEnd = () => {
114
- this._session?.removeEventListener("end", this.onEnd);
115
- this._renderer.setAnimationLoop(null);
116
- this._renderer.dispose();
117
- this._scene.clear();
118
- }
119
-
120
- private _lastTime = 0;
121
- private onFrame = (time: DOMHighResTimeStamp, _frame: XRFrame) => {
122
- const dt = time - this._lastTime;
123
- this.update(time, dt);
124
- if (this._camera.parent !== this._scene) {
125
- this._scene.add(this._camera);
126
- }
127
- this._renderer.render(this._scene, this._camera);
128
- }
129
-
130
- /** can be used to prepare the user or fade to black */
131
- private async onBeforeHandoff() {
132
- const obj = ObjectUtils.createPrimitive(PrimitiveType.Cube);
133
- obj.position.z = -3;
134
- obj.position.y = .5;
135
- this._scene.add(obj);
136
- await delay(4000);
137
- this._scene.clear();
138
- await delay(100);
139
- }
140
-
141
-
142
- private _spheres: Mesh[] = [];
143
- private setupScene() {
144
- this._scene.background = new Color(0x000000);
145
- this._scene.add(new GridHelper(5, 10, 0x111111, 0x222222));
146
-
147
- const light = new DirectionalLight(0xffffff, 1);
148
- light.position.set(2, 2, 2);
149
- light.castShadow = false;
150
- this._scene.add(light);
151
-
152
- const light2 = new DirectionalLight(0xffffff, 1);
153
- light2.position.set(-2, -2, -2);
154
- light2.castShadow = false;
155
- this._scene.add(light2);
156
-
157
- const sphereRange = 50;
158
- for (let i = 0; i < 100; i++) {
159
- const sphere = ObjectUtils.createPrimitive(PrimitiveType.Sphere, {
160
- material: new MeshStandardMaterial({
161
- color: 0x222222,
162
- metalness: 1,
163
- roughness: .8,
164
- })
165
- });
166
- sphere.position.x = Mathf.random(-sphereRange, sphereRange);
167
- sphere.position.y = Mathf.random(3, 40);
168
- sphere.position.z = Mathf.random(-sphereRange, sphereRange);
169
- sphere.scale.multiplyScalar(2);
170
- this._spheres.push(sphere);
171
- this._scene.add(sphere);
172
- }
173
- }
174
-
175
- private update(time: number, _deltaTime: number) {
176
-
177
- const speed = time * .0004;
178
- for (let i = 0; i < this._spheres.length; i++) {
179
- const sphere = this._spheres[i];
180
- sphere.position.y += Math.sin(speed + i * .5) * 0.002;
181
- }
182
- }
183
- }
src/engine/tests/test_utils.ts CHANGED
@@ -1,6 +1,6 @@
1
1
 
2
+ import * as utils from "../engine_utils.js";
2
3
  import { noVoip } from "../../engine-components/Voip.js";
3
- import * as utils from "../engine_utils.js";
4
4
 
5
5
 
6
6
  export function detect_run_tests(){
src/engine-components/TestRunner.ts CHANGED
@@ -1,12 +1,11 @@
1
+ import { Behaviour } from "./Component.js";
2
+ import * as tests from "../engine/tests/test_utils.js";
3
+ import { createTransformModel, SyncedTransform, SyncedTransformIdentifier } from "./SyncedTransform.js";
1
4
  import * as flatbuffers from 'flatbuffers';
5
+ import { SyncedTransformModel } from "../engine-schemes/synced-transform-model.js";
6
+ import { Rigidbody } from "./RigidBody.js";
2
7
  import { Vector3 } from "three";
3
-
4
8
  import type { IModel } from "../engine/engine_networking_types.js";
5
- import * as tests from "../engine/tests/test_utils.js";
6
- import { SyncedTransformModel } from "../engine-schemes/synced-transform-model.js";
7
- import { Behaviour } from "./Component.js";
8
- import { Rigidbody } from "./RigidBody.js";
9
- import { createTransformModel, SyncedTransform, SyncedTransformIdentifier } from "./SyncedTransform.js";
10
9
 
11
10
  export class TestRunner extends Behaviour {
12
11
  awake(): void {
src/engine-components/ui/Text.ts CHANGED
@@ -1,13 +1,12 @@
1
- import { Color } from 'three';
1
+ import { Graphic } from './Graphic.js';
2
2
  import * as ThreeMeshUI from 'three-mesh-ui'
3
3
  import type { DocumentedOptions as ThreeMeshUIEveryOptions } from "three-mesh-ui/build/types/core/elements/MeshUIBaseElement.js";
4
-
4
+ import { Color } from 'three';
5
+ import { updateRenderSettings } from './Utils.js';
6
+ import { Canvas } from './Canvas.js';
5
7
  import { serializable } from '../../engine/engine_serialization_decorator.js';
6
8
  import { getParam } from '../../engine/engine_utils.js';
7
- import { Canvas } from './Canvas.js';
8
- import { Graphic } from './Graphic.js';
9
9
  import { type ICanvas, type ICanvasEventReceiver, type IHasAlphaFactor } from './Interfaces.js';
10
- import { updateRenderSettings } from './Utils.js';
11
10
 
12
11
  const debug = getParam("debugtext");
13
12
 
@@ -314,12 +313,12 @@
314
313
  const child = this.uiObject.children[i];
315
314
  // @ts-ignore
316
315
  if (child.isUI) {
317
- this.uiObject.remove(child as any);
316
+ this.uiObject.remove(child);
318
317
  child.clear();
319
318
  }
320
319
  }
321
320
  const el = new ThreeMeshUI.Inline({ textContent: text.substring(0, currentTag.startIndex), color: 'inherit' });
322
- this.uiObject.add(el as any);
321
+ this.uiObject.add(el);
323
322
  }
324
323
 
325
324
  const stackArray: Array<TagStackEntry> = [];
@@ -336,13 +335,13 @@
336
335
  opts.textContent = this.getText(text, currentTag, next);
337
336
  this.handleTag(currentTag, opts, stackArray);
338
337
  const el = new ThreeMeshUI.Inline(opts);
339
- this.uiObject?.add(el as any)
338
+ this.uiObject?.add(el)
340
339
 
341
340
  } else {
342
341
  opts.textContent = text.substring(currentTag.endIndex);
343
342
  this.handleTag(currentTag, opts, stackArray);
344
343
  const el = new ThreeMeshUI.Inline(opts);
345
- this.uiObject?.add(el as any);
344
+ this.uiObject?.add(el);
346
345
  }
347
346
  currentTag = next;
348
347
  }
src/engine-components/export/usdz/ThreeUSDZExporter.ts CHANGED
@@ -1,37 +1,36 @@
1
+ import { Renderer } from '../../Renderer.js';
2
+ import { GameObject } from '../../Component.js';
3
+ import type { OffscreenCanvasExt } from '../../../engine/engine_shims.js';
1
4
  import '../../../engine/engine_shims.js';
2
-
3
5
  import {
4
- AnimationClip,
5
- Bone,
6
- BufferAttribute,
6
+ PlaneGeometry,
7
+ Texture,
8
+ Uniform,
9
+ PerspectiveCamera,
10
+ Scene,
11
+ Mesh,
12
+ ShaderMaterial,
13
+ WebGLRenderer,
14
+ MathUtils,
15
+ Matrix4,
16
+ DoubleSide,
7
17
  BufferGeometry,
18
+ Material,
8
19
  Color,
9
- DoubleSide,
10
- Material,
11
- MathUtils,
12
- Matrix4,
13
- Mesh,
14
- MeshBasicMaterial,
20
+ MeshStandardMaterial,
15
21
  MeshPhysicalMaterial,
16
- MeshStandardMaterial,
17
22
  Object3D,
18
- OrthographicCamera,
19
- PerspectiveCamera,
20
- PlaneGeometry,
21
- Scene,
22
- ShaderMaterial,
23
+ MeshBasicMaterial,
24
+ Bone,
23
25
  SkinnedMesh,
24
26
  SRGBColorSpace,
25
- Texture,
26
- Uniform,
27
- Vector4,
28
- WebGLRenderer} from 'three';
27
+ AnimationClip,
28
+ OrthographicCamera,
29
+ BufferAttribute,
30
+ Vector4
31
+ } from 'three';
29
32
  import * as fflate from 'three/examples/jsm/libs/fflate.module.js';
30
33
 
31
- import type { OffscreenCanvasExt } from '../../../engine/engine_shims.js';
32
- import { GameObject } from '../../Component.js';
33
- import { Renderer } from '../../Renderer.js';
34
-
35
34
  function makeNameSafe( str ) {
36
35
  str = str.replace( /[^a-zA-Z0-9_]/g, '' );
37
36
 
@@ -1759,17 +1758,17 @@
1759
1758
  ];
1760
1759
 
1761
1760
  export {
1761
+ USDZExporter,
1762
+ USDZExporterContext,
1763
+ USDWriter,
1764
+ USDObject,
1762
1765
  buildMatrix,
1763
- decompressGpuTexture,
1764
- findStructuralNodesInBoneHierarchy,
1765
1766
  getBoneName,
1766
1767
  getPathToSkeleton,
1768
+ fn as usdNumberFormatting,
1769
+ USDDocument,
1770
+ makeNameSafe as makeNameSafeForUSD,
1767
1771
  imageToCanvas,
1768
- makeNameSafe as makeNameSafeForUSD,
1769
- USDDocument,
1770
- fn as usdNumberFormatting,
1771
- USDObject,
1772
- USDWriter,
1773
- USDZExporter,
1774
- USDZExporterContext,
1772
+ decompressGpuTexture,
1773
+ findStructuralNodesInBoneHierarchy,
1775
1774
  };
src/engine-components/postprocessing/Effects/TiltShiftEffect.ts CHANGED
@@ -1,9 +1,8 @@
1
+ import { registerCustomEffectType } from "../VolumeProfile.js";
2
+ import { type EffectProviderResult, PostProcessingEffect } from "../PostProcessingEffect.js";
1
3
  import { KernelSize, TiltShiftEffect as TiltShift } from "postprocessing";
2
-
4
+ import { VolumeParameter } from "../VolumeParameter.js";
3
5
  import { serializable } from "../../../engine/engine_serialization.js";
4
- import { type EffectProviderResult, PostProcessingEffect } from "../PostProcessingEffect.js";
5
- import { VolumeParameter } from "../VolumeParameter.js";
6
- import { registerCustomEffectType } from "../VolumeProfile.js";
7
6
 
8
7
 
9
8
  export class TiltShiftEffect extends PostProcessingEffect {
src/engine-components/timeline/TimelineTracks.ts CHANGED
@@ -1,15 +1,14 @@
1
- import { AnimationAction, AnimationClip, AnimationMixer, Audio, AudioListener, AudioLoader, Euler, Object3D, Quaternion, QuaternionKeyframeTrack, Vector3, VectorKeyframeTrack } from "three";
2
-
3
- import { isDevEnvironment } from "../../engine/debug/index.js";
1
+ import { PlayableDirector } from "./PlayableDirector.js";
2
+ import * as Models from "./TimelineModels.js";
3
+ import { GameObject } from "../Component.js";
4
4
  import { Context } from "../../engine/engine_setup.js";
5
+ import { SignalReceiver } from "./SignalAsset.js";
6
+ import { Audio, AudioListener, AnimationAction, AnimationClip, AnimationMixer, AudioLoader, Euler, Object3D, Quaternion, QuaternionKeyframeTrack, Vector3, VectorKeyframeTrack } from "three";
5
7
  import { getParam, resolveUrl } from "../../engine/engine_utils.js";
8
+ import { AudioSource } from "../AudioSource.js";
9
+ import { Animator } from "../Animator.js"
6
10
  import { setObjectAnimated } from "../AnimationUtils.js";
7
- import { Animator } from "../Animator.js"
8
- import { AudioSource } from "../AudioSource.js";
9
- import { GameObject } from "../Component.js";
10
- import { PlayableDirector } from "./PlayableDirector.js";
11
- import { SignalReceiver } from "./SignalAsset.js";
12
- import * as Models from "./TimelineModels.js";
11
+ import { isDevEnvironment } from "../../engine/debug/index.js";
13
12
 
14
13
  const debug = getParam("debugtimeline");
15
14
 
@@ -564,16 +563,14 @@
564
563
 
565
564
  const muteAudioTracks = getParam("mutetimeline");
566
565
 
567
- declare type AudioClipModel = Models.ClipModel & { _didTriggerPlay: boolean };
568
-
569
566
  export class AudioTrackHandler extends TrackHandler {
570
567
 
571
- models: Array<AudioClipModel> = [];
568
+ models: Array<Models.ClipModel> = [];
572
569
  listener!: AudioListener;
573
570
  audio: Array<Audio> = [];
574
571
  audioContextTimeOffset: Array<number> = [];
575
572
  lastTime: number = 0;
576
- audioSource?: AudioSource;
573
+ audioSource?:AudioSource;
577
574
 
578
575
  private _audioLoader: AudioLoader | null = null;
579
576
 
@@ -594,9 +591,7 @@
594
591
  addModel(model: Models.ClipModel) {
595
592
  const audio = new Audio(this.listener as any);
596
593
  this.audio.push(audio);
597
- const audioClipModel = model as AudioClipModel;
598
- audioClipModel._didTriggerPlay = false;
599
- this.models.push(audioClipModel);
594
+ this.models.push(model);
600
595
  }
601
596
 
602
597
  onDisable() {
@@ -604,9 +599,6 @@
604
599
  if (audio.isPlaying)
605
600
  audio.stop();
606
601
  }
607
- for (const model of this.models) {
608
- model._didTriggerPlay = false;
609
- }
610
602
  }
611
603
 
612
604
  onDestroy() {
@@ -634,23 +626,8 @@
634
626
  if (audio?.isPlaying)
635
627
  audio.stop();
636
628
  }
637
- for (const model of this.models) {
638
- model._didTriggerPlay = false;
639
- }
640
629
  }
641
630
 
642
- private _playableDirectorResumed = false;
643
- onPauseChanged() {
644
- // if the timeline gets paused we stop all audio clips
645
- // we dont reset the triggerPlay here (this will automatically reset when the timeline start evaluating again)
646
- for (let i = 0; i < this.audio.length; i++) {
647
- const audio = this.audio[i];
648
- if (audio?.isPlaying)
649
- audio.stop();
650
- }
651
- this._playableDirectorResumed = this.director.isPlaying;
652
- }
653
-
654
631
  evaluate(time: number) {
655
632
  if (muteAudioTracks) return;
656
633
  if (this.track.muted) return;
@@ -659,8 +636,6 @@
659
636
  return;
660
637
  }
661
638
  const isMuted = this.director.context.application.muted;
662
- const resumePlay = this._playableDirectorResumed;
663
- this._playableDirectorResumed = false;
664
639
  // this is just so that we dont hear the very first beat when the audio starts but is muted
665
640
  // if we dont add a delay we hear a little bit of the audio before it shuts down
666
641
  // MAYBE instead of doing it like this we should connect a custom audio node (or disconnect the output node?)
@@ -678,24 +653,15 @@
678
653
  audio.playbackRate = this.director.context.time.timeScale * this.director.speed;
679
654
  audio.loop = asset.loop;
680
655
  if (time >= model.start && time <= model.end && time < this.director.duration) {
681
- if (!audio.isPlaying || !this.director.isPlaying) {
682
- // if the timeline is paused we trigger the audio clip once when the model is entered
683
- // we dont playback the audio clip if we scroll back in time
684
- // this is to support audioclip playback when using timeline with manual scrolling (scrollytelling)
685
- if (resumePlay || (!model._didTriggerPlay && this.lastTime < time)) {
686
- // we don't want to clip in the audio if it's a very short clip
687
- const clipDuration = model.duration * model.timeScale;
688
- if (clipDuration > .3)
689
- audio.offset = model.clipIn + (time - model.start) * model.timeScale;
690
- else audio.offset = 0;
691
- if (debug) console.log("Timeline Audio (" + this.track.name + ") play with offset " + audio.offset + " - " + model.asset.clip);
692
- audio.play(playTimeOffset);
693
- model._didTriggerPlay = true;
694
- }
695
- else {
696
- // do nothing...
697
- }
656
+ if (this.director.isPlaying == false) {
657
+ if (audio.isPlaying)
658
+ audio.stop();
659
+ if (this.lastTime === time) continue;
698
660
  }
661
+ else if (!audio.isPlaying) {
662
+ audio.offset = model.clipIn + (time - model.start) * model.timeScale;
663
+ audio.play(playTimeOffset);
664
+ }
699
665
  else {
700
666
  const targetOffset = model.clipIn + (time - model.start) * model.timeScale;
701
667
  // seems it's non-trivial to get the right time from audio sources;
@@ -711,7 +677,7 @@
711
677
  }
712
678
  let vol = asset.volume as number;
713
679
 
714
- if (this.track.volume !== undefined)
680
+ if(this.track.volume !== undefined)
715
681
  vol *= this.track.volume;
716
682
 
717
683
  if (isMuted) vol = 0;
@@ -726,12 +692,8 @@
726
692
  audio.setVolume(vol * this.director.weight);
727
693
  }
728
694
  else {
729
- model._didTriggerPlay = false;
730
- if (this.director.isPlaying) {
731
- if (audio.isPlaying) {
732
- audio.stop();
733
- }
734
- }
695
+ if (audio.isPlaying)
696
+ audio.stop();
735
697
  }
736
698
  }
737
699
  this.lastTime = time;
src/engine-components/postprocessing/Effects/Tonemapping.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import { ACESFilmicToneMapping, LinearToneMapping, NoToneMapping, ReinhardToneMapping } from "three";
2
-
3
2
  import { serializable } from "../../../engine/engine_serialization.js";
4
3
  import { type EffectProviderResult, PostProcessingEffect } from "../PostProcessingEffect.js";
5
4
  import { VolumeParameter } from "../VolumeParameter.js";
src/engine-components/TransformGizmo.ts CHANGED
@@ -1,11 +1,10 @@
1
- import { MathUtils,Mesh } from "three";
1
+ import { Behaviour, GameObject } from "./Component.js";
2
+ import { SyncedTransform } from "./SyncedTransform.js";
3
+ import { serializable } from "../engine/engine_serialization_decorator.js";
4
+ import * as params from "../engine/engine_default_parameters.js";
5
+ import { Mesh, MathUtils } from "three";
2
6
  import { TransformControls } from "three/examples/jsm/controls/TransformControls.js";
3
-
4
- import * as params from "../engine/engine_default_parameters.js";
5
- import { serializable } from "../engine/engine_serialization_decorator.js";
6
- import { Behaviour, GameObject } from "./Component.js";
7
7
  import { OrbitControls } from "./OrbitControls.js";
8
- import { SyncedTransform } from "./SyncedTransform.js";
9
8
 
10
9
  export class TransformGizmo extends Behaviour {
11
10
 
src/engine-components/webxr/types.ts DELETED
@@ -1,4 +0,0 @@
1
-
2
- export interface XRMovementBehaviour {
3
- isXRMovementHandler: true;
4
- }
src/engine/extensions/usage_tracker.ts CHANGED
@@ -1,7 +1,6 @@
1
1
 
2
+ import { type GLTF, type GLTFLoaderPlugin, GLTFParser } from "three/examples/jsm/loaders/GLTFLoader.js";
2
3
  import { Mesh, Object3D } from "three";
3
- import { type GLTF, type GLTFLoaderPlugin, GLTFParser } from "three/examples/jsm/loaders/GLTFLoader.js";
4
-
5
4
  import { getParam } from "../engine_utils.js";
6
5
 
7
6
 
src/engine-components/export/usdz/USDZExporter.ts CHANGED
@@ -1,24 +1,24 @@
1
- import { Matrix4,Mesh, Object3D } from "three";
2
-
1
+ import { delay, getParam, isiOS, isMobileDevice, isSafari } from "../../../engine/engine_utils.js";
2
+ import { Object3D, Mesh, Matrix4 } from "three";
3
+ import { USDZExporter as ThreeUSDZExporter } from "./ThreeUSDZExporter.js";
4
+ import { AnimationExtension } from "./extensions/Animation.js"
5
+ import { ensureQuicklookLinkIsCreated } from "./utils/quicklook.js";
6
+ import { getFormattedDate } from "./utils/timeutils.js";
7
+ import { registerAnimatorsImplictly } from "./utils/animationutils.js";
8
+ import type { IUSDExporterExtension } from "./Extension.js";
9
+ import { Behaviour, GameObject } from "../../Component.js";
10
+ import { WebXR } from "../../webxr/WebXR.js"
11
+ import { serializable } from "../../../engine/engine_serialization.js";
3
12
  import { showBalloonMessage, showBalloonWarning } from "../../../engine/debug/index.js";
4
- import { hasProLicense } from "../../../engine/engine_license.js";
5
- import { serializable } from "../../../engine/engine_serialization.js";
6
13
  import { Context } from "../../../engine/engine_setup.js";
7
- import { delay, getParam, isiOS, isMobileDevice, isSafari } from "../../../engine/engine_utils.js";
8
- import { Behaviour, GameObject } from "../../Component.js";
9
- import { Renderer } from "../../Renderer.js"
10
14
  import { WebARSessionRoot } from "../../webxr/WebARSessionRoot.js";
11
- import { XRFlag, XRState, XRStateFlag } from "../../webxr/XRFlag.js";
12
- import type { IUSDExporterExtension } from "./Extension.js";
13
- import { AnimationExtension } from "./extensions/Animation.js"
15
+ import { hasProLicense } from "../../../engine/engine_license.js";
16
+ import { BehaviorExtension } from "./extensions/behavior/Behaviour.js";
14
17
  import { AudioExtension } from "./extensions/behavior/AudioExtension.js";
15
- import { BehaviorExtension } from "./extensions/behavior/Behaviour.js";
16
18
  import { TextExtension } from "./extensions/USDZText.js";
17
19
  import { USDZUIExtension } from "./extensions/USDZUI.js";
18
- import { USDZExporter as ThreeUSDZExporter } from "./ThreeUSDZExporter.js";
19
- import { registerAnimatorsImplictly } from "./utils/animationutils.js";
20
- import { ensureQuicklookLinkIsCreated } from "./utils/quicklook.js";
21
- import { getFormattedDate } from "./utils/timeutils.js";
20
+ import { Renderer } from "../../Renderer.js"
21
+ import { XRFlag, XRState, XRStateFlag } from "../../XRFlag.js";
22
22
 
23
23
  const debug = getParam("debugusdz");
24
24
 
@@ -76,6 +76,7 @@
76
76
  extensions: IUSDExporterExtension[] = [];
77
77
 
78
78
  private link!: HTMLAnchorElement;
79
+ private webxr?: WebXR;
79
80
 
80
81
  start() {
81
82
  if (debug) {
@@ -113,6 +114,8 @@
113
114
  const ios = isiOS()
114
115
  const safari = isSafari();
115
116
  if (debug || (ios && safari)) {
117
+ if (debug || this.allowCreateQuicklookButton)
118
+ this.addQuicklookButton();
116
119
  this.lastCallback = this.quicklookCallback.bind(this);
117
120
  this.link = ensureQuicklookLinkIsCreated(this.context);
118
121
  this.link.addEventListener('message', this.lastCallback);
@@ -125,11 +128,11 @@
125
128
 
126
129
  onDisable() {
127
130
  this.link?.removeEventListener('message', this.lastCallback);
128
- // const ios = isiOS()
129
- // const safari = isSafari();
130
- // if (debug || (ios && safari)) {
131
- // this.removeQuicklookButton();
132
- // }
131
+ const ios = isiOS()
132
+ const safari = isSafari();
133
+ if (debug || (ios && safari)) {
134
+ this.removeQuicklookButton();
135
+ }
133
136
  if (debug)
134
137
  showBalloonMessage("USDZ Exporter disabled: " + this.name);
135
138
 
@@ -380,6 +383,74 @@
380
383
 
381
384
 
382
385
 
386
+
387
+ private _quicklookButton?: HTMLElement;
388
+
389
+ private async createQuicklookButton() {
390
+ if (!this.webxr) {
391
+ await delay(1);
392
+ this.webxr = GameObject.findObjectOfType(WebXR) ?? undefined;
393
+ if (this.webxr) {
394
+ if (this.webxr.VRButton) this.webxr.VRButton.parentElement?.removeChild(this.webxr.VRButton);
395
+ // check if we have an AR button already and re-use that
396
+ if (this.webxr.ARButton && this._quicklookButton !== this.webxr.ARButton) {
397
+ this._quicklookButton = this.webxr.ARButton;
398
+ // Hack to remove the immersiveweb link
399
+ const linkInButton = this.webxr.ARButton.parentElement?.querySelector("a");
400
+ if (linkInButton) {
401
+ linkInButton.href = "";
402
+ }
403
+ this.webxr.ARButton.innerText = "Open in Quicklook";
404
+ this.webxr.ARButton.disabled = false;
405
+ this.webxr.ARButton.addEventListener("click", evt => {
406
+ evt.preventDefault();
407
+ this.exportAsync();
408
+ });
409
+ this.webxr.ARButton.classList.add("quicklook-ar-button");
410
+ this._quicklookButtonContainer = this.webxr.ARButton.parentElement;
411
+ this.dispatchEvent(new CustomEvent("created-button", { detail: this.webxr.ARButton }))
412
+ }
413
+ // create a button if WebXR didnt create one yet
414
+ else {
415
+ this.webxr.createARButton = false;
416
+ this.webxr.createVRButton = false;
417
+ let container = this.context.domElement.shadowRoot!.querySelector(".webxr-buttons");
418
+ if (!container) {
419
+ container = document.createElement("div");
420
+ container.classList.add("webxr-buttons");
421
+ }
422
+ const button = document.createElement("button");
423
+ button.innerText = "Open in Quicklook";
424
+ button.addEventListener("click", () => {
425
+ this.exportAsync();
426
+ });
427
+ button.classList.add('webxr-ar-button');
428
+ button.classList.add('webxr-button');
429
+ button.classList.add("quicklook-ar-button");
430
+ this._quicklookButton = button;
431
+ container.appendChild(button);
432
+ this._quicklookButtonContainer = container;
433
+ this.dispatchEvent(new CustomEvent("created-button", { detail: button }))
434
+ }
435
+ }
436
+ else {
437
+ console.warn("Could not find WebXR component: will not create Quicklook button", Context.Current);
438
+ }
439
+ }
440
+ }
441
+
442
+
443
+ private _quicklookButtonContainer: Element | null = null;
444
+ private async addQuicklookButton() {
445
+ await this.createQuicklookButton();
446
+ if (this._quicklookButton && this._quicklookButtonContainer) {
447
+ this._quicklookButtonContainer.appendChild(this._quicklookButton);
448
+ }
449
+ }
450
+ private removeQuicklookButton() {
451
+ this._quicklookButton?.remove();
452
+ }
453
+
383
454
  private applyWebARSessionRoot() {
384
455
  if (!this.objectToExport) return;
385
456
 
@@ -403,7 +474,7 @@
403
474
  const scale = 1 / sessionRoot!.arScale;
404
475
  if (debug) console.log("AR Session Root scale", scale, target);
405
476
  target.matrix.makeScale(scale, scale, scale);
406
- if (sessionRoot.invertForward == false) {
477
+ if (sessionRoot.invertForward) {
407
478
  target.matrix.multiply(new Matrix4().makeRotationY(Math.PI));
408
479
  }
409
480
  }
src/engine-components/export/usdz/extensions/USDZText.ts CHANGED
@@ -1,12 +1,11 @@
1
- import { Color, Material, Matrix4, MeshStandardMaterial, Object3D, Vector3 } from "three";
2
-
1
+ import type { IUSDExporterExtension } from "../Extension.js";
2
+ import type { IBehaviorElement } from "../extensions/behavior/BehavioursBuilder.js";
3
+ import { USDDocument, USDObject, USDWriter, USDZExporterContext } from "../ThreeUSDZExporter.js";
3
4
  import { GameObject } from "../../../Component.js";
5
+ import { Text } from "../../../ui/Text.js"
4
6
  import { RectTransform } from "../../../ui/RectTransform.js";
5
- import { Text } from "../../../ui/Text.js"
7
+ import { Color, Material, Matrix4, MeshStandardMaterial, Object3D, Vector3 } from "three";
6
8
  import { TextAnchor } from "../../../ui/Text.js";
7
- import type { IUSDExporterExtension } from "../Extension.js";
8
- import type { IBehaviorElement } from "../extensions/behavior/BehavioursBuilder.js";
9
- import { USDDocument, USDObject, USDWriter, USDZExporterContext } from "../ThreeUSDZExporter.js";
10
9
 
11
10
 
12
11
  export enum TextWrapMode {
src/engine-components/export/usdz/extensions/USDZUI.ts CHANGED
@@ -1,14 +1,13 @@
1
- import { Color, Mesh, MeshBasicMaterial, Object3D } from "three";
2
-
1
+ import type { IUSDExporterExtension } from "../Extension.js";
2
+ import { USDObject, USDZExporterContext } from "../ThreeUSDZExporter.js";
3
3
  import { GameObject } from "../../../Component.js";
4
- import { $shadowDomOwner } from "../../../ui/BaseUIComponent.js";
5
4
  import { Canvas } from "../../../ui/Canvas.js";
6
- import { RenderMode } from "../../../ui/Canvas.js";
7
5
  import { CanvasGroup } from "../../../ui/CanvasGroup.js";
6
+ import { $shadowDomOwner } from "../../../ui/BaseUIComponent.js";
8
7
  import { RectTransform } from "../../../ui/RectTransform.js";
9
- import type { IUSDExporterExtension } from "../Extension.js";
10
- import { USDObject, USDZExporterContext } from "../ThreeUSDZExporter.js";
8
+ import { Color, Mesh, MeshBasicMaterial, Object3D } from "three";
11
9
  import { TextExtension } from "./USDZText.js";
10
+ import { RenderMode } from "../../../ui/Canvas.js";
12
11
 
13
12
  export class USDZUIExtension implements IUSDExporterExtension {
14
13
  get extensionName(): string {
@@ -32,7 +31,7 @@
32
31
  height = rt.height;
33
32
 
34
33
  const shadowRootModel = USDObject.createEmpty();
35
- const shadowComponent = rt.shadowComponent as unknown as Object3D;
34
+ const shadowComponent = rt.shadowComponent;
36
35
  model.add(shadowRootModel);
37
36
 
38
37
  if (shadowComponent) {
src/engine-components/ui/Utils.ts CHANGED
@@ -1,7 +1,5 @@
1
1
 
2
- import { DoubleSide, FrontSide, Object3D } from "three"
3
- import ThreeMeshUI from "three-mesh-ui";
4
-
2
+ import { FrontSide, DoubleSide, Object3D } from "three"
5
3
  import { FrameEvent } from "../../engine/engine_setup.js";
6
4
  import { Behaviour } from "../Component.js";
7
5
  import { $shadowDomOwner, BaseUIComponent } from "./BaseUIComponent.js";
@@ -29,7 +27,7 @@
29
27
  receiveShadows?: boolean;
30
28
  }
31
29
 
32
- export function updateRenderSettings(shadowComponent: Object3D | ThreeMeshUI.MeshUIBaseElement, settings: RenderSettings) {
30
+ export function updateRenderSettings(shadowComponent: Object3D, settings: RenderSettings) {
33
31
  if (!shadowComponent) return;
34
32
  // const owner = shadowComponent[$shadowDomOwner];
35
33
  // if (!owner)
src/engine/xr/utils.ts DELETED
@@ -1,40 +0,0 @@
1
- import { Object3D } from "three";
2
-
3
- import { AssetReference } from "../engine_addressables.js";
4
- import type { SourceIdentifier } from "../engine_types.js";
5
- import { getParam } from "../engine_utils.js";
6
-
7
- const debug = getParam("debugwebxr");
8
-
9
- export class NeedleXRUtils {
10
-
11
- /** Searches the hierarchy for objects following a specific naming scheme */
12
- static tryFindAvatarObjects(obj: Object3D, sourceId: SourceIdentifier, result: { head?: AssetReference, leftHand?: AssetReference, rightHand?: AssetReference }) {
13
- if (result.head && result.leftHand && result.rightHand) return;
14
-
15
- const name = obj.name.toLocaleLowerCase();
16
-
17
- if (!result.head && name.includes("head")) {
18
- if (debug) console.log("FOUND AVATAR HEAD", obj.name)
19
- result.head = new AssetReference("", sourceId, obj);
20
- }
21
- if (name.includes("hand")) {
22
- if (!result.leftHand && name.includes("left")) {
23
- if (debug) console.log("FOUND AVATAR LEFT HAND", obj.name)
24
- result.leftHand = new AssetReference("", sourceId, obj);
25
- }
26
- if (!result.rightHand && name.includes("right")) {
27
- if (debug) console.log("FOUND AVATAR RIGHT HAND", obj.name)
28
- result.rightHand = new AssetReference("", sourceId, obj);
29
- }
30
- }
31
-
32
- for (let i = 0; i < obj.children.length; i++) {
33
- if (result.head && result.leftHand && result.rightHand) return;
34
- const child = obj.children[i];
35
- this.tryFindAvatarObjects(child, sourceId, result);
36
- }
37
- }
38
-
39
-
40
- }
src/engine-components/js-extensions/Vector.ts CHANGED
@@ -1,7 +1,6 @@
1
+ import { applyPrototypeExtensions, registerPrototypeExtensions } from "./ExtensionUtils.js";
1
2
  import { Vector3 } from "three";
2
-
3
3
  import { slerp } from "../../engine/engine_three_utils.js";
4
- import { applyPrototypeExtensions, registerPrototypeExtensions } from "./ExtensionUtils.js";
5
4
 
6
5
  export function apply(object: Vector3) {
7
6
  if (object && object.isVector3 === true) {
src/engine-components/VideoPlayer.ts CHANGED
@@ -1,14 +1,13 @@
1
+ import { Behaviour, GameObject } from "./Component.js";
2
+ import { serializable } from "../engine/engine_serialization_decorator.js";
1
3
  import { Material, Mesh, Object3D, ShaderMaterial, SRGBColorSpace, sRGBEncoding, Texture, Vector2, Vector4, VideoTexture } from "three";
2
-
3
- import { isDevEnvironment } from "../engine/debug/index.js";
4
- import { ObjectUtils, PrimitiveType } from "../engine/engine_create_objects.js";
5
4
  import { awaitInput } from "../engine/engine_input_utils.js";
6
- import { serializable } from "../engine/engine_serialization_decorator.js";
7
- import { Context } from "../engine/engine_setup.js";
8
- import { getWorldScale } from "../engine/engine_three_utils.js";
9
5
  import { getParam } from "../engine/engine_utils.js";
10
- import { Behaviour, GameObject } from "./Component.js";
11
6
  import { Renderer } from "./Renderer.js";
7
+ import { getWorldScale } from "../engine/engine_three_utils.js";
8
+ import { ObjectUtils, PrimitiveType } from "../engine/engine_create_objects.js";
9
+ import { Context } from "../engine/engine_setup.js";
10
+ import { isDevEnvironment } from "../engine/debug/index.js";
12
11
 
13
12
  const debug = getParam("debugvideo");
14
13
 
src/engine-components/postprocessing/Effects/Vignette.ts CHANGED
@@ -1,9 +1,8 @@
1
- import { VignetteEffect } from "postprocessing";
2
-
3
1
  import { serializable } from "../../../engine/engine_serialization.js";
2
+ import { VolumeParameter } from "../VolumeParameter.js";
4
3
  import { type EffectProviderResult, PostProcessingEffect } from "../PostProcessingEffect.js";
5
- import { VolumeParameter } from "../VolumeParameter.js";
6
4
  import { registerCustomEffectType } from "../VolumeProfile.js";
5
+ import { VignetteEffect } from "postprocessing";
7
6
 
8
7
 
9
8
  export class Vignette extends PostProcessingEffect {
src/engine-components/Voip.ts CHANGED
@@ -1,12 +1,11 @@
1
- import { AudioAnalyser } from "three";
2
-
3
- import { isDevEnvironment, showBalloonError, showBalloonWarning } from "../engine/debug/index.js";
4
- import { RoomEvents } from "../engine/engine_networking.js";
5
- import { disposeStream,NetworkedStreamEvents, NetworkedStreams, StreamEndedEvent, StreamReceivedEvent } from "../engine/engine_networking_streams.js"
1
+ import { Behaviour } from "./Component.js";
2
+ import { StreamEndedEvent, NetworkedStreamEvents, NetworkedStreams, StreamReceivedEvent, disposeStream } from "../engine/engine_networking_streams.js"
6
3
  import { serializable } from "../engine/engine_serialization_decorator.js";
7
4
  import { getParam, microphonePermissionsGranted } from "../engine/engine_utils.js";
5
+ import { RoomEvents } from "../engine/engine_networking.js";
8
6
  import { delay } from "../engine/engine_utils.js";
9
- import { Behaviour } from "./Component.js";
7
+ import { isDevEnvironment, showBalloonError, showBalloonWarning } from "../engine/debug/index.js";
8
+ import { AudioAnalyser } from "three";
10
9
 
11
10
  export const noVoip = "noVoip";
12
11
  const debugParam = getParam("debugvoip");
src/engine-components/postprocessing/Volume.ts CHANGED
@@ -1,14 +1,13 @@
1
- import { EffectComposer } from "postprocessing";
2
-
3
- import { isDevEnvironment } from "../../engine/debug/index.js";
4
- import type { EditorModification, IEditorModification as IEditorModificationReceiver } from "../../engine/engine_editor-sync.js";
1
+ import { Behaviour } from "../Component.js";
5
2
  import { serializeable } from "../../engine/engine_serialization_decorator.js";
6
3
  import { getParam } from "../../engine/engine_utils.js";
7
- import { Behaviour } from "../Component.js";
4
+ import { VolumeProfile } from "./VolumeProfile.js";
5
+ import type { EditorModification, IEditorModification as IEditorModificationReceiver } from "../../engine/engine_editor-sync.js";
6
+ import { PostProcessingHandler } from "./PostProcessingHandler.js";
8
7
  import { PostProcessingEffect } from "./PostProcessingEffect.js";
9
- import { PostProcessingHandler } from "./PostProcessingHandler.js";
10
8
  import { VolumeParameter } from "./VolumeParameter.js";
11
- import { VolumeProfile } from "./VolumeProfile.js";
9
+ import { isDevEnvironment } from "../../engine/debug/index.js";
10
+ import { EffectComposer } from "postprocessing";
12
11
 
13
12
  const debug = getParam("debugpost");
14
13
 
@@ -36,6 +35,7 @@
36
35
  if (e.key === "p") {
37
36
  console.log("Toggle volume: " + this.name, !this.enabled);
38
37
  this.enabled = !this.enabled;
38
+ this.markDirty();
39
39
  }
40
40
  });
41
41
  }
@@ -49,6 +49,7 @@
49
49
  if (!this.context.isInXR) {
50
50
 
51
51
  if (this.context.composer && (this.context.composer instanceof EffectComposer) === false) {
52
+ if (debug) console.warn("PostProcessing: The current composer is not an EffectComposer - this is not supported");
52
53
  return;
53
54
  }
54
55
 
@@ -116,6 +117,7 @@
116
117
  }
117
118
 
118
119
  private unapply() {
120
+ if (debug) console.log("Unapply PostProcessing", this);
119
121
  this._postprocessing?.unapply();
120
122
  }
121
123
 
src/engine-components/postprocessing/VolumeParameter.ts CHANGED
@@ -1,8 +1,11 @@
1
1
  import { serializable } from "../../engine/engine_serialization.js";
2
+ import { getParam } from "../../engine/engine_utils.js";
2
3
 
3
4
  export declare type VolumeParameterChangedEvent = (newValue: any, oldValue: any, parameter: VolumeParameter) => void;
4
5
  export declare type VolumeParameterValueProcessor = (value: any) => any;
5
6
 
7
+ const debug = getParam("debugpost");
8
+
6
9
  export class VolumeParameter {
7
10
 
8
11
  constructor(value?: any) {
@@ -60,6 +63,7 @@
60
63
  return;
61
64
 
62
65
  const oldValue = this._value;
66
+ if (debug) console.log("VolumeParameter: value changed from", oldValue, "to", val);
63
67
 
64
68
  if (!this._active && this._defaultValue !== undefined) {
65
69
  // when setting the default value we dont process them (default values are explicitly set from the effect that declares them
@@ -78,6 +82,9 @@
78
82
  if (this.onValueChanged) {
79
83
  this.onValueChanged(val, oldValue, this);
80
84
  }
85
+ else if (debug) {
86
+ console.log("VolumeParameter: onValueChanged not set");
87
+ }
81
88
  }
82
89
 
83
90
  private testIfValueChanged(newValue: any): boolean {
src/engine-schemes/vr-user-state-buffer.ts CHANGED
@@ -24,109 +24,102 @@
24
24
  return (obj || new VrUserStateBuffer()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
25
25
  }
26
26
 
27
+ guid():string|null
28
+ guid(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
29
+ guid(optionalEncoding?:any):string|Uint8Array|null {
30
+ const offset = this.bb!.__offset(this.bb_pos, 4);
31
+ return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
32
+ }
33
+
27
34
  time():flatbuffers.Long {
28
- const offset = this.bb!.__offset(this.bb_pos, 4);
35
+ const offset = this.bb!.__offset(this.bb_pos, 6);
29
36
  return offset ? this.bb!.readInt64(this.bb_pos + offset) : this.bb!.createLong(0, 0);
30
37
  }
31
38
 
32
39
  avatarId():string|null
33
40
  avatarId(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
34
41
  avatarId(optionalEncoding?:any):string|Uint8Array|null {
35
- const offset = this.bb!.__offset(this.bb_pos, 6);
42
+ const offset = this.bb!.__offset(this.bb_pos, 8);
36
43
  return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
37
44
  }
38
45
 
39
46
  position(obj?:Vec3):Vec3|null {
40
- const offset = this.bb!.__offset(this.bb_pos, 8);
47
+ const offset = this.bb!.__offset(this.bb_pos, 10);
41
48
  return offset ? (obj || new Vec3()).__init(this.bb_pos + offset, this.bb!) : null;
42
49
  }
43
50
 
44
51
  rotation(obj?:Vec4):Vec4|null {
45
- const offset = this.bb!.__offset(this.bb_pos, 10);
52
+ const offset = this.bb!.__offset(this.bb_pos, 12);
46
53
  return offset ? (obj || new Vec4()).__init(this.bb_pos + offset, this.bb!) : null;
47
54
  }
48
55
 
49
56
  scale():number {
50
- const offset = this.bb!.__offset(this.bb_pos, 12);
57
+ const offset = this.bb!.__offset(this.bb_pos, 14);
51
58
  return offset ? this.bb!.readFloat32(this.bb_pos + offset) : 0.0;
52
59
  }
53
60
 
54
- headPosition(obj?:Vec3):Vec3|null {
55
- const offset = this.bb!.__offset(this.bb_pos, 14);
56
- return offset ? (obj || new Vec3()).__init(this.bb_pos + offset, this.bb!) : null;
57
- }
58
-
59
- headRotation(obj?:Vec4):Vec4|null {
61
+ posLeftHand(obj?:Vec3):Vec3|null {
60
62
  const offset = this.bb!.__offset(this.bb_pos, 16);
61
- return offset ? (obj || new Vec4()).__init(this.bb_pos + offset, this.bb!) : null;
62
- }
63
-
64
- posLeftHand(obj?:Vec3):Vec3|null {
65
- const offset = this.bb!.__offset(this.bb_pos, 18);
66
63
  return offset ? (obj || new Vec3()).__init(this.bb_pos + offset, this.bb!) : null;
67
64
  }
68
65
 
69
66
  posRightHand(obj?:Vec3):Vec3|null {
70
- const offset = this.bb!.__offset(this.bb_pos, 20);
67
+ const offset = this.bb!.__offset(this.bb_pos, 18);
71
68
  return offset ? (obj || new Vec3()).__init(this.bb_pos + offset, this.bb!) : null;
72
69
  }
73
70
 
74
71
  rotLeftHand(obj?:Vec4):Vec4|null {
75
- const offset = this.bb!.__offset(this.bb_pos, 22);
72
+ const offset = this.bb!.__offset(this.bb_pos, 20);
76
73
  return offset ? (obj || new Vec4()).__init(this.bb_pos + offset, this.bb!) : null;
77
74
  }
78
75
 
79
76
  rotRightHand(obj?:Vec4):Vec4|null {
80
- const offset = this.bb!.__offset(this.bb_pos, 24);
77
+ const offset = this.bb!.__offset(this.bb_pos, 22);
81
78
  return offset ? (obj || new Vec4()).__init(this.bb_pos + offset, this.bb!) : null;
82
79
  }
83
80
 
84
81
  static startVrUserStateBuffer(builder:flatbuffers.Builder) {
85
- builder.startObject(11);
82
+ builder.startObject(10);
86
83
  }
87
84
 
85
+ static addGuid(builder:flatbuffers.Builder, guidOffset:flatbuffers.Offset) {
86
+ builder.addFieldOffset(0, guidOffset, 0);
87
+ }
88
+
88
89
  static addTime(builder:flatbuffers.Builder, time:flatbuffers.Long) {
89
- builder.addFieldInt64(0, time, builder.createLong(0, 0));
90
+ builder.addFieldInt64(1, time, builder.createLong(0, 0));
90
91
  }
91
92
 
92
93
  static addAvatarId(builder:flatbuffers.Builder, avatarIdOffset:flatbuffers.Offset) {
93
- builder.addFieldOffset(1, avatarIdOffset, 0);
94
+ builder.addFieldOffset(2, avatarIdOffset, 0);
94
95
  }
95
96
 
96
97
  static addPosition(builder:flatbuffers.Builder, positionOffset:flatbuffers.Offset) {
97
- builder.addFieldStruct(2, positionOffset, 0);
98
+ builder.addFieldStruct(3, positionOffset, 0);
98
99
  }
99
100
 
100
101
  static addRotation(builder:flatbuffers.Builder, rotationOffset:flatbuffers.Offset) {
101
- builder.addFieldStruct(3, rotationOffset, 0);
102
+ builder.addFieldStruct(4, rotationOffset, 0);
102
103
  }
103
104
 
104
105
  static addScale(builder:flatbuffers.Builder, scale:number) {
105
- builder.addFieldFloat32(4, scale, 0.0);
106
+ builder.addFieldFloat32(5, scale, 0.0);
106
107
  }
107
108
 
108
- static addHeadPosition(builder:flatbuffers.Builder, headPositionOffset:flatbuffers.Offset) {
109
- builder.addFieldStruct(5, headPositionOffset, 0);
110
- }
111
-
112
- static addHeadRotation(builder:flatbuffers.Builder, headRotationOffset:flatbuffers.Offset) {
113
- builder.addFieldStruct(6, headRotationOffset, 0);
114
- }
115
-
116
109
  static addPosLeftHand(builder:flatbuffers.Builder, posLeftHandOffset:flatbuffers.Offset) {
117
- builder.addFieldStruct(7, posLeftHandOffset, 0);
110
+ builder.addFieldStruct(6, posLeftHandOffset, 0);
118
111
  }
119
112
 
120
113
  static addPosRightHand(builder:flatbuffers.Builder, posRightHandOffset:flatbuffers.Offset) {
121
- builder.addFieldStruct(8, posRightHandOffset, 0);
114
+ builder.addFieldStruct(7, posRightHandOffset, 0);
122
115
  }
123
116
 
124
117
  static addRotLeftHand(builder:flatbuffers.Builder, rotLeftHandOffset:flatbuffers.Offset) {
125
- builder.addFieldStruct(9, rotLeftHandOffset, 0);
118
+ builder.addFieldStruct(8, rotLeftHandOffset, 0);
126
119
  }
127
120
 
128
121
  static addRotRightHand(builder:flatbuffers.Builder, rotRightHandOffset:flatbuffers.Offset) {
129
- builder.addFieldStruct(10, rotRightHandOffset, 0);
122
+ builder.addFieldStruct(9, rotRightHandOffset, 0);
130
123
  }
131
124
 
132
125
  static endVrUserStateBuffer(builder:flatbuffers.Builder):flatbuffers.Offset {
src/engine-components/webxr/WebARCameraBackground.ts CHANGED
@@ -1,54 +1,49 @@
1
+ import { Behaviour } from "../Component.js";
2
+ import { serializable } from "../../engine/engine_serialization_decorator.js";
3
+ import { RGBAColor } from "../js-extensions/RGBAColor.js"
4
+ import { WebXR } from "./WebXR.js";
1
5
  import {
2
- DoubleSide,
6
+ Scene,
7
+ Texture,
3
8
  Mesh, MeshBasicMaterial,
4
- PerspectiveCamera,
9
+ UniformsUtils,
5
10
  PlaneGeometry,
6
- Scene,
7
11
  ShaderLib,
8
12
  ShaderMaterial,
9
- Texture,
10
- UniformsUtils,
13
+ DoubleSide,
14
+ PerspectiveCamera,
11
15
  } from "three";
12
16
 
13
- import { serializable } from "../../engine/engine_serialization_decorator.js";
14
- import { getParam } from "../../engine/engine_utils.js";
15
- import { NeedleXREventArgs } from "../../engine/engine_xr.js";
16
- import { Behaviour } from "../Component.js";
17
- import { RGBAColor } from "../js-extensions/RGBAColor.js"
17
+ export class WebARCameraBackground extends Behaviour {
18
18
 
19
- const debug = getParam("debugarcamera");
19
+ awake(): void {
20
+ WebXR.OptionalFeatures_AR.push('camera-access');
21
+ }
20
22
 
21
- export class WebARCameraBackground extends Behaviour {
23
+ @serializable()
24
+ public backgroundTint: RGBAColor = new RGBAColor(1,1,1,1);
22
25
 
23
- onBeforeXR(_mode: XRSessionMode, args: XRSessionInit): void {
24
- args.optionalFeatures = args.optionalFeatures || [];
25
- args.optionalFeatures.push('camera-access');
26
-
27
- if (debug) console.warn("Requesting camera-access");
26
+ public get background() {
27
+ return this.backgroundPlane;
28
28
  }
29
29
 
30
- onEnterXR(_args: NeedleXREventArgs): void {
30
+ private _preRender;
31
+
32
+ onEnable(): void {
33
+ this._preRender = this.preRender.bind(this);
34
+ this.context.pre_render_callbacks.push(this._preRender);
35
+
31
36
  if (this.backgroundPlane) {
32
- this.context.scene.add(this.backgroundPlane);
37
+ this.gameObject.add(this.backgroundPlane);
33
38
  this.backgroundPlane.visible = false;
34
39
  }
35
-
36
- if (this.backgroundPlane) this.context.scene.add(this.backgroundPlane);
37
- this.context.pre_render_callbacks.push(this.preRender);
38
40
  }
39
41
 
40
- onLeaveXR(_args: NeedleXREventArgs): void {
41
- if (this.backgroundPlane) this.backgroundPlane.removeFromParent();
42
- const i = this.context.pre_render_callbacks.indexOf(this.preRender);
43
- if (i >= 0)
44
- this.context.pre_render_callbacks.splice(i, 1);
45
- }
42
+ onDisable(): void {
43
+ this.context.pre_render_callbacks = this.context.pre_render_callbacks.filter(cb => cb !== this._preRender);
46
44
 
47
- @serializable()
48
- public backgroundTint: RGBAColor = new RGBAColor(1,1,1,1);
49
-
50
- public get background() {
51
- return this.backgroundPlane;
45
+ if (this.backgroundPlane)
46
+ this.gameObject.remove(this.backgroundPlane);
52
47
  }
53
48
 
54
49
  private backgroundPlane?: Mesh;
@@ -63,13 +58,11 @@
63
58
  return function forceTextureInitialization(renderer, texture) {
64
59
  material.map = texture;
65
60
  renderer.render(scene, camera);
66
- if (debug) console.warn("Force texture initialization");
67
61
  };
68
62
  }();
69
63
 
70
-
71
-
72
- private preRender = () => {
64
+ // TODO should only attach on session start, and detach on session end
65
+ private preRender() {
73
66
  if (!this || !this.gameObject) return;
74
67
 
75
68
  const xr = this.context.renderer.xr;
@@ -88,14 +81,19 @@
88
81
  // from three: WebGLBackground
89
82
  if (this.backgroundPlane === undefined) {
90
83
  this.backgroundPlane = makeFullscreenPlane(this.backgroundTint);
84
+ this.gameObject.add(this.backgroundPlane);
91
85
  }
92
- if(this.backgroundPlane.parent !== this.scene)
93
- this.scene.add(this.backgroundPlane);
94
86
 
95
87
  // WebXR Raw Camera Access -
96
88
  // we composite the camera texture into the scene background by rendering it first.
97
89
  this.updateFromFrame(frame);
98
90
  }
91
+
92
+ /*
93
+ if (this.planeMesh) {
94
+ this.planeMesh.visible = frame != null;
95
+ }
96
+ */
99
97
  }
100
98
 
101
99
  onBeforeRender(frame: XRFrame | null) {
@@ -133,9 +131,17 @@
133
131
  this.backgroundPlane.setTexture(this.threeTexture);
134
132
  this.backgroundPlane.visible = true;
135
133
  }
136
- else {
137
- if (debug) console.warn("No background plane to set texture on");
134
+
135
+ // TODO this would be a lot better but currently
136
+ // setting color space doesn't work.
137
+ // Plus we need to understand how we can supply a custom shader in
138
+ // this case.
139
+ /*
140
+ if (this.threeTexture) {
141
+ this.context.scene.background = this.threeTexture;
142
+ this.threeTexture.colorSpace = NoColorSpace;
138
143
  }
144
+ */
139
145
  }
140
146
  }
141
147
  else {
@@ -169,14 +175,15 @@
169
175
  gl_FragColor = texColor * <backgroundTint>;
170
176
 
171
177
  #include <tonemapping_fragment>
172
- #include <colorspace_fragment>
178
+ #include <encodings_fragment>
179
+
173
180
  }
174
181
  `;
175
182
 
176
183
  // not sure where we want to move this and in which form is best (extends Object3D?)
177
184
  export function makeFullscreenPlane(tint: RGBAColor ) {
178
185
  const replacementTint = "vec4(" + tint.r.toFixed(3) + "," + tint.g.toFixed(3) + "," + tint.b.toFixed(3) + "," + tint.a.toFixed(3) + ")";
179
- if (debug) console.log(replacementTint);
186
+ console.log(replacementTint);
180
187
  const planeMesh = new Mesh(
181
188
  new PlaneGeometry(2, 2),
182
189
  // @ts-ignore
@@ -184,7 +191,7 @@
184
191
  name: 'BackgroundMaterial',
185
192
  uniforms: UniformsUtils.clone( ShaderLib.background.uniforms ),
186
193
  vertexShader: ShaderLib.background.vertexShader,
187
- fragmentShader: backgroundFragment.replaceAll("<backgroundTint>", replacementTint), // ShaderLib.background.fragmentShader,
194
+ fragmentShader: backgroundFragment.replace("<backgroundTint>", replacementTint), // ShaderLib.background.fragmentShader,
188
195
  side: DoubleSide,
189
196
  depthTest: false,
190
197
  depthWrite: false,
@@ -204,8 +211,8 @@
204
211
  // Option 1: add the planeMesh to our scene for rendering.
205
212
  // This is useful for applying custom shader effects on the background (instead of using the system composite)
206
213
  planeMesh.renderOrder = -10000; // render first
207
- // planeMesh.layers.disableAll();
208
- planeMesh.layers.set(2); // ignore raycasts
214
+ planeMesh.layers.disableAll();
215
+ planeMesh.layers.enable(2); // ignore raycasts
209
216
  planeMesh.frustumCulled = false;
210
217
 
211
218
  // should be a class, for now lets just define a method for the weird way the texture needs to be set
src/engine-components/webxr/WebARSessionRoot.ts CHANGED
@@ -1,388 +1,44 @@
1
- import { AxesHelper, DoubleSide, Matrix4, Mesh, MeshBasicMaterial, Object3D, Plane, Quaternion, Raycaster, RingGeometry, Scene, Vector3 } from "three";
2
-
3
- import { isDevEnvironment, showBalloonWarning } from "../../engine/debug/index.js";
1
+ import { Behaviour, GameObject } from "../Component.js";
2
+ import { Matrix4, Object3D, Plane, Quaternion, Ray, Raycaster, Vector2, Vector3 } from "three";
3
+ import { WebAR, WebXR } from "./WebXR.js";
4
+ import { InstancingUtil } from "../../engine/engine_instancing.js";
5
+ import { serializable } from "../../engine/engine_serialization_decorator.js";
4
6
  import { Context } from "../../engine/engine_context.js";
5
- import { ObjectUtils, PrimitiveType } from "../../engine/engine_create_objects.js";
6
- import { destroy } from "../../engine/engine_gameobject.js";
7
- import { NEPointerEvent } from "../../engine/engine_input.js";
8
- import { serializable } from "../../engine/engine_serialization_decorator.js";
9
- import { IComponent, IGameObject } from "../../engine/engine_types.js";
10
- import { getParam } from "../../engine/engine_utils.js";
11
- import { NeedleXRController, NeedleXREventArgs, NeedleXRHitTestResult, NeedleXRSession } from "../../engine/engine_xr.js";
12
- import { Behaviour, GameObject } from "../Component.js";
7
+ import { isQuest } from "../../engine/engine_utils.js";
13
8
 
14
9
  // https://github.com/takahirox/takahirox.github.io/blob/master/js.mmdeditor/examples/js/controls/DeviceOrientationControls.js
15
10
 
16
- const debug = getParam("debugwebxr");
11
+ const tempMatrix = new Matrix4();
17
12
 
18
- const invertForwardMatrix = new Matrix4().makeRotationY(Math.PI);
13
+ export class WebARSessionRoot extends Behaviour {
19
14
 
20
- // TODO: webarsessionroot needs to place the rig (and not itself)
15
+ webAR: WebAR | null = null;
21
16
 
22
- export class WebARSessionRoot extends Behaviour {
23
-
24
- /** The scale of a user in AR:
25
- * a large value makes the scene appear smaller
26
- * default is 1
27
- */
28
- @serializable()
29
- get arScale(): number {
30
- return this._arScale;
17
+ get rig(): Object3D | undefined {
18
+ return this.webAR?.webxr.Rig;
31
19
  }
32
- set arScale(val: number) {
33
- if (val === this._arScale) return;
34
- this._arScale = val;
35
- this.onScaleChanged();
36
- }
37
- private _arScale: number = 1;
38
20
 
39
- /** When enabled the placed scene forward direction will towards the XRRig */
40
21
  @serializable()
41
22
  invertForward: boolean = false;
42
23
 
43
- /** When enabled we will create a XR anchor for the scene placement
44
- * and make sure the scene is at that anchored point during a XR session */
45
- @serializable()
46
- useXRAnchor: boolean = false;
47
-
48
24
  /** Preview feature: enable touch transform */
49
25
  @serializable()
50
26
  arTouchTransform: boolean = false;
51
27
 
52
- /** true if we're currently placing the scene */
53
- private _isPlacing = true;
54
-
55
- /** This is the world matrix of the ar session root when entering webxr
56
- * it is applied when the scene has been placed (e.g. if the session root is x:10, z:10 we want this position to be the center of the scene)
57
- */
58
- private readonly _startOffset: Matrix4 = new Matrix4();
59
-
60
- private _createdPlacementObject: Object3D | null = null;
61
- private readonly _reparentedComponents: Array<{ comp: IComponent, originalObject: IGameObject }> = [];
62
-
63
- // move objects into a temporary scene while placing (which is not rendered) so that the components won't be disabled during this process
64
- // e.g. we want the avatar to still be updated while placing
65
- // another possibly solution would be to ensure from this component that the Rig is *also* not disabled while placing
66
- private readonly _placementScene: Scene = new Scene();
67
-
68
- /** the reticles used for placement */
69
- private readonly _reticle: IGameObject[] = [];
70
- /** needs to be in sync with the reticles */
71
- private readonly _hits: XRHitTestResult[] = [];
72
-
73
- private _placementStartTime: number = -1;
74
- private _rigPlacementMatrix?: Matrix4;
75
- /** if useAnchor is enabled this is the anchor we have created on placing the scene using the placement hit */
76
- private _anchor: XRAnchor | null = null;
77
- /** user input is used for ar touch transform */
78
- private userInput?: WebXRSessionRootUserInput;
79
-
80
- supportsXR(mode: XRSessionMode): boolean {
81
- return mode === "immersive-ar";
28
+ @serializable()
29
+ get arScale(): number {
30
+ return this._arScale;
82
31
  }
83
-
84
- onEnterXR(_args: NeedleXREventArgs): void {
85
- if (debug) console.log("ENTER WEBXR: SessionRoot start...");
86
-
87
- this._anchor = null;
88
-
89
- // if (_args.xr.session.enabledFeatures?.includes("image-tracking")) {
90
- // console.warn("Image tracking is enabled - will not place scene");
91
- // return;
92
- // }
93
-
94
- // save the transform of the session root in the scene to apply it when placing the scene
95
- this.gameObject.updateMatrixWorld();
96
- this._startOffset.copy(this.gameObject.matrixWorld);
97
-
98
- // create a new root object for the session placement scripts
99
- // and move all the children in the scene in a temporary scene that is not rendered
100
- const rootObject = new Object3D();
101
- this._createdPlacementObject = rootObject;
102
- rootObject.name = "AR Session Root";
103
- this._placementScene.name = "AR Placement Scene";
104
- this._placementScene.children.length = 0;
105
- for (let i = this.context.scene.children.length - 1; i >= 0; i--) {
106
- const ch = this.context.scene.children[i];
107
- this._placementScene.add(ch);
108
- }
109
- this.context.scene.add(rootObject);
110
-
111
- // reparent components
112
- // save which gameobject the sessionroot component was previously attached to
113
- this._reparentedComponents.length = 0;
114
- this._reparentedComponents.push({ comp: this, originalObject: this.gameObject });
115
- GameObject.addComponent(rootObject, this);
116
- // const webXR = GameObject.findObjectOfType(WebXR2);
117
- // if (webXR) {
118
- // this._reparentedComponents.push({ comp: webXR, originalObject: webXR.gameObject });
119
- // GameObject.addComponent(rootObject, webXR);
120
- // const playerSync = GameObject.findObjectOfType(XRFlag);
121
- // }
122
-
123
- // recreate the reticle every time we enter AR
124
- for (const ret of this._reticle) {
125
- destroy(ret);
126
- }
127
- this._reticle.length = 0;
128
- this._isPlacing = true;
129
- this.context.input.addEventListener("pointerup", this.onPlaceScene);
32
+ set arScale(val: number) {
33
+ if (val === this._arScale) return;
34
+ this._arScale = val;
35
+ this.setScale(val);
130
36
  }
131
- onLeaveXR() {
132
- // TODO: WebARSessionRoot doesnt work when we enter passthrough and leave XR without having placed the session!!!
133
- this.context.input.removeEventListener("pointerup", this.onPlaceScene)
134
- this.onRevertSceneChanges();
135
- // this._anchor?.delete();
136
- this._anchor = null;
137
- this._rigPlacementMatrix = undefined;
138
- }
139
- onUpdateXR(args: NeedleXREventArgs): void {
140
37
 
141
- // disable session placement while images are being tracked
142
- if (args.xr.isTrackingImages) {
143
- for (const ret of this._reticle)
144
- ret.visible = false;
145
- return;
146
- }
147
-
148
- if (this._isPlacing) {
149
- const rigObject = args.xr.rig?.gameObject;
150
- // the rig should be parented to the scene while placing
151
- // since the camera is always parented to the rig this ensures that the camera is always rendering
152
- if (rigObject && rigObject.parent !== this.context.scene) {
153
- this.context.scene.add(rigObject);
154
- }
155
-
156
- // in pass through mode we want to place the scene using an XR controller
157
- let controllersDidHit = false;
158
- if (args.xr.isPassThrough && args.xr.controllers.length > 0) {
159
- for (const ctrl of args.xr.controllers) {
160
- // with this we can only place with the left / first controller right now
161
- // we also only have one reticle... this should probably be refactored a bit so we can have multiple reticles
162
- // and then place at the reticle for which the user clicked the place button
163
- const hit = ctrl.getHitTest();
164
- if (hit) {
165
- controllersDidHit = true;
166
- this.updateReticleAndHits(args.xr, ctrl.index, hit, args.xr.rigScale);
167
- }
168
- }
169
- }
170
- // in screen AR mode we use "camera" hit testing (or when using the simulator where controller hit testing is not supported)
171
- if (!controllersDidHit) {
172
- const hit = args.xr.getHitTest();
173
- if (hit) {
174
- this.updateReticleAndHits(args.xr, 0, hit, args.xr.rigScale);
175
- }
176
- }
177
-
178
- }
179
- else {
180
- if (this._anchor && args.xr.referenceSpace) {
181
- const pose = args.xr.frame.getPose(this._anchor.anchorSpace, args.xr.referenceSpace);
182
- if (pose && this.context.time.frame % 20 === 0) {
183
- // apply the anchor pose to one of the reticles
184
- const converted = args.xr.convertSpace(pose.transform);
185
- const reticle = this._reticle[0];
186
- if (reticle) {
187
- reticle.position.copy(converted.position);
188
- reticle.quaternion.copy(converted.quaternion);
189
- this.onApplyPose(reticle);
190
- }
191
- }
192
- }
193
-
194
- // scene has been placed
195
- if (this.arTouchTransform) {
196
- if (!this.userInput) this.userInput = new WebXRSessionRootUserInput(this.context);
197
- this.userInput?.enable();
198
- }
199
- else this.userInput?.disable();
200
- if (this.arTouchTransform && this.userInput?.hasChanged) {
201
- if (args.xr.rig) {
202
- const rig = args.xr.rig.gameObject;
203
- this.userInput.applyMatrixTo(rig.matrix, true);
204
- rig.matrix.decompose(rig.position, rig.quaternion, rig.scale);
205
- // if the rig is scaled large we want the drag touch to be faster
206
- this.userInput.factor = rig.scale.x;
207
- }
208
- this.userInput.reset();
209
- }
210
- }
211
- }
212
-
213
- private updateReticleAndHits(_xr: NeedleXRSession, i: number, hit: NeedleXRHitTestResult, scale: number) {
214
- // save the hit test
215
- this._hits[i] = hit.hit;
216
-
217
- let reticle = this._reticle[i];
218
- if (!reticle) {
219
- reticle = new Mesh(
220
- new RingGeometry(0.07, 0.09, 32).rotateX(- Math.PI / 2),
221
- new MeshBasicMaterial({ side: DoubleSide })
222
- ) as any as IGameObject;
223
- if (debug) {
224
- const axes = new AxesHelper(1);
225
- axes.position.y += .01;
226
- reticle.add(axes);
227
- }
228
- this._reticle[i] = reticle;
229
- reticle.name = "AR Placement Reticle";
230
- reticle.matrixAutoUpdate = false;
231
- reticle.visible = false;
232
- }
233
-
234
- reticle.position.lerp(hit.position, this.context.time.deltaTime / .1);
235
- reticle.quaternion.slerp(hit.quaternion, this.context.time.deltaTime / .05);
236
- reticle.scale.set(scale, scale, scale);
237
- // if (this.invertForward) {
238
- // reticle.rotateY(Math.PI);
239
- // }
240
- reticle.updateMatrix();
241
- reticle.visible = true;
242
- if (reticle.parent !== this.context.scene)
243
- this.context.scene.add(reticle);
244
-
245
- if (this._placementStartTime < 0) {
246
- this._placementStartTime = this.context.time.realtimeSinceStartup;
247
- }
248
- }
249
-
250
- private onPlaceScene = (evt: NEPointerEvent) => {
251
- if (this._isPlacing == false) return;
252
-
253
- let reticle = this._reticle[0];
254
- let hit = this._hits[0];
255
-
256
- if (evt.origin instanceof NeedleXRController) {
257
- // until we can use hit testing for both controllers and have multple reticles we only allow placement with the first controller
258
- reticle = this._reticle[evt.origin.index];
259
- hit = this._hits[evt.origin.index];
260
- }
261
-
262
- if (!reticle) {
263
- console.warn("No reticle to place...");
264
- return;
265
- }
266
-
267
- if (!reticle.visible) {
268
- console.warn("Reticle is not visible (can not place)");
269
- return;
270
- }
271
-
272
- if (NeedleXRSession.active?.isTrackingImages) {
273
- console.warn("Scene Placement is disabled while images are being tracked");
274
- return;
275
- }
276
-
277
- // if we place the scene we don't want this event to be propagated to any sub-objects (via the EventSystem) anymore and trigger e.g. a click on objects for the "place tap" event
278
- evt.stopImmediatePropagation();
279
-
280
- this._isPlacing = false;
281
- this.context.input.removeEventListener("pointerup", this.onPlaceScene);
282
-
283
- this.onRevertSceneChanges();
284
-
285
- this.onApplyPose(reticle);
286
-
287
- if (this.useXRAnchor) {
288
- this.onCreateAnchor(NeedleXRSession.active!, hit);
289
- }
290
- }
291
-
292
- private onScaleChanged() {
293
- // TODO: implement
294
- }
295
-
296
- private onRevertSceneChanges() {
297
- for (const ret of this._reticle) {
298
- ret.visible = false;
299
- ret?.removeFromParent();
300
- }
301
- this._reticle.length = 0;
302
-
303
- for (let i = this._placementScene.children.length - 1; i >= 0; i--) {
304
- const ch = this._placementScene.children[i];
305
- this.context.scene.add(ch);
306
- }
307
- this._createdPlacementObject?.removeFromParent();
308
-
309
- for (const reparented of this._reparentedComponents) {
310
- GameObject.addComponent(reparented.originalObject, reparented.comp);
311
- }
312
- }
313
-
314
- private async onCreateAnchor(session: NeedleXRSession, hit: XRHitTestResult) {
315
- if (hit.createAnchor === undefined) {
316
- console.warn("Hit does not support creating an anchor", hit);
317
- if (isDevEnvironment()) showBalloonWarning("Hit does not support creating an anchor");
318
- return;
319
- }
320
- else {
321
- const anchor = await hit.createAnchor(session.viewerPose!.transform);
322
- // make sure the session is still active
323
- if (session.running && anchor) {
324
- this._anchor = anchor;
325
- }
326
- }
327
- }
328
-
329
- private onApplyPose(reticle: Object3D) {
330
- const rigObject = NeedleXRSession.active?.rig?.gameObject;
331
- if (rigObject) {
332
- // save the previous rig parent
333
- const previousParent = rigObject.parent || this.context.scene;
334
-
335
- // if we have placed this rig before and this is just "replacing" with the anchor
336
- // we need to make sure the XRRig attached to the reticle is at the same position as last time
337
- // since in the following code we move it inside the reticle (relative to the reticle)
338
- if (this._rigPlacementMatrix) {
339
- this._rigPlacementMatrix?.decompose(rigObject.position, rigObject.quaternion, rigObject.scale);
340
- }
341
- else {
342
- this._rigPlacementMatrix = rigObject.matrix.clone();
343
- }
344
-
345
- reticle.updateMatrix();
346
- // attach rig to reticle (since the reticle is in rig space it's a easy way to place the rig where we want it relative to the reticle)
347
- this.context.scene.add(reticle);
348
- reticle.attach(rigObject);
349
- reticle.removeFromParent();
350
-
351
-
352
- // move rig now relative tot he reticle
353
- // apply scale
354
- rigObject.scale.set(this.arScale, this.arScale, this.arScale);
355
- rigObject.position.multiplyScalar(this.arScale);
356
-
357
- rigObject.updateMatrix();
358
- // if invert forward is disabled we need to invert the forward rotation
359
- // we want to look into positive Z direction (if invertForward is enabled we look into negative Z direction)
360
- if (this.invertForward == false)
361
- rigObject.matrix.premultiply(invertForwardMatrix);
362
- rigObject.matrix.premultiply(this._startOffset);
363
-
364
- // apply the rig modifications and add it back to the previous parent
365
- rigObject.matrix.decompose(rigObject.position, rigObject.quaternion, rigObject.scale);
366
- previousParent.add(rigObject);
367
- }
368
- }
369
-
370
-
371
-
372
-
373
- /*
374
-
375
- webAR: WebAR | null = null;
376
-
377
- get rig(): Object3D | undefined {
378
- return this.webAR?.webxr.Rig;
379
- }
380
-
381
-
382
-
383
38
  private readonly _initalMatrix = new Matrix4();
384
39
  private readonly _selectStartFn = this.onSelectStart.bind(this);
385
40
  private readonly _selectEndFn = this.onSelectEnd.bind(this);
41
+ private userInput?: WebXRSessionRootUserInput;
386
42
 
387
43
  start() {
388
44
  const xr = GameObject.findObjectOfType(WebXR);
@@ -392,6 +48,7 @@
392
48
  }
393
49
  }
394
50
 
51
+ private _arScale: number = 1;
395
52
  private _rig: Object3D | null = null;
396
53
  private _startPose: Matrix4 | null = null;
397
54
  private _placementPose: Matrix4 | null = null;
@@ -444,7 +101,7 @@
444
101
  if (this.webAR) this.webAR.setReticleActive(false);
445
102
  this.placeAt(rig, poseMatrix);
446
103
  if (hit && pose && !isQuest()) // TODO anchors seem to behave differently with an XRRig
447
- this.onCreatePlacementAnchor(hit, pose);
104
+ this.onCreatePlacementAnchor(hit, pose);
448
105
 
449
106
  return true;
450
107
  }
@@ -563,8 +220,6 @@
563
220
  rig.matrix.decompose(rig.position, rig.quaternion, rig.scale);
564
221
  rig.updateMatrixWorld();
565
222
  }
566
-
567
- */
568
223
  }
569
224
 
570
225
 
@@ -579,14 +234,11 @@
579
234
  twoFingerRotate: boolean = true;
580
235
  twoFingerScale: boolean = true;
581
236
 
582
- factor: number = 1;
583
-
584
237
  readonly context: Context;
585
238
  readonly offset: Matrix4;
586
239
  readonly plane: Plane;
587
240
 
588
241
  private _scale: number = 1;
589
- private _hasChanged: boolean = false;
590
242
 
591
243
  // readonly translate: Vector3 = new Vector3();
592
244
  // readonly rotation: Quaternion = new Quaternion();
@@ -618,21 +270,8 @@
618
270
  this._scale = 1;
619
271
  this.offset.identity();
620
272
  }
621
- get hasChanged() { return this._hasChanged; }
622
-
623
- /**
624
- * Applies the matrix to the offset matrix
625
- * @param matrix the matrix to apply the drag offset to
626
- * @param invert if true the offset matrix will be inverted before applying it to the matrix and premultiplied
627
- */
628
- applyMatrixTo(matrix: Matrix4, invert: boolean) {
629
- this._hasChanged = false;
630
- if (invert) {
631
- this.offset.invert();
632
- matrix.premultiply(this.offset);
633
- }
634
- else
635
- matrix.multiply(this.offset);
273
+ applyMatrixTo(matrix: Matrix4) {
274
+ matrix.premultiply(this.offset);
636
275
  // if (this._needsUpdate)
637
276
  // this.updateMatrix();
638
277
  // matrix.premultiply(this._rotationMatrix);
@@ -685,7 +324,7 @@
685
324
  }
686
325
  private touchMove = (evt: TouchEvent) => {
687
326
  if (evt.defaultPrevented) return;
688
-
327
+
689
328
  if (evt.touches.length === 1) {
690
329
  // if we had multiple touches before due to e.g. pinching / rotating
691
330
  // and stopping one of the touches, we don't want to move the scene suddenly
@@ -766,26 +405,21 @@
766
405
  // this.translate.z -= dz;
767
406
  // this._needsUpdate = true;
768
407
  // return
769
-
408
+ // some arbitrary factor
409
+ dx *= .75;
410
+ dz *= .75;
770
411
  // increase diff if the scene is scaled small
771
412
  dx /= this._scale;
772
413
  dz /= this._scale;
773
-
774
- dx *= this.factor;
775
- dz *= this.factor;
776
-
777
414
  // apply it
778
- this.offset.elements[12] += dx;
779
- this.offset.elements[14] += dz;
780
- if (dx !== 0 || dz !== 0)
781
- this._hasChanged = true;
415
+ this.offset.elements[12] -= dx;
416
+ this.offset.elements[14] -= dz;
782
417
  };
783
418
 
784
419
  private readonly _tempMatrix: Matrix4 = new Matrix4();
785
420
 
786
421
  private addScale(diff: number) {
787
422
  diff /= window.innerWidth
788
- diff *= -1;
789
423
 
790
424
  // this.scale.x *= 1 + diff;
791
425
  // this.scale.y *= 1 + diff;
@@ -799,19 +433,14 @@
799
433
  // apply the scale
800
434
  this._tempMatrix.makeScale(1 - diff, 1 - diff, 1 - diff);
801
435
  this.offset.premultiply(this._tempMatrix);
802
- if (diff !== 0)
803
- this._hasChanged = true;
804
436
  }
805
437
 
806
438
 
807
439
  private addRotation(rot: number) {
808
- rot *= -1;
809
440
  // this.rotation.multiply(new Quaternion().setFromAxisAngle(WebXRSessionRootUserInput.up, rot));
810
441
  // this._needsUpdate = true;
811
442
  // return;
812
443
  this._tempMatrix.makeRotationY(rot);
813
444
  this.offset.premultiply(this._tempMatrix);
814
- if (rot !== 0)
815
- this._hasChanged = true;
816
445
  }
817
446
  }
src/engine-components/webxr/WebXR.ts CHANGED
@@ -1,296 +1,762 @@
1
- import { Object3D } from "three";
1
+ import { Color, Euler, EventDispatcher, Group, Matrix4, Mesh, MeshBasicMaterial, Object3D, Quaternion, RingGeometry, Texture, Vector3, type WebXRArrayCamera } from 'three';
2
+ import { ARButton } from '../../include/three/ARButton.js';
3
+ import { VRButton } from '../../include/three/VRButton.js';
2
4
 
3
5
  import { AssetReference } from "../../engine/engine_addressables.js";
4
- import { serializable } from "../../engine/engine_serialization.js";
5
- import { getParam, isDesktop, isiOS,isMobileDevice, isQuest, isSafari } from "../../engine/engine_utils.js";
6
- import { NeedleXREventArgs, NeedleXRSession } from "../../engine/engine_xr.js";
7
- import { PlayerSync } from "../../engine-components-experimental/networking/PlayerSync.js";
6
+ import { serializable } from "../../engine/engine_serialization_decorator.js";
7
+ import { XRSessionMode } from "../../engine/engine_setup.js";
8
+ import { getWorldPosition, getWorldQuaternion, setWorldPosition, setWorldQuaternion } from "../../engine/engine_three_utils.js";
9
+ import type { INeedleEngineComponent } from "../../engine/engine_types.js";
10
+ import { getParam, isMozillaXR, isQuest, setOrAddParamsToUrl } from "../../engine/engine_utils.js";
11
+
8
12
  import { Behaviour, GameObject } from "../Component.js";
9
- import { USDZExporter } from "../export/usdz/USDZExporter.js";
10
- import { SpatialGrabRaycaster } from "../ui/Raycaster.js";
11
- import { Avatar } from "./Avatar.js";
12
- import { XRControllerModel } from "./controllers/XRControllerModel.js";
13
- import { XRControllerMovement } from "./controllers/XRControllerMovement.js";
13
+ import { noVoip } from "../Voip.js";
14
14
  import { WebARSessionRoot } from "./WebARSessionRoot.js";
15
- import { NeedleWebXRHtmlElement } from "./WebXRButtons.js";
16
- import { XRState, XRStateFlag } from "./XRFlag.js";
15
+ import { ControllerType, WebXRController } from "./WebXRController.js";
16
+ import { XRRig } from "./WebXRRig.js";
17
+ import { WebXRSync } from "./WebXRSync.js";
18
+ import { XRState, XRStateFlag } from "../XRFlag.js";
19
+ import { showBalloonWarning } from '../../engine/debug/index.js';
20
+ import { isDestroyed } from '../../engine/engine_gameobject.js';
17
21
 
18
- const debug = getParam("debugwebxr");
19
- const debugQuicklook = getParam("debugusdz");
22
+ const debugWebXR = getParam("debugwebxr");
20
23
 
21
- export class WebXR extends Behaviour {
24
+ export async function detectARSupport() {
25
+ if (isMozillaXR()) return true;
26
+ if ("xr" in navigator) {
27
+ //@ts-ignore
28
+ return (await navigator["xr"].isSessionSupported('immersive-ar')) === true;
29
+ }
30
+ return false;
31
+ }
32
+ export async function detectVRSupport() {
33
+ if ("xr" in navigator) {
34
+ //@ts-ignore
35
+ return (await navigator["xr"].isSessionSupported('immersive-vr')) === true;
36
+ }
37
+ return false;
38
+ }
22
39
 
23
- // UI
24
- /** When enabled a button will be added to the UI to enter VR */
25
- createVRButton: boolean = true;
26
- /** When enabled a button will be added to the UI to enter AR */
27
- createARButton: boolean = true;
28
- /** When enabled a send to quest button will be shown if the device does not support VR */
29
- createSendToQuestButton: boolean = true;
30
- /** When enabled a QRCode will be created to open the website on a mobile device */
31
- createQRCode: boolean = true;
40
+ let arSupported = false;
41
+ let vrSupported = false;
42
+ detectARSupport().then(res => arSupported = res);
43
+ detectVRSupport().then(res => vrSupported = res);
32
44
 
33
- // VR Settings
34
- /** When enabled default movement behaviour will be added */
35
- useDefaultControls: boolean = true;
36
- /** When enabled controller models will automatically be created and updated when you are using controllers in WebXR */
37
- showControllerModels: boolean = true;
38
- /** When enabled hand models will automatically be created and updated when you are using hands in WebXR */
39
- showHandModels: boolean = true;
45
+ // import TeleportVR from "teleportvr.js";
40
46
 
41
- // AR Settings
42
- /** When enabled the scene must be placed in AR */
43
- usePlacementReticle: boolean = true;
44
- /** When enabled you can position, rotate or scale your AR scene with one or two fingers */
45
- usePlacementAdjustment: boolean = true;
46
- /** Used when `usePlacementReticle` is enabled */
47
- arSceneScale: number = 1;
48
- /** Experimental: When enabled an XRAnchor will be created for the AR scene and the position will be updated to the anchor position every few frames */
49
- useXRAnchor: boolean = false;
47
+ export enum WebXREvent {
48
+ XRStarted = "xrStarted",
49
+ XRStopped = "xrStopped",
50
+ XRUpdate = "xrUpdate",
51
+ RequestVRSession = "requestVRSession",
52
+ ModifyAROptions = "modify-ar-options",
53
+ }
50
54
 
51
- /** When enabled a USDZExporter component will be added to the scene (if none is found) */
52
- useQuicklookExport: boolean = false;
55
+ export declare type CreateButtonOptions = {
56
+ registerClick: boolean
57
+ };
53
58
 
59
+ export class WebXR extends Behaviour {
54
60
 
55
- /** Preview feature enabling occlusion (when available: https://github.com/cabanier/three.js/commit/b6ee92bcd8f20718c186120b7f19a3b68a1d4e47)
56
- * Enables the 'depth-sensing' WebXR feature to provide realtime depth occlusion. Only supported on Oculus Quest right now.
57
- */
58
- useDepthSensing: boolean = false;
61
+ @serializable()
62
+ enableVR = true;
63
+ @serializable()
64
+ enableAR = true;
59
65
 
60
-
61
- /** This avatar representation will be spawned when you enter a webxr session */
62
66
  @serializable(AssetReference)
63
67
  defaultAvatar?: AssetReference;
68
+ @serializable()
69
+ handModelPath: string = "";
64
70
 
65
- private _playerSync?: PlayerSync;
66
- /** these components were created by the WebXR component on session start and will be cleaned up again in session end */
67
- private readonly _createdComponentsInSession: Behaviour[] = [];
71
+ @serializable()
72
+ createVRButton: boolean = true;
73
+ @serializable()
74
+ createARButton: boolean = true;
68
75
 
69
- private _usdzExporter?: USDZExporter;
76
+ private static _isInXr: boolean = false;
77
+ private static events: EventDispatcher = new EventDispatcher();
70
78
 
71
- awake() {
72
- NeedleXRSession.getXRSync(this.context);
73
- if (debug) window.addEventListener("keydown", evt => { if (evt.key === "x") this.exitXR(); });
79
+ public static get IsInWebXR(): boolean { return this._isInXr; }
80
+ public static get XRSupported(): boolean { return 'xr' in navigator && (arSupported || vrSupported); }
81
+ public static get IsARSupported(): boolean { return arSupported; }
82
+ public static get IsVRSupported(): boolean { return vrSupported; }
83
+
84
+ private static _optionalFeatures_VR: string[] = ['local-floor', 'bounded-floor', 'hand-tracking', 'high-fixed-foveation-level', 'layers'];
85
+ private static _optionalFeatures_AR: string[] = ['anchors', 'local-floor', 'hand-tracking', 'layers'];
86
+ public static get OptionalFeatures_VR(): string[] { return this._optionalFeatures_VR; }
87
+ public static get OptionalFeatures_AR(): string[] { return this._optionalFeatures_AR; }
88
+
89
+ public static addEventListener(type: string, listener: any): any {
90
+ this.events.addEventListener(type, listener);
91
+ return listener;
74
92
  }
93
+ public static removeEventListener(type: string, listener: any): any {
94
+ this.events.removeEventListener(type, listener);
95
+ return listener;
96
+ }
97
+ private static dispatchEvent(type: string, event: any): void {
98
+ this.events.dispatchEvent({ type, detail: event });
99
+ }
75
100
 
76
- onEnable(): void {
77
- if (this.useQuicklookExport) {
78
- const existingUSDZExporter = GameObject.findObjectOfType(USDZExporter);
79
- if (!existingUSDZExporter) {
80
- // if no USDZ Exporter is found we add one and assign the scene to be exported
81
- if (debug) console.log("WebXR: Adding USDZExporter");
82
- this._usdzExporter = GameObject.addNewComponent(this.gameObject, USDZExporter);
83
- this._usdzExporter.objectToExport = this.context.scene;
84
- }
101
+ public static createVRButton(webXR: WebXR, opts?: CreateButtonOptions): HTMLButtonElement | HTMLAnchorElement {
102
+ if (!WebXR.XRSupported) {
103
+ console.warn("WebXR is not supported on this device");
85
104
  }
105
+ else
106
+ webXR.__internalAwake();
107
+ const options = { optionalFeatures: WebXR.OptionalFeatures_VR };
108
+ const vrButton = VRButton.createButton(webXR.context.renderer, options);
109
+ vrButton.classList.add('webxr-ar-button');
110
+ vrButton.classList.add('webxr-button');
111
+ this.resetButtonStyles(vrButton);
112
+ // if (this.enableAR) vrButton.style.marginLeft = "60px";
113
+ if (opts?.registerClick ?? true)
114
+ vrButton.addEventListener('click', webXR.onClickedVRButton.bind(webXR));
115
+ return vrButton;
116
+ }
86
117
 
87
- this.handleCreatingHTML();
88
- this.handleOfferSession();
89
-
90
- if (this.defaultAvatar) {
91
- this._playerSync = this.gameObject.getOrAddComponent(PlayerSync);
92
- this._playerSync.autoSync = false;
118
+ public static createARButton(webXR: WebXR, opts?: CreateButtonOptions): HTMLButtonElement | HTMLAnchorElement {
119
+ webXR.__internalAwake();
120
+ const domOverlayRoot = webXR.webAR?.getAROverlayContainer();
121
+ const options: any = { optionalFeatures: [...this.OptionalFeatures_AR] };
122
+ if (domOverlayRoot) {
123
+ options.domOverlay = { root: domOverlayRoot };
124
+ options.optionalFeatures.push('dom-overlay')
125
+ options.optionalFeatures.push('hit-test');
126
+ options.optionalFeatures.push('anchors');
93
127
  }
94
- if (this._playerSync) {
95
- this._playerSync.asset = this.defaultAvatar;
96
- this._playerSync.onPlayerSpawned?.removeEventListener(this.onAvatarSpawned);
97
- this._playerSync.onPlayerSpawned?.addEventListener(this.onAvatarSpawned);
128
+ else {
129
+ console.warn("No dom overlay root found, HTML overlays on top of screen-based AR will not work.");
98
130
  }
99
131
 
100
- // if a button container exists and is not attached to any parent element we attach it to the shadow root (assuming it was removed during onDisable)
101
- if (this._container && !this._container.parentNode) {
102
- this.context.domElement.shadowRoot?.appendChild(this._container);
103
- }
132
+ const arButton = ARButton.createButton(webXR.context.renderer, options, this.onModifyAROptions.bind(this));
133
+ arButton.classList.add('webxr-ar-button');
134
+ arButton.classList.add('webxr-button');
135
+ WebXR.resetButtonStyles(arButton);
136
+ if (opts?.registerClick ?? true)
137
+ arButton.addEventListener('click', webXR.onClickedARButton.bind(webXR));
138
+ return arButton;
104
139
  }
105
140
 
106
- onDisable(): void {
107
- // remove the container automatically if it was added to the shadow root
108
- this._container?.remove();
109
- this._usdzExporter?.destroy();
141
+ private static onModifyAROptions(options) {
142
+ WebXR.dispatchEvent(WebXREvent.ModifyAROptions, options);
110
143
  }
111
144
 
112
- private async handleOfferSession() {
113
- if (this.createVRButton) {
114
- const hasVRSupport = await NeedleXRSession.isVRSupported();
115
- if (hasVRSupport && this.createVRButton) {
116
- return NeedleXRSession.offerSession("immersive-vr", "default", this.context);
117
- }
118
- }
119
- if (this.createARButton) {
120
- const hasARSupport = await NeedleXRSession.isARSupported();
121
- if (hasARSupport && this.createARButton) {
122
- return NeedleXRSession.offerSession("immersive-ar", "default", this.context);
123
- }
124
- }
125
- return false;
145
+ public static resetButtonStyles(button) {
146
+ if (!button) return;
147
+ button.style.position = "";
148
+ button.style.bottom = "";
149
+ button.style.left = "";
126
150
  }
127
151
 
128
- /** the currently active webxr input session */
129
- get session(): NeedleXRSession | null {
130
- return NeedleXRSession.active ?? null;
152
+ public endSession() {
153
+ const session = this.context.renderer.xr.getSession();
154
+ if (session) session.end();
131
155
  }
132
- /** immersive-vr or immersive-ar */
133
- get sessionMode(): XRSessionMode | null {
134
- return NeedleXRSession.activeMode ?? null;;
156
+
157
+ public get Rig(): Object3D {
158
+ this.ensureRig();
159
+ return this.rig;
135
160
  }
136
161
 
137
- /** Call to start an WebVR session */
138
- async enterVR(init?: XRSessionInit): Promise<NeedleXRSession | null> {
139
- return NeedleXRSession.start("immersive-vr", init, this.context);
162
+
163
+ private controllers: WebXRController[] = [];
164
+ public get Controllers(): WebXRController[] {
165
+ return this.controllers;
140
166
  }
141
- /** Call to start an WebAR session */
142
- async enterAR(init?: XRSessionInit): Promise<NeedleXRSession | null> {
143
- return NeedleXRSession.start("immersive-ar", init, this.context);
167
+
168
+ public get LeftController(): WebXRController | null {
169
+ if (this.controllers.length > 0 && this.controllers[0].input?.handedness === "left") return this.controllers[0];
170
+ if (this.controllers.length > 1 && this.controllers[1].input?.handedness === "left") return this.controllers[1];
171
+ return null;
144
172
  }
145
- /** Call to end a WebXR (AR or VR) session */
146
- exitXR() {
147
- NeedleXRSession.stop();
173
+
174
+ public get RightController(): WebXRController | null {
175
+ if (this.controllers.length > 0 && this.controllers[0].input?.handedness === "right") return this.controllers[0];
176
+ if (this.controllers.length > 1 && this.controllers[1].input?.handedness === "right") return this.controllers[1];
177
+ return null;
148
178
  }
149
179
 
150
- private _previousXRState: number = 0;
180
+ public get ARButton(): HTMLButtonElement | undefined {
181
+ return this._arButton;
182
+ }
151
183
 
152
- onBeforeXR(_mode: XRSessionMode, args: XRSessionInit): void {
153
- if (_mode == "immersive-ar" && this.useDepthSensing) {
154
- args.optionalFeatures = args.optionalFeatures || [];
155
- args.optionalFeatures.push("depth-sensing");
156
- }
184
+ public get VRButton(): HTMLButtonElement | undefined {
185
+ return this._vrButton;
157
186
  }
158
187
 
159
- async onEnterXR(args: NeedleXREventArgs) {
160
- if (debug) console.log("WebXR onEnterXR")
161
- // set XR flags
162
- this._previousXRState = XRState.Global.Mask;
163
- const isVR = args.xr.isVR;
164
- XRState.Global.Set(isVR ? XRStateFlag.VR : XRStateFlag.AR);
188
+ public get IsInVR() { return this._isInVR; }
189
+ public get IsInAR() { return this._isInAR; }
165
190
 
166
- // Handle AR session root
167
- if (this.usePlacementReticle && args.xr.isAR) {
168
- let sessionroot = GameObject.findObjectOfType(WebARSessionRoot);
169
- if (!sessionroot) {
170
- const implicitSessionRoot = new Object3D();
171
- for (const ch of this.context.scene.children)
172
- implicitSessionRoot.add(ch);
173
- this.context.scene.add(implicitSessionRoot);
174
- sessionroot = GameObject.addNewComponent(implicitSessionRoot, WebARSessionRoot)!;
175
- this._createdComponentsInSession.push(sessionroot);
176
- sessionroot.arScale = this.arSceneScale;
177
- sessionroot.arTouchTransform = this.usePlacementAdjustment;
178
- sessionroot.useXRAnchor = this.useXRAnchor;
191
+ /** When enabled */
192
+ allowARPlacementReticle: boolean = true;
193
+
194
+ private rig!: Object3D;
195
+ private isInit: boolean = false;
196
+
197
+ private _requestedAR: boolean = false;
198
+ private _requestedVR: boolean = false;
199
+ private _isInAR: boolean = false;
200
+ private _isInVR: boolean = false;
201
+
202
+ private _arButton?: HTMLButtonElement;
203
+ private _vrButton?: HTMLButtonElement;
204
+
205
+ private webAR: WebAR | null = null;
206
+
207
+ awake(): void {
208
+ // as the webxr component is most of the times currently loaded as part of the scene
209
+ // and not part of the glTF directly and thus does not go through the whole serialization process currently
210
+ // we need to to manuall make sure it is of the correct type here
211
+ if (this.defaultAvatar) {
212
+ if (typeof (this.defaultAvatar) === "string") {
213
+ this.defaultAvatar = AssetReference.getOrCreate(this.sourceId ?? "/", this.defaultAvatar, this.context);
179
214
  }
180
- else if(debug) console.log("WebXR: WebARSessionRoot already exists, not creating a new one")
181
215
  }
216
+ if (!GameObject.findObjectOfType(WebXRSync, this.context)) {
217
+ const sync = GameObject.addNewComponent(this.gameObject, WebXRSync, false) as WebXRSync;
218
+ sync.webXR = this;
219
+ }
220
+ this.webAR = new WebAR(this);
182
221
 
183
- // handle VR controls
184
- if (this.useDefaultControls) {
185
- this.setDefaultMovementEnabled(true);
222
+ if (location.protocol == 'http:' && location.host.indexOf('localhost') < 0) {
223
+ showBalloonWarning("WebXR only works on https");
224
+ console.warn("WebXR only works on https. https://engine.needle.tools/docs/xr.html");
186
225
  }
187
- if (this.showControllerModels || this.showHandModels) {
188
- this.setDefaultControllerRenderingEnabled(true);
226
+ }
227
+
228
+ onEnable() {
229
+ if (this.isInit) return;
230
+ if (!this.enableAR && !this.enableVR) return;
231
+ this.isInit = true;
232
+
233
+ this.context.renderer.xr.enabled = true;
234
+
235
+ // TODO: move the whole buttons positioning out of here and make it configureable from css
236
+ // better set proper classes so user code can react to it instead
237
+ // of this hardcoded stuff
238
+ let arButton, vrButton;
239
+ const buttonsContainer = document.createElement('div');
240
+ buttonsContainer.classList.add("webxr-buttons");
241
+ buttonsContainer.style.cssText = `
242
+ position: absolute;
243
+ bottom: 21px;
244
+ left: 50%;
245
+ transform: translate(-50%, 0%);
246
+ z-index: 1000;
247
+
248
+ display: flex;
249
+ flex-direction: row;
250
+ justify-content: center;
251
+ align-items: flex-start;
252
+ gap: 10px;
253
+ `;
254
+ this.context.appendHTMLElement(buttonsContainer);
255
+
256
+ const forceButtons = debugWebXR;
257
+ if (debugWebXR) console.log("ARSupported?", arSupported, "VRSupported?", vrSupported);
258
+
259
+ // AR support
260
+ if (forceButtons || (this.createARButton && this.enableAR && arSupported)) {
261
+ arButton = WebXR.createARButton(this);
262
+ this._arButton = arButton;
263
+ buttonsContainer.appendChild(arButton);
189
264
  }
190
265
 
191
- // ensure we have a spatial grab raycaster for close grabs
192
- let raycaster = GameObject.findObjectOfType(SpatialGrabRaycaster);
193
- if (!raycaster) {
194
- raycaster = this.gameObject.addNewComponent(SpatialGrabRaycaster);
266
+ // VR support
267
+ if (forceButtons || (this.createVRButton && this.enableVR && vrSupported)) {
268
+ vrButton = WebXR.createVRButton(this);
269
+ this._vrButton = vrButton;
270
+ buttonsContainer.appendChild(vrButton);
195
271
  }
196
272
 
197
- this.createLocalAvatar(args.xr);
273
+ setTimeout(() => {
274
+ WebXR.resetButtonStyles(vrButton);
275
+ WebXR.resetButtonStyles(arButton);
276
+ }, 1000);
198
277
  }
199
278
 
200
- onLeaveXR(_: NeedleXREventArgs): void {
201
- // revert XR flags
202
- XRState.Global.Set(this._previousXRState);
279
+ private _transformOrientation: Quaternion = new Quaternion();
280
+ public get TransformOrientation(): Quaternion { return this._transformOrientation; }
203
281
 
204
- this._playerSync?.destroyInstance();
282
+ private _currentHeadPose: XRViewerPose | null = null;
283
+ public get HeadPose(): XRViewerPose | null { return this._currentHeadPose; }
205
284
 
206
- for (const comp of this._createdComponentsInSession) {
207
- comp.destroy();
285
+ onBeforeRender(frame:XRFrame | null | undefined) {
286
+ if (!frame) return;
287
+ // TODO: figure out why screen is black if we enable the code written here
288
+ // const referenceSpace = renderer.xr.getReferenceSpace();
289
+ const session = this.context.renderer.xr.getSession();
290
+
291
+
292
+ if (session) {
293
+ const referenceSpace = this.context.renderer.xr.getReferenceSpace();
294
+ if(!referenceSpace) return;
295
+ const pose = frame.getViewerPose(referenceSpace);
296
+ if (!pose) return;
297
+ this._currentHeadPose = pose;
298
+ const transform: XRRigidTransform = pose?.transform;
299
+ if (transform) {
300
+ this._transformOrientation.set(transform.orientation.x, transform.orientation.y, transform.orientation.z, transform.orientation.w);
301
+ }
302
+
303
+ if (WebXR._isInXr === false && session) {
304
+ this.onEnterXR(session, frame);
305
+ }
306
+ else if (this.IsInVR) {
307
+ if (this.context.mainCamera) {
308
+ this.ensureRig();
309
+ }
310
+ }
311
+
312
+ for (const ctrl of this.controllers) {
313
+ ctrl.onUpdate(session);
314
+ }
315
+
316
+ if (this._isInAR) {
317
+ this.webAR?.onUpdate(session, frame);
318
+ }
208
319
  }
209
- this._createdComponentsInSession.length = 0;
210
320
 
211
- this.handleOfferSession();
321
+ WebXR.events.dispatchEvent({ type: WebXREvent.XRUpdate, frame: frame, xr: this.context.renderer.xr, rig: this.rig });
212
322
  }
213
323
 
324
+ private onClickedARButton() {
325
+ if (!this._isInAR) {
326
+ this._requestedAR = true;
327
+ this._requestedVR = false;
214
328
 
215
- /** Call to enable or disable default controller behaviour */
216
- setDefaultMovementEnabled(enabled: boolean): XRControllerMovement | null {
217
- let movement = this.gameObject.getComponent(XRControllerMovement)
218
- if (!movement && enabled) {
219
- movement = this.gameObject.addNewComponent(XRControllerMovement)!;
220
- this._createdComponentsInSession.push(movement);
329
+ // if we do this on enter xr the state has already been changed in AR mode
330
+ // so we need to to this before session has started
331
+ this.captureStateBeforeXR();
221
332
  }
222
- if (movement) movement.enabled = enabled;
223
- return movement;
224
333
  }
225
- /** Call to enable or disable default controller rendering */
226
- setDefaultControllerRenderingEnabled(enabled: boolean): XRControllerModel | null {
227
- let models = this.gameObject.getComponent(XRControllerModel);
228
- if (!models && enabled) {
229
- models = this.gameObject.addNewComponent(XRControllerModel)!;
230
- this._createdComponentsInSession.push(models);
231
- models.createControllerModel = this.showControllerModels;
232
- models.createHandModel == this.showHandModels;
334
+
335
+ private onClickedVRButton() {
336
+ if (!this._isInVR) {
337
+
338
+ // happens e.g. when headset is off and xr session never actually started
339
+ if (this._requestedVR) {
340
+ this.onExitXR(null);
341
+ return;
342
+ }
343
+
344
+ this._requestedAR = false;
345
+ this._requestedVR = true;
346
+ this.captureStateBeforeXR();
347
+
348
+ // build controllers before session begins - this seems to fix issue with controller models not appearing/not getting connection event
349
+ this.ensureRig();
350
+ for (let i = 0; i < 2; i++) {
351
+ WebXRController.Create(this, i, this.gameObject as GameObject, ControllerType.PhysicalDevice);
352
+ }
353
+
354
+ WebXR.events.dispatchEvent({ type: WebXREvent.RequestVRSession });
233
355
  }
234
- if (models) models.enabled = enabled;
235
- return models;
236
356
  }
237
357
 
358
+ private captureStateBeforeXR() {
359
+ if (this.context.mainCamera) {
360
+ this._originalCameraPosition.copy(getWorldPosition(this.context.mainCamera));
361
+ this._originalCameraRotation.copy(getWorldQuaternion(this.context.mainCamera));
362
+ this._originalCameraParent = this.context.mainCamera.parent;
363
+ }
364
+ if (this.Rig) {
365
+ this._originalXRRigParent = this.Rig.parent;
366
+ this._originalXRRigPosition.copy(this.Rig.position);
367
+ this._originalXRRigRotation.copy(this.Rig.quaternion);
368
+ }
369
+ }
238
370
 
371
+ private ensureRig() {
372
+ if (!this.rig || isDestroyed(this.rig)) {
373
+ // currently just used for pose
374
+ const xrRig = GameObject.findObjectOfType(XRRig, this.context);
375
+ if (xrRig) {
376
+ // make it match unity forward
377
+ this.rig = xrRig.gameObject;
378
+ this.rig.rotateY(Math.PI);
379
+ // this.rig.position.copy(existing.worldPosition);
380
+ // this.rig.quaternion.premultiply(existing.worldQuaternion);
381
+ }
382
+ else {
383
+ this.rig = new Group();
384
+ this.rig.rotateY(Math.PI);
385
+ this.rig.name = "XRRig";
386
+ this.context.scene.add(this.rig);
387
+ }
388
+ }
239
389
 
240
- protected async createLocalAvatar(xr: NeedleXRSession) {
241
- if (this._playerSync && xr.running) {
242
- this._playerSync.asset = this.defaultAvatar;
243
- await this._playerSync.getInstance();
390
+ // Make sure the webxr camera is parented to the xr rig
391
+ if (this.context.isInXR && this.context.mainCamera && this.context.mainCamera.parent !== this.rig) {
392
+ this.rig.add(this.context.mainCamera);
393
+
394
+ // Hack: make sure we have the correct position and rotation (e.g. where we are dealing with an implicitly created rig)
395
+ // This handles the case where we switch between multiple scenes
396
+ if (this.IsInVR) {
397
+ const other = GameObject.findObjectOfType(XRRig);
398
+ if (other && other?.gameObject !== this.rig) {
399
+ this.rig.position.copy(other.gameObject.position);
400
+ this.rig.quaternion.copy(other.gameObject.quaternion);
401
+ this.rig.rotateY(Math.PI);
402
+ this.rig.scale.copy(other.gameObject.scale);
403
+ }
404
+ }
244
405
  }
245
406
  }
246
407
 
247
- private onAvatarSpawned = (instance: GameObject) => {
248
- // spawned webxr avatars must have a avatar component
249
- if (debug) console.log("WebXR.onAvatarSpawned", instance);
250
- GameObject.getOrAddComponent(instance, Avatar);
251
- };
252
408
 
409
+ private _originalCameraParent: Object3D | null = null;
410
+ private _originalCameraPosition: Vector3 = new Vector3();
411
+ private _originalCameraRotation: Quaternion = new Quaternion();
253
412
 
413
+ private _originalXRRigParent: Object3D | null = null;
414
+ private _originalXRRigPosition: Vector3 = new Vector3();
415
+ private _originalXRRigRotation: Quaternion = new Quaternion();
254
416
 
417
+ private onEnterXR(session: XRSession, frame: XRFrame) {
418
+ console.log("[XR] session begin", session, frame);
419
+ WebXR._isInXr = true;
255
420
 
256
- // HTML UI
257
- /** Calling this function will get the Needle WebXR button container (it will be created if it doesnt exist yet)
258
- * @returns the Needle WebXR button container */
259
- getButtonsContainer(): NeedleWebXRHtmlElement {
260
- if (!this._container) {
261
- this._container = NeedleWebXRHtmlElement.create();
262
- this.context.domElement.shadowRoot?.appendChild(this._container);
421
+ this.ensureRig();
422
+
423
+ const space = this.context.renderer.xr.getReferenceSpace();
424
+ if (space && this.rig) {
425
+ const pose = frame.getViewerPose(space);
426
+ const rot = pose?.transform.orientation;
427
+ if (rot) {
428
+ const quat = new Quaternion(rot.x, rot.y, rot.z, rot.w);
429
+ const eu = new Euler().setFromQuaternion(quat);
430
+ this.rig.rotateY(eu.y);
431
+ // this.rig.quaternion.multiply(quat);
432
+ }
263
433
  }
264
- return this._container;
434
+
435
+ // when we set unity layers objects will only be rendered on one eye
436
+ // we set layers to sync raycasting and have a similar behaviour to unity
437
+ const xr = this.context.renderer.xr;
438
+ if (this.context.mainCamera) {
439
+ const cam = xr.getCamera() as WebXRArrayCamera;
440
+ if (debugWebXR) console.log("WebXRCamera", cam);
441
+ const cull = this.context.mainCameraComponent?.cullingMask;
442
+ if (cam && cull !== undefined) {
443
+ for (const c of cam.cameras) {
444
+ c.layers.mask = cull;
445
+ }
446
+ cam.layers.mask = cull;
447
+ }
448
+ else if (cam) {
449
+ for (const c of cam.cameras) {
450
+ c.layers.enableAll();
451
+ }
452
+ cam.layers.enableAll();
453
+ }
454
+ if (this._requestedAR) {
455
+ this.context.scene.add(this.rig);
456
+ }
457
+ }
458
+
459
+ const flag = this._requestedAR ? XRStateFlag.AR : XRStateFlag.VR;
460
+
461
+ XRState.Global.Set(flag);
462
+
463
+ switch (flag) {
464
+ case XRStateFlag.AR:
465
+ this.context.xrSessionMode = XRSessionMode.ImmersiveAR;
466
+ this._isInAR = true;
467
+ this.webAR?.onBegin(session);
468
+ break;
469
+ case XRStateFlag.VR:
470
+ this.context.xrSessionMode = XRSessionMode.ImmersiveVR;
471
+ this._isInVR = true;
472
+ this.onEnterVR(session);
473
+ break;
474
+ }
475
+
476
+ session.addEventListener('end', () => {
477
+ console.log("[XR] session end");
478
+ WebXR._isInXr = false;
479
+ this.onExitXR(session);
480
+ });
481
+
482
+ this.onEnterXR_HandleMirrorWindow(session);
483
+
484
+ WebXR.events.dispatchEvent({ type: WebXREvent.XRStarted, session: session });
265
485
  }
266
486
 
267
- private _container?: NeedleWebXRHtmlElement;
268
- private handleCreatingHTML() {
487
+ private onExitXR(session: XRSession | null) {
269
488
 
270
- if (this.createARButton || this.createVRButton || this.useQuicklookExport) {
271
- // Quicklook / iOS
272
- if ((isiOS() && isSafari()) || debugQuicklook) {
273
- if (this.useQuicklookExport) {
274
- this.getButtonsContainer().createQuicklookButton();
489
+ const wasInAR = this._isInAR;
490
+
491
+ if (session) {
492
+ if (this._isInAR) {
493
+ this.webAR?.onEnd(session);
494
+ }
495
+ else {
496
+ // if in VR we want to restore the FOV
497
+ this.context.mainCameraComponent?.applyClearFlagsIfIsActiveCamera();
498
+ }
499
+ }
500
+
501
+ this._isInAR = false;
502
+ this._isInVR = false;
503
+ this._requestedAR = false;
504
+ this._requestedVR = false;
505
+ this.context.xrSessionMode = undefined;
506
+
507
+ if (this.xrMirrorWindow) {
508
+ this.xrMirrorWindow.close();
509
+ this.xrMirrorWindow = null;
510
+ }
511
+
512
+ this.destroyControllers();
513
+
514
+ if (this.context.mainCamera) {
515
+ this._originalCameraParent?.add(this.context.mainCamera);
516
+ setWorldPosition(this.context.mainCamera, this._originalCameraPosition);
517
+ setWorldQuaternion(this.context.mainCamera, this._originalCameraRotation);
518
+ this.context.mainCamera.scale.set(1, 1, 1);
519
+ }
520
+
521
+ if (wasInAR) {
522
+ this._originalXRRigParent?.add(this.rig);
523
+ this.rig.position.copy(this._originalXRRigPosition);
524
+ this.rig.quaternion.copy(this._originalXRRigRotation);
525
+ }
526
+
527
+ XRState.Global.Set(XRStateFlag.Browser | XRStateFlag.ThirdPerson);
528
+ WebXR.events.dispatchEvent({ type: WebXREvent.XRStopped, session: session });
529
+ }
530
+
531
+ private onEnterVR(_session: XRSession) {
532
+ }
533
+
534
+ private destroyControllers() {
535
+ for (let i = this.controllers.length - 1; i >= 0; i -= 1) {
536
+ this.controllers[i]?.destroy();
537
+ }
538
+ this.controllers.length = 0;
539
+ }
540
+
541
+ private xrMirrorWindow: Window | null = null;
542
+
543
+ private onEnterXR_HandleMirrorWindow(session: XRSession) {
544
+ if (!getParam("mirror")) return;
545
+ setTimeout(() => {
546
+ if (!WebXR.IsInWebXR) return;
547
+ const url = new URL(window.location.href);
548
+ setOrAddParamsToUrl(url.searchParams, noVoip, 1);
549
+ setOrAddParamsToUrl(url.searchParams, "isMirror", 1);
550
+ const str = url.toString();
551
+ this.xrMirrorWindow = window.open(str, "webxr sync", "popup=yes");
552
+ if (this.xrMirrorWindow) {
553
+ this.xrMirrorWindow.onload = () => {
554
+ if (this.xrMirrorWindow)
555
+ this.xrMirrorWindow.onbeforeunload = () => {
556
+ if (WebXR.IsInWebXR)
557
+ session.end();
558
+ };
275
559
  }
276
560
  }
277
- // WebXR
278
- if (this.createARButton) this.getButtonsContainer().createARButton();
279
- if (this.createVRButton) this.getButtonsContainer().createVRButton();
561
+ }, 1000);
562
+ }
563
+ }
564
+
565
+
566
+ // not sure if this should be a behaviour.
567
+ // for now we dont really need it to go through the usual update loop
568
+ export class WebAR {
569
+
570
+ get webxr(): WebXR { return this._webxr; }
571
+
572
+ private _webxr: WebXR;
573
+
574
+ private reticle: Object3D | null = null;
575
+ private reticleParent: Object3D | null = null;
576
+ private hitTestSource: XRHitTestSource | null = null;
577
+ private reticleActive: boolean = true;
578
+
579
+ // scene.background before entering AR
580
+ private previousBackground: Color | null | Texture = null;
581
+ private previousEnvironment: Texture | null = null;
582
+
583
+ private sessionRoot: WebARSessionRoot | null = null;
584
+ private _previousParent: Object3D | null = null;
585
+ // we need this in case the session root is on the same object as the webxr component
586
+ // so if we disable the session root we attach the webxr component to this temporary object
587
+ // to still receive updates
588
+ private static tempWebXRObject: Object3D;
589
+
590
+ private get context() { return this.webxr.context; }
591
+
592
+ constructor(webxr: WebXR) {
593
+ this._webxr = webxr;
594
+ }
595
+
596
+ private arDomOverlay: HTMLElement | null = null;
597
+ private arOverlayElement: INeedleEngineComponent | HTMLElement | null = null;
598
+ private noHitTestAvailable: boolean = false;
599
+ private didPlaceARSessionRoot: boolean = false;
600
+
601
+ getAROverlayContainer(): HTMLElement | null {
602
+ this.arDomOverlay = this.webxr.context.domElement as HTMLElement;
603
+ // for react cases we dont have an Engine Element
604
+ const element: any = this.arDomOverlay;
605
+ if (element.getAROverlayContainer)
606
+ this.arOverlayElement = element.getAROverlayContainer();
607
+ else this.arOverlayElement = this.arDomOverlay;
608
+ return this.arOverlayElement;
609
+ }
610
+
611
+ setReticleActive(active: boolean) {
612
+ this.reticleActive = active;
613
+ }
614
+
615
+ async onBegin(session: XRSession) {
616
+ const context = this.webxr.context;
617
+ this.reticleActive = true;
618
+ this.didPlaceARSessionRoot = false;
619
+ this.getAROverlayContainer();
620
+
621
+ const deviceType = isQuest() ? ControllerType.PhysicalDevice : ControllerType.Touch;
622
+ const controllerCount = deviceType === ControllerType.Touch ? 4 : 2;
623
+ for (let i = 0; i < controllerCount; i++) {
624
+ WebXRController.Create(this.webxr, i, this.webxr.gameObject as GameObject, deviceType)
280
625
  }
281
626
 
282
- if (this.createSendToQuestButton && !isQuest()) {
283
- NeedleXRSession.isVRSupported().then(supported => {
284
- if (!supported) this.getButtonsContainer().createSendToQuestButton();
285
- });
627
+ if (!this.sessionRoot || this.sessionRoot.destroyed || !this.sessionRoot.activeAndEnabled)
628
+ this.sessionRoot = GameObject.findObjectOfType(WebARSessionRoot, context);
629
+ if (!this.sessionRoot) {
630
+ // TODO: adding it on the scene directly doesnt work (probably because then everything in the scene is disabled including this component). See code a bit furhter below where we add this component to a temporary object inside the scene
631
+ const obj = this.webxr.gameObject;
632
+ this.sessionRoot = GameObject.addNewComponent(obj, WebARSessionRoot);
633
+ console.warn("WebAR: No ARSessionRoot found, creating one automatically on the WebXR object");
286
634
  }
287
635
 
288
- if (this.createQRCode && !isMobileDevice()) {
289
- NeedleXRSession.isXRSupported().then(supported => {
290
- if (isDesktop() || !supported) this.getButtonsContainer().createQRCode();
636
+ this.previousBackground = context.scene.background;
637
+ this.previousEnvironment = context.scene.environment;
638
+ context.scene.background = null;
639
+
640
+ session.requestReferenceSpace('viewer').then((referenceSpace) => {
641
+ session.requestHitTestSource?.call(session, { space: referenceSpace })?.then((source) => {
642
+ this.hitTestSource = source;
643
+ }).catch((err) => {
644
+ this.noHitTestAvailable = true;
645
+ console.warn("WebXR: Hit test not supported", err);
291
646
  });
647
+ });
648
+
649
+ if (!this.reticle && this.sessionRoot) {
650
+ this.reticle = new Mesh(
651
+ new RingGeometry(0.07, 0.09, 32).rotateX(- Math.PI / 2),
652
+ new MeshBasicMaterial()
653
+ );
654
+ this.reticle.name = "AR Placement reticle";
655
+ this.reticle.matrixAutoUpdate = false;
656
+ this.reticle.visible = false;
657
+
658
+ // create AR reticle parent to allow WebXRSessionRoot to be translated, rotated or scaled
659
+ this.reticleParent = new Object3D();
660
+ this.reticleParent.name = "AR Reticle Parent";
661
+ this.reticleParent.matrixAutoUpdate = false;
662
+ this.reticleParent.add(this.reticle);
663
+ // this.reticleParent.matrix.copy(this.sessionRoot.gameObject.matrixWorld);
664
+
665
+ if (this.webxr.scene) {
666
+ this.context.scene.add(this.reticleParent);
667
+ // this.context.scene.add(this.reticle);
668
+ this.context.scene.visible = true;
669
+ }
670
+ else console.warn("Could not found WebXR Rig");
292
671
  }
672
+
673
+ this._previousParent = this.webxr.gameObject;
674
+ if (!WebAR.tempWebXRObject) WebAR.tempWebXRObject = new Object3D();
675
+ this.context.scene.add(WebAR.tempWebXRObject);
676
+ GameObject.addComponent(WebAR.tempWebXRObject as GameObject, this.webxr);
677
+
678
+ if (this.sessionRoot) {
679
+ this.sessionRoot.webAR = this;
680
+ this.sessionRoot?.onBegin(session);
681
+ }
682
+ else console.warn("No WebARSessionRoot found in scene")
683
+
684
+ const eng = this.context.domElement as INeedleEngineComponent;
685
+ eng?.onEnterAR?.call(eng, session, this.arOverlayElement!);
686
+
687
+ this.context.mainCameraComponent?.applyClearFlagsIfIsActiveCamera();
293
688
  }
294
689
 
690
+ onEnd(session: XRSession) {
691
+ if (this._previousParent) {
692
+ GameObject.addComponent(this._previousParent as GameObject, this.webxr);
693
+ this._previousParent = null;
694
+ }
695
+ this.hitTestSource = null;
696
+ const context = this.webxr.context;
697
+ context.scene.background = this.previousBackground;
698
+ context.scene.environment = this.previousEnvironment;
699
+ if (this.sessionRoot) {
700
+ this.sessionRoot.onEnd(this.webxr.Rig, session);
701
+ }
295
702
 
703
+ const el = this.context.domElement as INeedleEngineComponent;
704
+ el.onExitAR?.call(el, session);
705
+
706
+ this.context.mainCameraComponent?.applyClearFlagsIfIsActiveCamera();
707
+ }
708
+
709
+ onUpdate(session: XRSession, frame: XRFrame) {
710
+
711
+ if (this.noHitTestAvailable === true) {
712
+ if (this.reticle)
713
+ this.reticle.visible = false;
714
+ if (!this.didPlaceARSessionRoot) {
715
+ this.didPlaceARSessionRoot = true;
716
+ const rig = this.webxr.Rig;
717
+ const placementMatrix = arPlacementWithoutHitTestMatrix.clone();
718
+ // if (rig) {
719
+ // const positionFromRig = new Vector3(0, 0, 0).add(rig.position).divideScalar(this.sessionRoot?.arScale ?? 1);
720
+ // placementMatrix.multiply(new Matrix4().makeTranslation(positionFromRig.x, positionFromRig.y, positionFromRig.z));
721
+ // // placementMatrix.setPosition(positionFromRig);
722
+ // }
723
+ this.sessionRoot?.placeAt(rig, placementMatrix);
724
+ }
725
+ return;
726
+ }
727
+
728
+ if (!this.hitTestSource) return;
729
+ const hitTestResults = frame.getHitTestResults(this.hitTestSource);
730
+ if (hitTestResults.length) {
731
+ const hit = hitTestResults[0];
732
+ const referenceSpace = this.webxr.context.renderer.xr.getReferenceSpace();
733
+ if (referenceSpace) {
734
+ const pose = hit.getPose(referenceSpace);
735
+
736
+ if (this.sessionRoot) {
737
+ const didPlace = this.sessionRoot.onUpdate(this.webxr.Rig, session, hit, pose);
738
+ this.didPlaceARSessionRoot = didPlace;
739
+ }
740
+
741
+ if (this.reticle) {
742
+ this.reticle.visible = this.reticleActive && this._webxr.allowARPlacementReticle;
743
+ if (this.reticleActive) {
744
+ if (pose) {
745
+ const matrix = pose.transform.matrix;
746
+ this.reticle.matrix.fromArray(matrix);
747
+ if (this.webxr.Rig)
748
+ this.reticle.matrix.premultiply(this.webxr.Rig.matrix);
749
+ }
750
+ }
751
+ }
752
+ }
753
+
754
+ } else {
755
+ this.sessionRoot?.onUpdate(this.webxr.Rig, session, null, null);
756
+ if (this.reticle)
757
+ this.reticle.visible = false;
758
+ }
759
+ }
296
760
  }
761
+
762
+ const arPlacementWithoutHitTestMatrix = new Matrix4().identity().makeTranslation(0, 0, 0);
src/engine-components/webxr/WebXRAvatar.ts CHANGED
@@ -1,8 +1,16 @@
1
+ import { Behaviour, GameObject } from "../Component.js";
2
+ import { WebXR } from "./WebXR.js";
3
+ import { Quaternion, Vector3 } from "three";
4
+ import { AvatarLoader } from "../AvatarLoader.js";
5
+ import { XRFlag, XRStateFlag } from "../XRFlag.js";
6
+ import { Avatar_POI } from "../avatar/Avatar_Brain_LookAt.js";
7
+ import { Context } from "../../engine/engine_setup.js";
8
+ import { AssetReference } from "../../engine/engine_addressables.js";
1
9
  import { Object3D } from "three";
2
-
10
+ import { VRUserState } from "./WebXRSync.js";
3
11
  import { getParam } from "../../engine/engine_utils.js";
4
- import { Behaviour, GameObject } from "../Component.js";
5
- import { XRFlag } from "./XRFlag.js";
12
+ import { ViewDevice } from "../../engine/engine_playerview.js";
13
+ import { InstancingUtil } from "../../engine/engine_instancing.js";
6
14
 
7
15
  export const debug = getParam("debugavatar");
8
16
 
@@ -11,12 +19,6 @@
11
19
  gameObject: Object3D;
12
20
  }
13
21
 
14
- /**
15
- * This is used to mark an object being controlled / owned by a player
16
- * This system might be refactored and moved to a more centralized place in a future version
17
- */
18
- // We might be updating this system in the future to a centralized API (PlayerView)
19
- // but since currently quite a few core components rely on it, we're keeping it for now
20
22
  export class AvatarMarker extends Behaviour {
21
23
 
22
24
  public static getAvatar(index: number): AvatarMarker | null {
@@ -42,7 +44,7 @@
42
44
 
43
45
 
44
46
  public connectionId!: string;
45
- public avatar?: Object3D & { flags?: XRFlag[] }
47
+ public avatar?: WebXRAvatar | Object3D;
46
48
 
47
49
  awake() {
48
50
  AvatarMarker.instances.push(this);
@@ -63,4 +65,292 @@
63
65
  isLocalAvatar() {
64
66
  return this.connectionId === this.context.connection.connectionId;
65
67
  }
68
+
69
+ setVisible(visible: boolean) {
70
+ if (this.avatar) {
71
+ if ("setVisible" in this.avatar)
72
+ this.avatar.setVisible(visible);
73
+ else {
74
+ GameObject.setActive(this.avatar, visible);
75
+ }
76
+ }
77
+ }
66
78
  }
79
+
80
+
81
+ export class WebXRAvatar {
82
+ private static loader: AvatarLoader = new AvatarLoader();
83
+
84
+ private _isVisible: boolean = true;
85
+ setVisible(visible: boolean) {
86
+ this._isVisible = visible;
87
+ this.updateVisibility();
88
+ }
89
+
90
+ get isWebXRAvatar() { return true; }
91
+
92
+ // TODO: set layers on all avatars
93
+ /** the user id */
94
+ public guid: string;
95
+
96
+ private root: Object3D | null = null;
97
+ public head: Object3D | null = null;
98
+ public handLeft: Object3D | null = null;
99
+ public handRight: Object3D | null = null;
100
+ public lastUpdate: number = -1;
101
+ public isLocalAvatar: boolean = false;
102
+ public flags: XRFlag[] | null = null;
103
+ private headScale: Vector3 = new Vector3(1, 1, 1);
104
+ private handLeftScale: Vector3 = new Vector3(1, 1, 1);
105
+ private handRightScale: Vector3 = new Vector3(1, 1, 1);
106
+
107
+ private readonly webxr: WebXR;
108
+
109
+ private lastAvatarId: string | null = null;
110
+ private hasAvatarOverride: boolean = false;
111
+
112
+
113
+ private context: Context;
114
+ private avatarMarker: AvatarMarker | null = null;
115
+
116
+ constructor(context: Context, guid: string, webXR: WebXR) {
117
+ this.context = context;
118
+ this.guid = guid;
119
+ this.webxr = webXR;
120
+ this.setupCustomAvatar(this.webxr.defaultAvatar);
121
+ }
122
+
123
+ public updateFlags() {
124
+ if (!this.flags)
125
+ return;
126
+ let mask = this.isLocalAvatar ? XRStateFlag.FirstPerson : XRStateFlag.ThirdPerson;
127
+ if (this.context.isInVR)
128
+ mask |= XRStateFlag.VR;
129
+ else if (this.context.isInAR)
130
+ mask |= XRStateFlag.AR;
131
+ else
132
+ mask |= XRStateFlag.Browser;
133
+ for (const f of this.flags) {
134
+ f.gameObject.visible = true;
135
+ f.UpdateVisible(mask);
136
+ }
137
+ }
138
+
139
+ public async setAvatarOverride(avatarId: string | null): Promise<boolean | null> {
140
+ this.hasAvatarOverride = avatarId !== null;
141
+ if (this.hasAvatarOverride && this.lastAvatarId !== avatarId) {
142
+ this.lastAvatarId = avatarId;
143
+ if (avatarId != null && avatarId.length > 0)
144
+ return await this.setupCustomAvatar(avatarId);
145
+ }
146
+ return null;
147
+ }
148
+
149
+ private _headTarget: Object3D = new Object3D();
150
+ private _handLeftTarget: Object3D = new Object3D();
151
+ private _handRightTarget: Object3D = new Object3D();
152
+ private _canInterpolate: boolean = false;
153
+
154
+ private static invertRotation: Quaternion = new Quaternion().setFromAxisAngle(new Vector3(0, 1, 0), Math.PI);
155
+
156
+ public tryUpdate(state: VRUserState, _timeDiff: number) {
157
+ if (state.guid === this.guid) {
158
+
159
+ if (this.lastAvatarId !== state.avatarId && state.avatarId && state.avatarId.length > 0) {
160
+ this.lastAvatarId = state.avatarId;
161
+ this.setupCustomAvatar(state.avatarId);
162
+ }
163
+
164
+ this.lastUpdate = state.time;
165
+ if (this.head) {
166
+
167
+ const device = this.webxr.IsInAR ? ViewDevice.Handheld : ViewDevice.Headset;
168
+ const viewObj = this.head;
169
+ // if (this.isLocalAvatar) {
170
+ // if (this.context.mainCamera && this.context.isInXR) {
171
+ // viewObj = this.context.renderer.xr.getCamera(this.context.mainCamera);
172
+ // }
173
+ // }
174
+ this.context.players.setPlayerView(state.guid, viewObj, device);
175
+
176
+ InstancingUtil.markDirty(this.head);
177
+
178
+ this._canInterpolate = true;
179
+ const ht = this.isLocalAvatar ? this.head : this._headTarget;
180
+ ht.position.set(state.position.x, state.position.y, state.position.z);
181
+ // not sure how position in local space can be correct but rotation is wrong / offset when parent rotates
182
+ ht.quaternion.set(state.rotation.x, state.rotation.y, state.rotation.z, state.rotation.w);
183
+ ht.scale.set(state.scale, state.scale, state.scale);
184
+ ht.scale.multiply(this.headScale);
185
+
186
+ if (this.handLeft) {
187
+ const ht = this.isLocalAvatar ? this.handLeft : this._handLeftTarget;
188
+ ht.position.set(state.posLeftHand.x, state.posLeftHand.y, state.posLeftHand.z);
189
+ ht.quaternion.set(state.rotLeftHand["_x"], state.rotLeftHand["_y"], state.rotLeftHand["_z"], state.rotLeftHand["_w"]);
190
+ ht.quaternion.multiply(WebXRAvatar.invertRotation);
191
+ ht.scale.set(state.scale, state.scale, state.scale);
192
+ ht.scale.multiply(this.handLeftScale);
193
+ InstancingUtil.markDirty(this.handLeft);
194
+ }
195
+
196
+ if (this.handRight) {
197
+ const ht = this.isLocalAvatar ? this.handRight : this._handRightTarget;
198
+ ht.position.set(state.posRightHand.x, state.posRightHand.y, state.posRightHand.z);
199
+ ht.quaternion.set(state.rotRightHand["_x"], state.rotRightHand["_y"], state.rotRightHand["_z"], state.rotRightHand["_w"]);
200
+ ht.quaternion.multiply(WebXRAvatar.invertRotation);
201
+ ht.scale.set(state.scale, state.scale, state.scale);
202
+ ht.scale.multiply(this.handRightScale);
203
+ InstancingUtil.markDirty(this.handRight);
204
+ }
205
+ }
206
+ }
207
+ }
208
+
209
+ public update() {
210
+ if (this.isLocalAvatar)
211
+ return;
212
+ if (!this._canInterpolate)
213
+ return;
214
+ const t = this.context.time.deltaTime / .1;
215
+ if (this.head) {
216
+ this.head.position.lerp(this._headTarget.position, t);
217
+ this.head.quaternion.slerp(this._headTarget.quaternion, t);
218
+ this.head.scale.lerp(this._headTarget.scale, t);
219
+ }
220
+ if (this.handLeft && this._handLeftTarget) {
221
+ this.handLeft.position.lerp(this._handLeftTarget.position, t);
222
+ this.handLeft.quaternion.slerp(this._handLeftTarget.quaternion, t);
223
+ this.handLeft.scale.lerp(this._handLeftTarget.scale, t);
224
+ }
225
+ if (this.handRight && this._handRightTarget) {
226
+ this.handRight.position.lerp(this._handRightTarget.position, t);
227
+ this.handRight.quaternion.slerp(this._handRightTarget.quaternion, t);
228
+ this.handRight.scale.lerp(this._handRightTarget.scale, t);
229
+ }
230
+ }
231
+
232
+ public destroy() {
233
+ if (debug)
234
+ console.log("Destroy avatar", this.guid);
235
+ this.root?.removeFromParent();
236
+ this.avatarMarker?.destroy();
237
+ this.lastAvatarId = null;
238
+
239
+ if (this.head) {
240
+ Avatar_POI.Remove(this.context, this.head);
241
+ }
242
+ // this.head?.removeFromParent();
243
+ // this.handLeft?.removeFromParent();
244
+ // this.handRight?.removeFromParent();
245
+ }
246
+
247
+ private updateVisibility() {
248
+ const root = this.root;
249
+ if (root) {
250
+ GameObject.setActive(root, this._isVisible);
251
+ }
252
+ }
253
+
254
+ private async setupCustomAvatar(avatarId: string | Object3D | AssetReference | undefined): Promise<boolean> {
255
+ if (debug)
256
+ console.log("LOAD", avatarId, this);
257
+
258
+ if (!avatarId || (typeof avatarId === "string" && avatarId.length <= 0))
259
+ return false;
260
+
261
+ if (this.head) {
262
+ Avatar_POI.Remove(this.context, this.head);
263
+ }
264
+
265
+ const reference = avatarId as AssetReference;
266
+ if (reference?.loadAssetAsync !== undefined) {
267
+ await reference.loadAssetAsync();
268
+ const prefab = reference.asset as Object3D;
269
+ GameObject.setActive(prefab, false);
270
+ avatarId = GameObject.instantiate(prefab as Object3D) as Object3D;
271
+ GameObject.setActive(avatarId, true);
272
+ // console.log("Avatar", avatarId);
273
+ }
274
+ if (debug)
275
+ console.log(avatarId);
276
+
277
+ const model = await WebXRAvatar.loader.getOrCreateNewAvatarInstance(this.context, avatarId as (Object3D | string));
278
+ if (debug)
279
+ console.log(model, model?.isValid, this.lastAvatarId, avatarId);
280
+ // if (this.lastAvatarId !== avatarId) {
281
+ // // avatar id changed in the meantime
282
+ // return true;
283
+ // }
284
+ if (model?.isValid) {
285
+ this.root = model.root;
286
+
287
+ this.root.position.set(0, 0, 0);
288
+ this.root.quaternion.set(0, 0, 0, 1);
289
+ this.root.scale.set(1, 1, 1); // should we allow a scaled avatar root?!
290
+
291
+ this.avatarMarker = GameObject.addNewComponent(this.root as GameObject, AvatarMarker) as AvatarMarker;
292
+ this.avatarMarker.connectionId = this.guid;
293
+ this.avatarMarker.avatar = this;
294
+
295
+ if (this.head && this.head !== model.head)
296
+ this.head?.removeFromParent();
297
+ this.head = model.head;
298
+ this.headScale.copy(this.head.scale);
299
+
300
+ if (this.head && !this.isLocalAvatar) {
301
+ Avatar_POI.Add(this.context, this.head, this.avatarMarker);
302
+ }
303
+
304
+ if (model.leftHand)
305
+ this.handLeft?.removeFromParent();
306
+ this.handLeft = model.leftHand ?? this.handLeft;
307
+ if (this.handLeft)
308
+ this.handLeftScale.copy(this.handLeft.scale);
309
+ else
310
+ this.handLeftScale.set(1, 1, 1);
311
+
312
+ if (model.rigthHand)
313
+ this.handRight?.removeFromParent();
314
+ this.handRight = model.rigthHand ?? this.handRight;
315
+ if (this.handRight)
316
+ this.handRightScale.copy(this.handRight.scale);
317
+ else
318
+ this.handRightScale.set(1, 1, 1);
319
+
320
+
321
+ this.context.scene.add(this.root);
322
+ // scene.add(this.handLeft);
323
+ // scene.add(this.handRight);
324
+ // this.mouthShapes = null;
325
+ // this.needSearchEyes = true;
326
+ if (this.flags == null)
327
+ this.flags = [];
328
+ this.flags.length = 0;
329
+ this.flags.push(...GameObject.getComponentsInChildren(this.root as GameObject, XRFlag));
330
+ // if no flags are found add at least a head flag to hide head in first person VR
331
+ if (this.flags.length <= 0) {
332
+ if (this.head) {
333
+ const flag = GameObject.addNewComponent(this.head, XRFlag) as XRFlag;
334
+ // TODO: the defaults are wrong? should be Desktop | ThirdPerson ?
335
+ flag.visibleIn = XRStateFlag.ThirdPerson | XRStateFlag.VR;
336
+ this.flags.push(flag);
337
+ if (debug)
338
+ console.log("Added flag to head: " + flag.visibleIn, this.head.name);
339
+ }
340
+ }
341
+
342
+ if (debug)
343
+ console.log("[Avatar], is Local? ", this.isLocalAvatar, this.root);
344
+ this.updateFlags();
345
+
346
+ this.updateVisibility();
347
+
348
+ return true;
349
+ }
350
+ else {
351
+ if (debug)
352
+ console.warn("build avatar failed");
353
+ return false;
354
+ }
355
+ }
356
+ }
src/engine-components/webxr/WebXRButtons.ts DELETED
@@ -1,291 +0,0 @@
1
- import { isDevEnvironment } from "../../engine/debug/index.js";
2
- import { generateQRCode } from "../../engine/engine_utils.js";
3
- import { isMozillaXR } from "../../engine/engine_utils.js";
4
- import { NeedleXRSession } from "../../engine/engine_xr.js";
5
- import { GameObject } from "../Component.js";
6
- import { USDZExporter } from "../export/usdz/USDZExporter.js";
7
-
8
- const webXRElementName = "needle-webxr-buttons";
9
-
10
- // TODO: add feedback process to the buttons when XR session is requested/pending... perhaps we need to expose an event on the NeedleXRSession class for this
11
-
12
- export class NeedleWebXRHtmlElement extends HTMLElement {
13
-
14
- static create() {
15
- return document.createElement(webXRElementName) as NeedleWebXRHtmlElement;
16
- }
17
-
18
- constructor() {
19
- super();
20
- this.attachShadow({ mode: 'open' });
21
- const template = document.createElement('template');
22
- template.innerHTML = `<style>
23
- :host {
24
- position: absolute;
25
- display: flex;
26
- flex-wrap: wrap;
27
- justify-content: center;
28
- /** increase z-index (nipplejs has 999 as default) */
29
- z-index: 5000;
30
- width: 100%;
31
- bottom: 100px;
32
- left: 50%;
33
- transform: translateX(-50%);
34
- }
35
- :host button {
36
- font-family: Roboto, sans-serif, Arial;
37
- border: none;
38
- color: black;
39
- background: rgba(255, 255, 255, 1);
40
- margin: 5px 5px;
41
- padding: 0.5rem .7rem;
42
- font-size: 1rem;
43
- white-space: nowrap;
44
- transition: all 0.2s ease-in-out;
45
- border-radius: .2rem;
46
- border: rgba(255, 255, 255, 0.2) solid 1px;
47
- box-shadow: 0 0 12px rgba(0, 0, 0, 0.2);
48
- font-weight: normal;
49
- }
50
- :host button:hover {
51
- cursor: pointer;
52
- background: rgba(255, 255, 255, 1);
53
- box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.1), 0 0 15px rgba(255, 255, 255, 0.2);
54
- transition: all 0.1s ease-in-out;
55
- }
56
- :host button:disabled {
57
- background: rgba(255, 255, 255, 1);
58
- color: rgba(100, 100, 100, 1);
59
- border: rgba(0,0,0,0) 1px solid;
60
- box-shadow: none;
61
- cursor: initial;
62
- }
63
- :host button.this-mode-is-requested {
64
- font-weight: bold;
65
- background: repeating-linear-gradient(to right, #fff 0%, #fff 40%, #aaffff 55%, #fff 80%);
66
- background-size: 200% auto;
67
- background-position: 0 100%;
68
- animation: AnimationName .7s ease infinite forwards;
69
- }
70
- :host button.other-mode-is-requested {
71
- }
72
-
73
- @keyframes AnimationName {
74
- 0% { background-position: 0% 0 }
75
- 100% { background-position: -200% 0 }
76
- }
77
-
78
- :host .qr-code-container {
79
- position: absolute;
80
- display: initial;
81
- bottom: 100%;
82
- left: 50%;
83
- transform: translateX(-50%) translateY(-10px);
84
- background-color: white;
85
- padding: 1.2rem;
86
- border-radius: 0.4rem;
87
- pointer-events: all;
88
- opacity: 1;
89
- transition: opacity 0.2s ease-in-out;
90
- box-shadow: 0 0 12px rgba(0, 0, 0, 0.2);
91
- }
92
-
93
- :host .qr-code-container img {
94
- max-width: calc(min(100vw, 300px) - 20px);
95
- }
96
-
97
- :host .qr-code-container.hidden {
98
- opacity: 0;
99
- display: none; /* prevents the QR code from overflowing the body when it's actually disabled but breaks animation */
100
- pointer-events: none;
101
- }
102
- </style>
103
- `;
104
- if (this.shadowRoot)
105
- this.shadowRoot.appendChild(template.content.cloneNode(true));
106
- }
107
-
108
- /** @returns the quicklook button if it was created */
109
- get quicklookButton() { return this.shadowRoot?.querySelector("[data-needle='quicklook-button']") as HTMLButtonElement | null; }
110
- /** get or create the quicklook button
111
- * Behaviour of the button:
112
- * - if the button is clicked a USDZExporter component will be searched for in the scene and if found, it will be used to export the scene to USDZ / Quicklook
113
- */
114
- createQuicklookButton(): HTMLButtonElement {
115
- const existingButton = this.shadowRoot?.querySelector("[data-needle='quicklook-button']") as HTMLButtonElement | null;
116
- if (existingButton) return existingButton;
117
- const button = document.createElement("button");
118
- button.dataset["needle"] = "quicklook-button";
119
- button.innerText = "Open in Quicklook";
120
- button.addEventListener("click", () => {
121
- const usdzExporter = GameObject.findObjectOfType(USDZExporter);
122
- if (usdzExporter) {
123
- usdzExporter.exportAsync();
124
- }
125
- else {
126
- console.warn("No USDZExporter component found in the scene");
127
- }
128
- });
129
- this.shadowRoot?.appendChild(button);
130
- return button;
131
- }
132
-
133
- /** @returns the WebXR AR button if it was created */
134
- get arButton() { return this.shadowRoot?.querySelector("[data-needle='webxr-ar-button']") as HTMLButtonElement | null; }
135
- /** get or create the WebXR AR button
136
- * @param init optional session init options
137
- * Behaviour of the button:
138
- * - if the device supports AR, the button will be visible and clickable
139
- * - if the device does not support AR, the button will be hidden
140
- * - if the device changes and now supports AR, the button will be visible
141
- */
142
- createARButton(init?: XRSessionInit): HTMLButtonElement {
143
- const existingButton = this.shadowRoot?.querySelector("[data-needle='webxr-ar-button']") as HTMLButtonElement | null;
144
- if (existingButton) return existingButton;
145
- const mode: XRSessionMode = "immersive-ar";
146
- const button = document.createElement("button");
147
- button.dataset["needle"] = "webxr-ar-button";
148
- button.innerText = "Enter AR";
149
- button.addEventListener("click", () => NeedleXRSession.start(mode, init));
150
- this.updateSessionSupported(button, mode);
151
- this.listenToXRSessionState(button, mode);
152
- this.shadowRoot?.appendChild(button);
153
-
154
- if (!isMozillaXR()) // WebXR Viewer can't attach events before session start
155
- navigator.xr?.addEventListener("devicechange", () => this.updateSessionSupported(button, mode));
156
-
157
- return button;
158
- }
159
-
160
- /** @returns the WebXR VR button if it was created */
161
- get vrButton() { return this.shadowRoot?.querySelector("[data-needle='webxr-vr-button']") as HTMLButtonElement | null; }
162
- /** get or create the WebXR VR button
163
- * @param init optional session init options
164
- * Behaviour of the button:
165
- * - if the device supports VR, the button will be visible and clickable
166
- * - if the device does not support VR, the button will be hidden
167
- * - if the device changes and now supports VR, the button will be visible
168
- */
169
- createVRButton(init?: XRSessionInit): HTMLButtonElement {
170
- const hasButton = this.shadowRoot?.querySelector("[data-needle='webxr-vr-button']");
171
- if (hasButton) return hasButton as HTMLButtonElement;
172
- const mode: XRSessionMode = "immersive-vr";
173
- const button = document.createElement("button");
174
- button.dataset["needle"] = "webxr-vr-button";
175
- button.innerText = "Enter VR";
176
- button.addEventListener("click", () => NeedleXRSession.start(mode, init));
177
- this.updateSessionSupported(button, mode);
178
- this.listenToXRSessionState(button, mode);
179
- this.shadowRoot?.appendChild(button);
180
-
181
- if (!isMozillaXR()) // WebXR Viewer can't attach events before session start
182
- navigator.xr?.addEventListener("devicechange", () => this.updateSessionSupported(button, mode));
183
-
184
- return button;
185
- }
186
-
187
- /** @returns the Send to Quest button */
188
- get sendToQuestButton() { return this.shadowRoot?.querySelector("[data-needle='webxr-sendtoquest-button']") as HTMLButtonElement | null; }
189
- /** get or create the Send To Quest button
190
- * Behaviour of the button:
191
- * - if the button is clicked, the current URL will be sent to the Oculus Browser on the Quest
192
- */
193
- createSendToQuestButton(): HTMLButtonElement {
194
- const hasButton = this.shadowRoot?.querySelector("[data-needle='webxr-sendtoquest-button']");
195
- if (hasButton) return hasButton as HTMLButtonElement;
196
- const baseUrl = `https://oculus.com/open_url/?url=`
197
- const button = document.createElement("button");
198
- button.dataset["needle"] = "webxr-sendtoquest-button";
199
- button.innerText = "Open on Quest";
200
- button.addEventListener("click", () => {
201
- const urlParameter = encodeURIComponent(window.location.href);
202
- window.open(baseUrl + urlParameter);
203
- });
204
- // make sure to hide the button when we have VR support directly on the device
205
- if (!isMozillaXR()) { // WebXR Viewer can't attach events before session start
206
- navigator.xr?.addEventListener("devicechange", () => {
207
- if (navigator.xr?.isSessionSupported("immersive-vr")) {
208
- button.style.display = "none";
209
- }
210
- else {
211
- button.style.display = "";
212
- }
213
- });
214
- }
215
- this.shadowRoot?.appendChild(button);
216
- return button;
217
- }
218
-
219
- async createQRCode() {
220
- const wrapper = document.createElement("div");
221
- wrapper.style.position = "relative";
222
- wrapper.style.display = "inline-block";
223
-
224
- const qrCodeContainer = document.createElement("div");
225
- qrCodeContainer.classList.add("qr-code-container");
226
- qrCodeContainer.classList.add("hidden");
227
- generateAndInsertQRCode();
228
-
229
- const qrCodeButton = document.createElement("button");
230
- qrCodeButton.innerText = "QR Code";
231
- qrCodeButton.title = "Scan this QR code with your phone to open this page";
232
-
233
- qrCodeButton.addEventListener("click", () => {
234
- qrCodeContainer.classList.toggle("hidden");
235
- if (qrCodeContainer.classList.contains("hidden")) return;
236
- // generate the qr code when the button is clicked - this ensures that we get the QRcode with the latest URL
237
- generateAndInsertQRCode();
238
- });
239
- async function generateAndInsertQRCode() {
240
- const size = 200;
241
- const code = await generateQRCode({
242
- text: window.location.href,
243
- width: size,
244
- height: size,
245
- });
246
- qrCodeContainer.innerHTML = "";
247
- qrCodeContainer.appendChild(code);
248
- }
249
-
250
- wrapper.appendChild(qrCodeButton);
251
- wrapper.appendChild(qrCodeContainer);
252
-
253
- this.shadowRoot?.appendChild(wrapper);
254
- }
255
-
256
- private updateSessionSupported(button: HTMLButtonElement, mode: XRSessionMode) {
257
- if (!navigator.xr) {
258
- button.style.display = "none";
259
- return;
260
- }
261
- navigator.xr.isSessionSupported(mode).then(supported => {
262
- button.style.display = !supported ? "none" : "";
263
- if (isDevEnvironment() && !supported) console.warn(mode + " is not supported on this device - make sure your server runs using HTTPS and you have a device connected that supports " + mode);
264
- });
265
- }
266
-
267
- private listenToXRSessionState(button: HTMLButtonElement, mode: XRSessionMode) {
268
- NeedleXRSession.onSessionRequestStart(args => {
269
- if (args.mode === mode) {
270
- button.classList.add("this-mode-is-requested");
271
- // button["original-text"] = button.innerText;
272
- // let modeText = mode === "immersive-vr" ? "VR" : "AR";
273
- // button.innerText = "Starting " + modeText + "...";
274
- }
275
- else {
276
- button["was-disabled"] = button.disabled;
277
- button.disabled = true;
278
- button.classList.add("other-mode-is-requested");
279
- }
280
- });
281
- NeedleXRSession.onSessionRequestEnd(_ => {
282
- button.classList.remove("this-mode-is-requested");
283
- button.classList.remove("other-mode-is-requested");
284
- button.disabled = button["was-disabled"];
285
- // button.innerText = button["original-text"];
286
- });
287
- }
288
- }
289
-
290
- if (!customElements.get(webXRElementName))
291
- customElements.define(webXRElementName, NeedleWebXRHtmlElement);
src/engine-components/webxr/WebXRImageTracking.ts CHANGED
@@ -1,14 +1,13 @@
1
+ import { WebXR, WebXREvent } from "./WebXR.js";
2
+ import { serializable } from "../../engine/engine_serialization.js";
3
+ import { Behaviour, GameObject } from "../Component.js";
1
4
  import { Object3D, Quaternion, Vector3 } from "three";
5
+ import { CircularBuffer, getParam } from "../../engine/engine_utils.js";
6
+ import { AssetReference } from "../../engine/engine_addressables.js";
7
+ import { showBalloonMessage, showBalloonWarning } from "../../engine/debug/index.js";
2
8
 
3
- import { showBalloonMessage, showBalloonWarning } from "../../engine/debug/index.js";
4
- import { AssetReference } from "../../engine/engine_addressables.js";
5
- import { serializable } from "../../engine/engine_serialization.js";
6
- import { CircularBuffer, getParam } from "../../engine/engine_utils.js";
7
- import { NeedleXREventArgs, NeedleXRSession } from "../../engine/xr/index.js";
8
- import { imageToCanvas,USDWriter, USDZExporterContext } from "../../engine-components/export/usdz/ThreeUSDZExporter.js";
9
9
  import { USDZExporter } from "../../engine-components/export/usdz/USDZExporter.js";
10
- import { Behaviour, GameObject } from "../Component.js";
11
- import { InstancingUtil, Renderer } from "../Renderer.js";
10
+ import { USDZExporterContext, USDWriter, imageToCanvas } from "../../engine-components/export/usdz/ThreeUSDZExporter.js";
12
11
 
13
12
  // https://github.com/immersive-web/marker-tracking/blob/main/explainer.md
14
13
 
@@ -45,13 +44,11 @@
45
44
  if (t01 === undefined || t01 >= 1 || haveChanged) {
46
45
  object.position.copy(this._position);
47
46
  object.quaternion.copy(this._rotation);
48
- // InstancingUtil.markDirty(object);
49
47
  }
50
48
  else {
51
49
  t01 = Math.max(0, Math.min(1, t01));
52
50
  object.position.lerp(this._position, t01);
53
51
  object.quaternion.slerp(this._rotation, t01);
54
- // InstancingUtil.markDirty(object);
55
52
  }
56
53
  object.quaternion.multiply(WebXRTrackedImage.y180);
57
54
  }
@@ -64,10 +61,15 @@
64
61
  if (!this._position) {
65
62
  this._position = WebXRTrackedImage._positionBuffer.get();
66
63
  this._rotation = WebXRTrackedImage._rotationBuffer.get();
67
- const t = this._pose.transform as XRRigidTransform;
68
- const converted = NeedleXRSession.active!.convertSpace(t);
69
- this._position.copy(converted?.position);
70
- this._rotation.copy(converted?.quaternion);
64
+ const t = this._pose.transform;
65
+
66
+ // when parented to the world, we need to flip data here
67
+ //this._position.set(-t.position.x, t.position.y, -t.position.z);
68
+ // this._rotation.set(-t.orientation.x, t.orientation.y, -t.orientation.z, t.orientation.w);
69
+
70
+ // for some reason when parented to the XRRig, we need the original data
71
+ this._position.set(t.position.x, t.position.y, t.position.z);
72
+ this._rotation.set(t.orientation.x, t.orientation.y, t.orientation.z, t.orientation.w);
71
73
  }
72
74
  }
73
75
 
@@ -139,7 +141,9 @@
139
141
  trackedImages?: WebXRImageTrackingModel[];
140
142
 
141
143
  private readonly trackedImageIndexMap: Map<number, WebXRImageTrackingModel> = new Map();
144
+
142
145
  private static _imageElements: Map<string, ImageBitmap | null> = new Map();
146
+ private webxr: WebXR | null = null;
143
147
 
144
148
  awake(): void {
145
149
  if (debug) console.log(this)
@@ -178,35 +182,51 @@
178
182
  }
179
183
  }
180
184
 
181
- onBeforeXR(_mode: XRSessionMode, args: XRSessionInit & { trackedImages: Array<any> }): void {
182
- // console.log("onXRRequested", args, this.trackedImages)
183
- if (this.trackedImages) {
184
- args.optionalFeatures = args.optionalFeatures || [];
185
- if (!args.optionalFeatures.includes("image-tracking"))
186
- args.optionalFeatures.push("image-tracking");
187
185
 
188
- args.trackedImages = [];
189
- for (const trackedImage of this.trackedImages) {
190
- if (trackedImage.image?.length && trackedImage.widthInMeters > 0) {
191
- const bitmap = WebXRImageTracking._imageElements.get(trackedImage.image);
192
- if (bitmap) {
193
- this.trackedImageIndexMap.set(args.trackedImages.length, trackedImage);
194
- args.trackedImages.push({
195
- image: bitmap,
196
- widthInMeters: trackedImage.widthInMeters
197
- });
198
- }
186
+ onEnable(): void {
187
+ this.webxr = GameObject.findObjectOfType(WebXR);
188
+ WebXR.addEventListener(WebXREvent.ModifyAROptions, this.onModifyAROptions);
189
+ WebXR.addEventListener(WebXREvent.XRStarted, this.onXRStarted);
190
+ WebXR.addEventListener(WebXREvent.XRUpdate, this.onXRUpdate);
191
+ this.addEventListener("image-tracking", this.onImageTrackingUpdate);
192
+ }
193
+
194
+ onDisable(): void {
195
+ WebXR.removeEventListener(WebXREvent.ModifyAROptions, this.onModifyAROptions);
196
+ WebXR.removeEventListener(WebXREvent.XRStarted, this.onXRStarted);
197
+ WebXR.removeEventListener(WebXREvent.XRUpdate, this.onXRUpdate);
198
+ this.removeEventListener("image-tracking", this.onImageTrackingUpdate);
199
+ }
200
+
201
+ private onModifyAROptions = (event: any) => {
202
+ if (!this.trackedImages) return;
203
+ const options = event.detail;
204
+ const features = options.optionalFeatures || [];
205
+ if (!features.includes("image-tracking"))
206
+ features.push("image-tracking");
207
+ options.optionalFeatures = features;
208
+
209
+ options.trackedImages = [];
210
+ for (const trackedImage of this.trackedImages) {
211
+ if (trackedImage.image?.length && trackedImage.widthInMeters > 0) {
212
+ const bitmap = WebXRImageTracking._imageElements.get(trackedImage.image);
213
+ if (bitmap) {
214
+ this.trackedImageIndexMap.set(options.trackedImages.length, trackedImage);
215
+ options.trackedImages.push({
216
+ image: bitmap,
217
+ widthInMeters: trackedImage.widthInMeters
218
+ });
199
219
  }
200
220
  }
201
221
  }
202
222
  }
203
223
 
204
- onEnterXR(_args: NeedleXREventArgs): void {
224
+ private onXRStarted = (_: any) => {
205
225
  if (this.trackedImages) {
206
226
  for (const trackedImage of this.trackedImages) {
207
227
  if (trackedImage.object?.asset) {
208
228
  const obj = trackedImage.object.asset;
209
- // obj.visible = false;
229
+ obj.visible = false;
210
230
  }
211
231
  }
212
232
  }
@@ -216,16 +236,17 @@
216
236
  }
217
237
  };
218
238
 
219
- private readonly imageToObjectMap = new Map<WebXRImageTrackingModel, { object: GameObject | null, frames: number, lastTrackingTime: number }>();
239
+ private readonly imageToObjectMap = new Map<WebXRImageTrackingModel, { object: GameObject | null, frames: number, lastTrackingTime:number }>();
220
240
  private readonly currentImages: WebXRTrackedImage[] = [];
221
241
 
222
- onUpdateXR(args: NeedleXREventArgs): void {
242
+
243
+ private onXRUpdate = (evt): void => {
223
244
  this.currentImages.length = 0;
224
245
 
225
- const frame = args.xr.frame;
246
+ const frame = evt.frame;
226
247
  if (!frame) return;
227
248
 
228
- if (!("getImageTrackingResults" in frame)) {
249
+ if (frame.session && !("getImageTrackingResults" in frame)) {
229
250
  const warning = "Image tracking is currently not supported on this device. On Chrome for Android, you can enable the <a target=\"_blank\" href=\"#\" onclick=\"() => console.log('I')\">chrome://flags/#webxr-incubations</a> flag.";
230
251
  if (!this["didPrintWarning"]) {
231
252
  this["didPrintWarning"] = true;
@@ -234,7 +255,8 @@
234
255
  showBalloonWarning(warning);
235
256
  return;
236
257
  }
237
- else if (frame.session && typeof frame.getImageTrackingResults === "function") {
258
+
259
+ if (frame.session && typeof frame.getImageTrackingResults === "function") {
238
260
  const results = frame.getImageTrackingResults();
239
261
  if (results.length > 0) {
240
262
  const space = this.context.renderer.xr.getReferenceSpace();
@@ -257,7 +279,9 @@
257
279
  if (this.currentImages.length > 0) {
258
280
  try {
259
281
  this.dispatchEvent(new CustomEvent("image-tracking", { detail: this.currentImages }));
260
- this.onImageTrackingUpdate(this.currentImages);
282
+ if (this.webxr && this.webxr.allowARPlacementReticle) {
283
+ this.webxr.allowARPlacementReticle = false;
284
+ }
261
285
  }
262
286
  catch (e) {
263
287
  console.error(e);
@@ -290,11 +314,9 @@
290
314
  }
291
315
 
292
316
 
293
- private onImageTrackingUpdate = (images: WebXRTrackedImage[]) => {
294
- const xr = NeedleXRSession.active;
295
- if (!xr) return;
317
+ private onImageTrackingUpdate = (event: any) => {
318
+ const images = event.detail as WebXRTrackedImage[];
296
319
 
297
-
298
320
  for (const image of images) {
299
321
  const model = image.model;
300
322
  const isTracked = image.state === "tracked";
@@ -314,31 +336,20 @@
314
336
  if (asset) {
315
337
  trackedData!.object = asset;
316
338
 
317
- // workaround for instancing currently not properly updating
318
- // instanced objects become visible when the image is recognized for the second time
319
- // we need to look into this further https://linear.app/needle/issue/NE-3936
320
- for (const rend of asset.getComponentsInChildren(Renderer)) {
321
- rend.setInstancingEnabled(false);
322
- }
323
-
324
339
  // make sure to parent to the WebXR.rig
325
- if (xr.rig) {
326
- xr.rig.gameObject.add(asset);
327
- image.applyToObject(asset);
328
- if (!asset.activeSelf)
329
- GameObject.setActive(asset, true);
330
- // InstancingUtil.markDirty(asset);
340
+ if (this.webxr) {
341
+ this.webxr.Rig.add(asset);
331
342
  }
332
- else {
333
- console.warn("XRImageTracking: missing XRRig");
334
- }
335
343
 
344
+ image.applyToObject(asset);
345
+ if (!asset.activeSelf)
346
+ GameObject.setActive(asset, true);
336
347
  }
337
348
  });
338
349
  }
339
350
  else {
340
351
  trackedData.frames++;
341
- if (isTracked)
352
+ if(isTracked)
342
353
  trackedData.lastTrackingTime = Date.now();
343
354
 
344
355
  // TODO we could do a bit more here: e.g. sample for the first 1s or so of getting pose data
@@ -348,16 +359,13 @@
348
359
 
349
360
  if (!trackedData.object) continue;
350
361
 
351
- if (xr.rig) {
362
+ if (this.webxr) {
363
+ this.webxr.Rig.add(trackedData.object);
364
+ }
352
365
 
353
- xr.rig.gameObject.add(trackedData.object);
354
-
355
- image.applyToObject(trackedData.object);
356
- if (!trackedData.object.activeSelf) {
357
- GameObject.setActive(trackedData.object, true);
358
- }
359
- // InstancingUtil.markDirty(trackedData.object);
360
- }
366
+ image.applyToObject(trackedData.object);
367
+ if (!trackedData.object.activeSelf)
368
+ GameObject.setActive(trackedData.object, true);
361
369
  }
362
370
  }
363
371
  }
src/engine-components/webxr/WebXRPlaneTracking.ts CHANGED
@@ -1,14 +1,13 @@
1
- import { Box3, BufferAttribute, BufferGeometry, Group, Material, Matrix4, Mesh, MeshBasicMaterial, MeshNormalMaterial, Object3D, Vector3 } from "three";
1
+ import { Box3, BufferAttribute, BufferGeometry, Group, Material, Mesh, Object3D, Vector3 } from "three";
2
2
 
3
- import { AssetReference } from "../../engine/engine_addressables.js";
4
- import { disposeObjectResources } from "../../engine/engine_assetdatabase.js";
5
- import { destroy } from "../../engine/engine_gameobject.js";
3
+ import { MeshCollider } from "../Collider.js";
4
+ import { Behaviour, GameObject } from "../Component.js";
5
+ import { WebXR, WebXREvent } from "./WebXR.js";
6
6
  import { serializable } from "../../engine/engine_serialization.js";
7
7
  import type { Vec3 } from "../../engine/engine_types.js";
8
+ import { disposeObjectResources } from "../../engine/engine_assetdatabase.js";
8
9
  import { getParam } from "../../engine/engine_utils.js";
9
- import { NeedleXREventArgs, NeedleXRSession } from "../../engine/engine_xr.js";
10
- import { MeshCollider } from "../Collider.js";
11
- import { Behaviour, GameObject } from "../Component.js";
10
+ import { destroy } from "../../engine/engine_gameobject.js";
12
11
  // import { SimplifyModifier } from 'three/examples/jsm/modifiers/SimplifyModifier.js';
13
12
 
14
13
  const debug = getParam("debugplanetracking");
@@ -42,8 +41,8 @@
42
41
  export class WebXRPlaneTracking extends Behaviour {
43
42
 
44
43
  /** Optional: if assigned it will be instantiated per tracked plane/tracked mesh */
45
- @serializable(AssetReference)
46
- dataTemplate?: AssetReference;
44
+ @serializable(Object3D)
45
+ dataTemplate?: Object3D;
47
46
 
48
47
  @serializable()
49
48
  initiateRoomCaptureIfNoData = true;
@@ -54,25 +53,34 @@
54
53
  @serializable()
55
54
  useMeshData: boolean = true;
56
55
 
57
- /** when enabled mesh or plane tracking will also be used in VR */
58
- @serializable()
59
- runInVR = true;
60
-
61
56
  get trackedPlanes() { return this._allPlanes.values(); }
62
57
  get trackedMeshes() { return this._allMeshes.values(); }
63
58
 
59
+ onEnable(): void {
60
+ WebXR.addEventListener(WebXREvent.XRStarted, this.onXRStarted);
61
+ WebXR.addEventListener(WebXREvent.XRUpdate, this.onXRUpdate);
62
+ WebXR.addEventListener(WebXREvent.ModifyAROptions, this.onModifyAROptions);
63
+ }
64
64
 
65
+ onDisable(): void {
66
+ WebXR.removeEventListener(WebXREvent.XRStarted, this.onXRStarted);
67
+ WebXR.removeEventListener(WebXREvent.XRUpdate, this.onXRUpdate);
68
+ WebXR.removeEventListener(WebXREvent.ModifyAROptions, this.onModifyAROptions);
69
+ }
65
70
 
66
- onBeforeXR(_mode: XRSessionMode, args: XRSessionInit): void {
67
- if (_mode === "immersive-vr" && !this.runInVR) return;
68
- args.optionalFeatures = args.optionalFeatures || [];
69
- if (this.usePlaneData && !args.optionalFeatures.includes("plane-detection"))
70
- args.optionalFeatures.push("plane-detection");
71
- if (this.useMeshData && !args.optionalFeatures.includes("mesh-detection"))
72
- args.optionalFeatures.push("mesh-detection");
71
+ private onModifyAROptions = (event: any) => {
72
+ const options = event.detail;
73
+ const features = options.optionalFeatures || [];
74
+
75
+ if (this.usePlaneData && !features.includes("plane-detection"))
76
+ features.push("plane-detection");
77
+ if (this.useMeshData && !features.includes("mesh-detection"))
78
+ features.push("mesh-detection");
79
+
80
+ options.optionalFeatures = features;
73
81
  }
74
82
 
75
- onEnterXR(_evt) {
83
+ private onXRStarted = (_evt) => {
76
84
  // remove all previously added data from the scene again
77
85
  for (const data of this._allPlanes.keys()) {
78
86
  this.removeData(data, this._allPlanes);
@@ -82,24 +90,18 @@
82
90
  }
83
91
  }
84
92
 
85
- onUpdateXR(args: NeedleXREventArgs): void {
86
-
87
- if (!this.runInVR && args.xr.isVR) return;
88
-
93
+ private onXRUpdate = (evt) => {
94
+
89
95
  // parenting tracked planes to the XR rig ensures that they synced with the real-world user data;
90
96
  // otherwise they would "swim away" when the user rotates / moves / teleports and so on.
91
97
  // There may be cases where we want that! E.g. a user walks around on their own table in castle builder
92
- const rig = args.xr.rig;
93
- if (!rig) {
94
- console.warn("No XR rig found, cannot parent tracked planes to it");
95
- return;
96
- }
98
+ if (!evt.rig) return;
97
99
 
98
- const frame = args.xr.frame as XRFramePlanes;
100
+ const frame = evt.frame as XRFramePlanes;
99
101
  const renderer = this.context.renderer;
100
102
  const referenceSpace = renderer.xr.getReferenceSpace();
101
103
  if (!referenceSpace) return;
102
-
104
+
103
105
  const planes = frame.detectedPlanes;
104
106
  const meshes = frame.detectedMeshes;
105
107
  const hasAnyPlanes = planes !== undefined && planes.size > 0;
@@ -124,10 +126,10 @@
124
126
  }
125
127
 
126
128
  if (planes !== undefined)
127
- this.processFrameData(args.xr, rig.gameObject, frame, planes, this._allPlanes);
129
+ this.processFrameData(evt.rig, evt.frame, planes, this._allPlanes);
128
130
 
129
131
  if (meshes !== undefined)
130
- this.processFrameData(args.xr, rig.gameObject, frame, meshes, this._allMeshes);
132
+ this.processFrameData(evt.rig, evt.frame, meshes, this._allMeshes);
131
133
  }
132
134
 
133
135
  private removeData(data: XRPlane | XRMesh, _all: Map<XRPlane | XRMesh, XRPlaneContext>) {
@@ -154,11 +156,11 @@
154
156
  private readonly _allMeshes = new Map<XRMesh, XRPlaneContext>();
155
157
  private firstTimeNoPlanesDetected = -100;
156
158
 
157
- private processFrameData(_xr: NeedleXRSession, rig: Object3D, frame: XRFramePlanes, detected: Set<XRPlane | XRMesh>, _all: Map<XRPlane | XRMesh, XRPlaneContext>) {
159
+ private processFrameData(rig: Object3D, frame: XRFramePlanes, detected: Set<XRPlane | XRMesh>, _all: Map<XRPlane | XRMesh, XRPlaneContext>) {
158
160
  const renderer = this.context.renderer;
159
161
  const referenceSpace = renderer.xr.getReferenceSpace();
160
162
  if (!referenceSpace) return;
161
-
163
+
162
164
  for (const data of _all.keys()) {
163
165
  if (!detected.has(data)) {
164
166
  this.removeData(data, _all);
@@ -168,7 +170,7 @@
168
170
  for (const data of detected) {
169
171
  const space = "planeSpace" in data ? data.planeSpace
170
172
  : ("meshSpace" in data ? data.meshSpace
171
- : undefined);
173
+ : undefined);
172
174
  if (!space) continue;
173
175
  const planePose = frame.getPose(space, referenceSpace);
174
176
 
@@ -241,18 +243,12 @@
241
243
 
242
244
  // if we don't have any template assigned we just use a simple mesh object
243
245
  if (!this.dataTemplate) {
244
- const mesh = new Mesh();
245
- if (debug) mesh.material = new MeshNormalMaterial();
246
- else mesh.material = new MeshBasicMaterial({ wireframe: true, opacity: .33, transparent: true, color: 0x000000 });
247
- this.dataTemplate = new AssetReference("", "", mesh);
246
+ this.dataTemplate = new Mesh();
248
247
  }
249
248
 
250
- if (!this.dataTemplate.asset) {
251
- this.dataTemplate.loadAssetAsync();
252
- }
253
- else {
249
+ if (this.dataTemplate) {
254
250
  // Create instance
255
- const newPlane = GameObject.instantiate(this.dataTemplate.asset) as GameObject;
251
+ const newPlane = GameObject.instantiate(this.dataTemplate) as GameObject;
256
252
  planeMesh = newPlane;
257
253
 
258
254
  if (newPlane instanceof Mesh) {
@@ -269,7 +265,7 @@
269
265
  }
270
266
  }
271
267
  }
272
-
268
+
273
269
  const mc = newPlane.getComponent(MeshCollider) as MeshCollider;
274
270
  if (mc) {
275
271
  const mesh = newPlane as unknown as Mesh;
@@ -316,7 +312,6 @@
316
312
  if (planePose) {
317
313
  planeMesh.visible = true;
318
314
  planeMesh.matrix.fromArray(planePose.transform.matrix);
319
- planeMesh.matrix.premultiply(this._flipForwardMatrix);
320
315
  } else {
321
316
  planeMesh.visible = false;
322
317
  }
@@ -324,11 +319,9 @@
324
319
  };
325
320
  }
326
321
 
327
- private _flipForwardMatrix = new Matrix4().makeRotationY(Math.PI);
328
-
329
322
  // heuristic to determine if a collider should be convex or not -
330
323
  // the "global mesh" should be non-convex, other meshes should be
331
- private checkIfContextShouldBeConvex(mesh: Mesh | Group | undefined, xrData: XRPlane | XRMesh) {
324
+ checkIfContextShouldBeConvex(mesh: Mesh | Group | undefined, xrData: XRPlane | XRMesh) {
332
325
  if (!mesh) return true;
333
326
  if (mesh) {
334
327
  // get bounding box of the mesh
@@ -353,7 +346,7 @@
353
346
  return true;
354
347
  }
355
348
 
356
- private createGeometry(data: XRPlane | XRMesh) {
349
+ createGeometry(data: XRPlane | XRMesh) {
357
350
  if ("polygon" in data) {
358
351
  return this.createPlaneGeometry(data.polygon);
359
352
  }
@@ -366,7 +359,7 @@
366
359
  // we cache vertices-to-geometry, because it looks like when we get an update sometimes the geometry stays the same.
367
360
  // so we don't want to re-create the geometry every time.
368
361
  private _verticesCache = new Map<string, BufferGeometry>();
369
- private createMeshGeometry(vertices: Float32Array, indices: Uint32Array) {
362
+ createMeshGeometry(vertices: Float32Array, indices: Uint32Array) {
370
363
  const key = vertices.toString() + "_" + indices.toString();
371
364
  if (this._verticesCache.has(key)) {
372
365
  return this._verticesCache.get(key)!;
@@ -376,7 +369,7 @@
376
369
  geometry.setAttribute('position', new BufferAttribute(vertices, 3));
377
370
  // set UVs in worldspace
378
371
  const uvs = Array<number>();
379
- for (let i = 0; i < vertices.length; i += 3) {
372
+ for (let i = 0; i < vertices.length; i+=3) {
380
373
  uvs.push(vertices[i], vertices[i + 2]);
381
374
  }
382
375
  geometry.setAttribute('uv', new BufferAttribute(new Float32Array(uvs), 2));
@@ -394,9 +387,9 @@
394
387
 
395
388
  this._verticesCache.set(key, geometry);
396
389
  return geometry;
397
- }
390
+ }
398
391
 
399
- private createPlaneGeometry(polygon: Vec3[]) {
392
+ createPlaneGeometry(polygon: Vec3[]) {
400
393
  const geometry = new BufferGeometry();
401
394
 
402
395
  const vertices: number[] = [];
src/engine-components/webxr/WebXRRig.ts CHANGED
@@ -1,59 +1,22 @@
1
- import { AxesHelper, Euler, Object3D, Quaternion, Vector3 } from "three";
2
-
3
- import { serializable } from "../../engine/engine_serialization_decorator.js";
1
+ import { Object3D } from "three";
4
2
  import type { IGameObject } from "../../engine/engine_types.js";
5
3
  import { getParam } from "../../engine/engine_utils.js";
6
- import { NeedleXREventArgs, NeedleXRSession } from "../../engine/engine_xr.js";
7
- import { IXRRig } from "../../engine/engine_xr.js";
8
4
  import { Behaviour } from "../Component.js";
9
5
  import { BoxGizmo } from "../Gizmos.js";
10
6
 
11
- const debug = getParam("debugwebxr");
7
+ const debug = getParam("debugrig");
12
8
 
13
- export class XRRig extends Behaviour implements IXRRig {
14
-
15
- @serializable()
16
- priority: number = 0;
17
-
18
- get isActive() { return this.activeAndEnabled && this.gameObject.visible; }
19
-
20
- /** Sets this rig to be the active XR rig (needs to be called during an active XR session) */
21
- setAsActiveXRRig() {
22
- NeedleXRSession.active?.setRigActive(this);
23
- }
24
-
9
+ export class XRRig extends Behaviour {
25
10
  awake(): void {
11
+ // const helper = new AxesHelper(.1);
12
+ // this.gameObject.add(helper);
26
13
  if (debug) {
27
14
  const gizmoObj = new Object3D() as IGameObject;
28
15
  gizmoObj.position.y += .5;
29
16
  this.gameObject.add(gizmoObj);
30
- const box = gizmoObj.addNewComponent(BoxGizmo);
31
- if (box)
32
- box.isGizmo = false;
33
- const axes = new AxesHelper(.5);
34
- this.gameObject.add(axes)
17
+ const gizmo = gizmoObj.addNewComponent(BoxGizmo);
18
+ if (gizmo)
19
+ gizmo.isGizmo = false;
35
20
  }
36
21
  }
37
-
38
- isXRRig(): boolean {
39
- return true;
40
- }
41
-
42
- supportsXR(_mode: XRSessionMode): boolean {
43
- return true;
44
- }
45
-
46
- private _startScale?: Vector3;
47
-
48
- onEnterXR(args: NeedleXREventArgs): void {
49
- this._startScale = this.gameObject.scale.clone();
50
- args.xr.addRig(this);
51
- if(debug) console.log("WebXR: add Rig", this.name, this.priority)
52
- }
53
- onLeaveXR(args: NeedleXREventArgs): void {
54
- args.xr.removeRig(this);
55
- if (this._startScale && this.gameObject)
56
- this.gameObject.scale.copy(this._startScale);
57
- }
58
-
59
22
  }
src/engine-components/webxr/controllers/XRControllerFollow.ts DELETED
@@ -1,67 +0,0 @@
1
-
2
- import { serializable } from "../../../engine/engine_serialization_decorator.js";
3
- import { NeedleXREventArgs } from "../../../engine/engine_xr.js";
4
- import { Behaviour } from "../../Component.js";
5
-
6
-
7
- /** Add this script to an object and set `side` to make the object follow a specific controller */
8
- export class XRControllerFollow extends Behaviour {
9
-
10
- // override active and enabled here so that we always receive xr update events
11
- get activeAndEnabled() {
12
- return true;
13
- }
14
-
15
- /** should this object follow a right hand/controller or left hand/controller */
16
- @serializable()
17
- side: XRHandedness = "none";
18
-
19
- /** should it follow controllers (the physics controller) */
20
- @serializable()
21
- controller: boolean = true;
22
-
23
- /** should it follow hands (when using hand tracking in WebXR) */
24
- hands: boolean = false;
25
-
26
- /** Disable if you don't want this script to modify the object's visibility
27
- * If enabled the object will be hidden when the configured controller or hand is not available
28
- * If disabled this script will not modify the object's visibility
29
- */
30
- controlVisibility: boolean = true;
31
-
32
- /** when true it will use the grip space, otherwise the ray space */
33
- useGripSpace = false;
34
-
35
- onUpdateXR(args: NeedleXREventArgs): void {
36
-
37
- // try to get the controller
38
- const ctrl = args.xr.getController(this.side);
39
- if (ctrl) {
40
- // check if this is a hand and hands are allowed
41
- if (ctrl.hand && !this.hands) {
42
- if (this.controlVisibility)
43
- this.gameObject.visible = false;
44
- return;
45
- }
46
- // check if this is a controller and controllers are allowed
47
- else if (!this.controller) {
48
- if (this.controlVisibility)
49
- this.gameObject.visible = false;
50
- return;
51
- }
52
- // we're following a controller (or hand)
53
- if (this.controlVisibility)
54
- this.gameObject.visible = true;
55
- if (this.useGripSpace) {
56
- this.gameObject.worldPosition = ctrl.gripWorldPosition;
57
- this.gameObject.worldQuaternion = ctrl.gripWorldQuaternion;
58
- }
59
- else {
60
- this.gameObject.worldPosition = ctrl.rayWorldPosition;
61
- this.gameObject.worldQuaternion = ctrl.rayWorldQuaternion;
62
- }
63
- }
64
-
65
- }
66
-
67
- }
src/engine-components/webxr/controllers/XRControllerModel.ts DELETED
@@ -1,253 +0,0 @@
1
- import { AxesHelper, Group, Material, Mesh, Object3D } from "three";
2
- import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
3
- import { XRControllerModelFactory } from "three/examples/jsm/webxr/XRControllerModelFactory.js";
4
- import { XRHandMeshModel } from "three/examples/jsm/webxr/XRHandMeshModel.js";
5
-
6
- import { showBalloonWarning } from "../../../engine/debug/index.js";
7
- import { AssetReference } from "../../../engine/engine_addressables.js";
8
- import { addDracoAndKTX2Loaders } from "../../../engine/engine_loaders.js";
9
- import { serializable } from "../../../engine/engine_serialization_decorator.js";
10
- import { IGameObject } from "../../../engine/engine_types.js";
11
- import { getParam } from "../../../engine/engine_utils.js";
12
- import { NeedleXRController, NeedleXRControllerEventArgs, NeedleXREventArgs, NeedleXRSession } from "../../../engine/engine_xr.js";
13
- import { Behaviour, GameObject } from "../../Component.js"
14
-
15
- const debug = getParam("debugwebxr");
16
-
17
- export class XRControllerModel extends Behaviour {
18
-
19
- @serializable()
20
- createControllerModel: boolean = true;
21
-
22
- @serializable()
23
- createHandModel: boolean = true;
24
-
25
- /** assign a model or model url to create custom hand models */
26
- @serializable(AssetReference)
27
- customLeftHand?: AssetReference;
28
- /** assign a model or model url to create custom hand models */
29
- @serializable(AssetReference)
30
- customRightHand?: AssetReference;
31
-
32
-
33
- static readonly factory: XRControllerModelFactory = new XRControllerModelFactory();
34
-
35
- supportsXR(mode: XRSessionMode): boolean {
36
- return mode === "immersive-vr" || mode === "immersive-ar";
37
- }
38
-
39
- private _models = new Array<{ model?: IGameObject, controller: NeedleXRController, handmesh?: XRHandMeshModel }>();
40
-
41
-
42
- async onXRControllerAdded(args: NeedleXRControllerEventArgs) {
43
-
44
- // TODO we may want to treat controllers differently in AR/Passthrough mode
45
- const isSupportedSession = args.xr.isVR || args.xr.isPassThrough;
46
- if (!isSupportedSession) return;
47
-
48
- const { controller } = args;
49
-
50
- if (debug) console.warn("Add Controller Model for", controller.side, controller.index)
51
-
52
- if (this.createControllerModel) {
53
- if (controller.hand) {
54
- if (this.createHandModel) {
55
- const res = await this.loadHandModel(controller);
56
- if (!res || !controller.connected) return;
57
- this._models[controller.index] = { controller: controller, model: res.handObject, handmesh: res.handmesh };
58
- this.scene.add(res.handObject);
59
- }
60
- }
61
- else {
62
- if (this.createControllerModel) {
63
- const assetUrl = await controller.getModelUrl();
64
- if (assetUrl) {
65
- const model = await this.loadModel(controller, assetUrl);
66
- if (!model || !controller.connected) return;
67
- this._models[controller.index] = { controller: controller, model };
68
- this.scene.add(model);
69
- // The controller mesh should by default inherit layers.
70
- model.traverse(child => {
71
- child.layers.disableAll();
72
- child.layers.enable(2);
73
- });
74
- }
75
- else {
76
- console.warn("XRControllerModel: no model found for " + controller.side);
77
- }
78
- }
79
- }
80
- }
81
- }
82
- onXRControllerRemoved(args: NeedleXRControllerEventArgs): void {
83
- // we need to find the index by the controller because if controller 0 is removed first then args.controller.index 1 will be at index 0
84
- const indexInArray = this._models.findIndex(m => m?.controller === args.controller);
85
- const entry = this._models[indexInArray];
86
- if (!entry) return;
87
- this._models.splice(indexInArray, 1);
88
-
89
- if (entry.handmesh) {
90
- entry.handmesh.handModel?.removeFromParent();
91
- }
92
- if (entry.model) {
93
- entry.model.removeFromParent();
94
- }
95
- }
96
- onBeforeRender() {
97
- if (!NeedleXRSession.active) return;
98
-
99
- const xr = NeedleXRSession.active;
100
-
101
- for (let i = 0; i < this._models.length; i++) {
102
- const entry = this._models[i];
103
- if (!entry) continue;
104
- const ctrl = entry.controller;
105
- if (!ctrl.connected) {
106
- // the actual removal of the model happens in onXRControllerRemoved
107
- if (debug) console.warn("XRControllerModel.onUpdateXR: controller is not connected anymore", ctrl.side, ctrl.hand);
108
- continue;
109
- }
110
-
111
- // do we have a controller model?
112
- if (entry.model && !entry.handmesh) {
113
- // TODO: if the model would just be in the XR rig, we could just set grip position and we would not need to override the scale
114
- // entry.model.position.copy(ctrl.gripWorldPosition);
115
- entry.model.position.copy(ctrl.gripPosition);
116
- // entry.model.quaternion.copy(ctrl.gripWorldQuaternion);
117
- entry.model.quaternion.copy(ctrl.gripQuaternion);
118
- entry.model.visible = ctrl.isTracking;
119
- // ensure that controller models are in rig space
120
- xr.rig?.gameObject.add(entry.model);
121
- }
122
- // do we have a hand mesh?
123
- else if (ctrl.inputSource.hand && entry.handmesh) {
124
- const referenceSpace = xr.referenceSpace;
125
- const hand = this.context.renderer.xr.getHand(ctrl.index);
126
- if (referenceSpace && xr.frame.getJointPose) {
127
- for (const inputjoint of ctrl.inputSource.hand.values()) {
128
- // Update the joints groups with the XRJoint poses
129
- const jointPose = xr.frame.getJointPose(inputjoint, referenceSpace);
130
- // The transform of this joint will be updated with the joint pose on each frame
131
- const joint = hand.joints[inputjoint.jointName];
132
- if (joint) {
133
- if (jointPose) {
134
- const { position, quaternion } = xr.convertSpace(jointPose.transform);
135
- joint.position.copy(position);
136
- joint.quaternion.copy(quaternion);
137
- joint.matrixWorldNeedsUpdate = true;
138
- // joint.jointRadius = jointPose.radius;
139
- }
140
- joint.visible = jointPose != null;
141
- }
142
- }
143
- // ensure that the hand renders in rig space
144
- if (entry.model) {
145
- entry.model.visible = ctrl.isTracking;
146
- if (entry.model.parent !== xr.rig?.gameObject) {
147
- entry.model.position.set(0, 0, 0);
148
- xr.rig?.gameObject.add(entry.model);
149
- }
150
- }
151
-
152
- entry.handmesh?.updateMesh();
153
- }
154
- }
155
- }
156
- }
157
- onLeaveXR(_args: NeedleXREventArgs): void {
158
- for (const entry of this._models) {
159
- if (!entry) continue;
160
- entry.model?.removeFromParent();
161
- }
162
- this._models = [];
163
- }
164
-
165
- protected async loadModel(controller: NeedleXRController, url: string): Promise<IGameObject | null> {
166
- if (!controller.connected) {
167
- console.warn("XRControllerModel.onXRControllerAdded: controller is not connected anymore", controller.side);
168
- return null;
169
- }
170
- const assetReference = AssetReference.getOrCreate("", url);
171
- const model = await assetReference.instantiate() as GameObject;
172
-
173
- if (NeedleXRSession.active?.isPassThrough) {
174
- model.traverseVisible((obj: Object3D) => {
175
- this.makeOccluder(obj);
176
- })
177
- }
178
- return model as IGameObject;
179
- }
180
-
181
- protected async loadHandModel(controller: NeedleXRController): Promise<{ handObject: IGameObject, handmesh: XRHandMeshModel } | null> {
182
-
183
- const context = this.context;
184
- const hand = context.renderer.xr.getHand(controller.index);
185
-
186
- const loader = new GLTFLoader();
187
- addDracoAndKTX2Loaders(loader, context);
188
- loader.setPath('https://cdn.jsdelivr.net/npm/@webxr-input-profiles/[email protected]/dist/profiles/generic-hand/');
189
-
190
- // TODO: we should handle the loading here ourselves to not have this requirement of a specific model name
191
- const expectedHandModelName = controller.side === "left" ? "left." : "right.";
192
- const customHand = controller.side === "left" ? this.customLeftHand : this.customRightHand;
193
- if (customHand) {
194
- if (!customHand.uri.includes(expectedHandModelName)) {
195
- console.warn("XRControllerModel: custom hand model must be named " + expectedHandModelName);
196
- showBalloonWarning("Custom Hand: unexpected name, please see the console for details");
197
- }
198
- else {
199
- const basePath = customHand.uri.substring(0, customHand.uri.indexOf(expectedHandModelName));
200
- loader.setPath(basePath);
201
- if(debug) console.log("XRControllerModel: loading custom hand model from " + basePath);
202
- }
203
- }
204
-
205
-
206
- const handObject = new Object3D();
207
- // @ts-ignore
208
- const handmesh = new XRHandMeshModel(handObject, hand, loader.path, controller.inputSource.handedness, loader, (object: Object3D) => {
209
- // The hand mesh should not receive raycasts
210
- object.traverseVisible(child => {
211
- child.layers.disableAll();
212
- child.layers.enable(2);
213
- if (NeedleXRSession.active?.isPassThrough)
214
- this.makeOccluder(child);
215
- });
216
- });
217
-
218
- if (debug) handObject.add(new AxesHelper(.5));
219
-
220
- if (controller.inputSource.hand) {
221
- if (debug) console.log(controller.inputSource.hand);
222
- for (const inputjoint of controller.inputSource.hand.values()) {
223
-
224
- if (hand.joints[inputjoint.jointName] === undefined) {
225
-
226
- const joint = new Group();
227
- joint.matrixAutoUpdate = false;
228
- joint.visible = true;
229
- // joint.jointRadius = 0.01;
230
- // @ts-ignore
231
- hand.joints[inputjoint.jointName] = joint;
232
- hand.add(joint);
233
-
234
- }
235
- }
236
- }
237
- return { handObject: handObject as IGameObject, handmesh: handmesh };
238
- }
239
-
240
- private makeOccluder(obj: Object3D) {
241
- if (obj instanceof Mesh) {
242
- let mat = obj.material;
243
- if (mat instanceof Material) {
244
- mat = obj.material = mat.clone();
245
- // depth only
246
- mat.depthWrite = true;
247
- mat.depthTest = true;
248
- mat.colorWrite = false;
249
- obj.renderOrder = -100;
250
- }
251
- }
252
- }
253
- }
src/engine-components/webxr/controllers/XRControllerMovement.ts DELETED
@@ -1,328 +0,0 @@
1
- import { AdditiveBlending, BufferAttribute, Color, DoubleSide, Line, Line3, Mesh, MeshBasicMaterial, Object3D, RingGeometry, SubtractiveBlending, Vector3 } from "three";
2
- import { Line2 } from "three/examples/jsm/lines/Line2.js";
3
- import { LineGeometry } from "three/examples/jsm/lines/LineGeometry.js";
4
- import { LineMaterial } from "three/examples/jsm/lines/LineMaterial.js";
5
-
6
- import { Gizmos } from "../../../engine/engine_gizmos.js";
7
- import { Mathf } from "../../../engine/engine_math.js";
8
- import { RaycastTestObjectCallback } from "../../../engine/engine_physics.js";
9
- import { serializable } from "../../../engine/engine_serialization.js"
10
- import { getTempVector, getWorldQuaternion, getWorldScale } from "../../../engine/engine_three_utils.js";
11
- import { IGameObject } from "../../../engine/engine_types.js";
12
- import { getParam } from "../../../engine/engine_utils.js";
13
- import { NeedleXRController, NeedleXREventArgs, NeedleXRSession } from "../../../engine/engine_xr.js";
14
- import { Behaviour, GameObject } from "../../Component.js"
15
- import { TeleportTarget } from "../TeleportTarget.js";
16
- import { XRMovementBehaviour } from "../types.js";
17
-
18
- const debug = getParam("debugwebxr");
19
-
20
- export class XRControllerMovement extends Behaviour implements XRMovementBehaviour {
21
-
22
- /** Movement speed in meters per second */
23
- @serializable()
24
- movementSpeed = 1;
25
-
26
- /** How many degrees to rotate the XR rig when using the rotation trigger */
27
- @serializable()
28
- rotationStep = 60;
29
-
30
- /** When enabled you can teleport using the right XR controller's thumbstick by pressing forward */
31
- @serializable()
32
- useTeleport: boolean = true;
33
-
34
- /** Enable to only allow teleporting on objects with a teleport target component */
35
- @serializable()
36
- useTeleportTarget = false;
37
-
38
- /** Enable to fade out the scene when teleporting */
39
- @serializable()
40
- useTeleportFade = false;
41
-
42
- /** enable to visualize controller rays in the 3D scene */
43
- @serializable()
44
- showRays: boolean = true;
45
-
46
- /** enable to visualize pointer targets in the 3D scene */
47
- @serializable()
48
- showHits: boolean = true;
49
-
50
- readonly isXRMovementHandler: true = true;
51
-
52
- readonly xrSessionMode = "immersive-vr";
53
-
54
- private _didApplyRotation = false;
55
- private _didTeleport = false;
56
-
57
- onUpdateXR(args: NeedleXREventArgs): void {
58
- const rig = args.xr.rig;
59
- if (!rig?.gameObject) return;
60
-
61
- // in AR pass through mode we dont want to move the rig
62
- if (args.xr.isPassThrough) {
63
- if (this.showRays)
64
- this.renderRays(args.xr);
65
- if (this.showHits)
66
- this.renderHits(args.xr);
67
- return;
68
- }
69
-
70
- const movementController = args.xr.leftController;
71
- const teleportController = args.xr.rightController;
72
-
73
- if (movementController)
74
- this.onHandleMovement(movementController, rig.gameObject);
75
- if (teleportController) {
76
- this.onHandleRotation(teleportController, rig.gameObject);
77
- if (this.useTeleport)
78
- this.onHandleTeleport(teleportController, rig.gameObject);
79
- }
80
-
81
- if (this.showRays)
82
- this.renderRays(args.xr);
83
- if (this.showHits)
84
- this.renderHits(args.xr);
85
- }
86
- onLeaveXR(_: NeedleXREventArgs): void {
87
- for (const line of this._lines) {
88
- line.removeFromParent();
89
- }
90
- for (const disc of this._hitDiscs) {
91
- disc?.removeFromParent();
92
- }
93
- }
94
-
95
- protected onHandleMovement(controller: NeedleXRController, rig: IGameObject) {
96
- const stick = controller.getStick("xr-standard-thumbstick");
97
- const vec = new Vector3(stick.x, 0, stick.y);
98
- vec.multiplyScalar(this.context.time.deltaTime * this.movementSpeed);
99
- const scale = getWorldScale(rig);
100
- vec.multiplyScalar(scale.x);
101
- vec.applyQuaternion(controller.xr.poseOrientation);
102
- vec.y = 0;
103
- vec.applyQuaternion(rig.worldQuaternion);
104
- rig.position.add(vec);
105
-
106
- // TODO: if we dont do this here the XRControllerModel will be frame-delayed - maybe we need to introduce a priority order for XR components?
107
- rig.updateMatrixWorld();
108
- }
109
-
110
-
111
- protected onHandleRotation(controller: NeedleXRController, rig: IGameObject) {
112
- const stick = controller.getStick("xr-standard-thumbstick");
113
- const rotationInput = stick.x;
114
- if (this._didApplyRotation) {
115
- if (Math.abs(rotationInput) < .3) {
116
- this._didApplyRotation = false;
117
- }
118
- }
119
- else if (Math.abs(rotationInput) > .5) {
120
- this._didApplyRotation = true;
121
- const dir = rotationInput > 0 ? 1 : -1;
122
- rig.rotateY(dir * Mathf.toRadians(this.rotationStep));
123
- }
124
-
125
- const pos = controller.rayWorldPosition;
126
- pos.y += .1
127
- if (debug) Gizmos.DrawLabel(pos, stick.x.toFixed(2) + ", " + stick.y.toFixed(2), .02, 0)
128
- }
129
-
130
- protected onHandleTeleport(controller: NeedleXRController, rig: IGameObject) {
131
- const teleportInput = controller.getStick("xr-standard-thumbstick")
132
- if (this._didTeleport) {
133
- if (teleportInput.y < .2) {
134
- this._didTeleport = false;
135
- }
136
- }
137
- else if (teleportInput.y > .8) {
138
- this._didTeleport = true;
139
- const hit = this.context.physics.raycastFromRay(controller.ray)[0];
140
- if (hit) {
141
- if (this.useTeleportTarget) {
142
- const teleportTarget = GameObject.getComponentInParent(hit.object, TeleportTarget);
143
- if (!teleportTarget) return;
144
- }
145
- if (debug) Gizmos.DrawSphere(hit.point, .025, 0xff0000, 5);
146
- const point = hit.point.clone();
147
- if (this.useTeleportFade) {
148
- controller.xr.fadeTransition()?.then(() => {
149
- rig.worldPosition = point;
150
- })
151
- }
152
- else {
153
- rig.worldPosition = point;
154
- }
155
- }
156
- else {
157
- // TODO: add option to allow teleportation on current ground plane
158
- }
159
- }
160
- }
161
-
162
- private readonly _lines: Object3D[] = [];
163
- private readonly _hitDiscs: Object3D[] = [];
164
- private readonly _hitDistances: number[] = [];
165
-
166
- protected renderRays(session: NeedleXRSession) {
167
-
168
- if (session.controllers.length < this._lines.length) {
169
- for (let i = session.controllers.length; i < this._lines.length; i++) {
170
- const line = this._lines[i];
171
- line.visible = false;
172
- }
173
- }
174
-
175
- for (const disc of this._hitDiscs) {
176
- if (disc) disc.visible = false;
177
- }
178
-
179
- for (let i = 0; i < session.controllers.length; i++) {
180
- const ctrl = session.controllers[i];
181
- let line = this._lines[i];
182
- if (!line) {
183
- line = this.createRayLineObject();
184
- line.scale.z = .5;
185
- this._lines[i] = line;
186
- }
187
-
188
- const pos = ctrl.rayWorldPosition;
189
- const rot = ctrl.rayWorldQuaternion;
190
- line.position.copy(pos);
191
- line.quaternion.copy(rot);
192
- const scale = session.rigScale;
193
- const dist = this._hitDistances[i] ?? 1;
194
- line.scale.set(scale, scale, scale * dist);
195
- line.visible = true;
196
- line.layers.disableAll();
197
- line.layers.enable(2);
198
- if (line.parent !== this.context.scene)
199
- this.context.scene.add(line);
200
- }
201
- }
202
-
203
- protected renderHits(session: NeedleXRSession) {
204
- for (const disc of this._hitDiscs) {
205
- if (disc) disc.visible = false;
206
- }
207
- for (let i = 0; i < session.controllers.length; i++) {
208
- const ctrl = session.controllers[i];
209
- const hit = this.context.physics.raycastFromRay(ctrl.ray, { testObject: this.hitPointRaycastFilter })[0];
210
- this._hitDistances[i] = hit?.distance;
211
- if (hit) {
212
- const rigScale = (session.rigScale ?? 1);
213
- if (debug) {
214
- Gizmos.DrawWireSphere(hit.point, .025 * rigScale, 0xff0000, .2);
215
- Gizmos.DrawLabel(getTempVector(0, .2, 0).add(hit.point), hit.object.name, .02, 0);
216
- }
217
-
218
- let disc = this._hitDiscs[i];
219
- if (!disc) {
220
- disc = this.createHitPointObject();
221
- this._hitDiscs[i] = disc;
222
- }
223
- disc.visible = true;
224
- const size = (.01 * (1 + hit.distance));
225
- disc.scale.set(size, size, size);
226
- disc.layers.disableAll();
227
- disc.layers.enable(2);
228
-
229
- if (hit.normal) {
230
- const factor = 0.02 * rigScale;
231
- disc.position.set(0, 0, -.1 * factor).applyQuaternion(ctrl.rayWorldQuaternion);
232
- disc.position.add(hit.point);
233
- const worldNormal = hit.normal.applyQuaternion(getWorldQuaternion(hit.object));
234
- disc.quaternion.setFromUnitVectors(up, worldNormal);
235
- }
236
- else {
237
- disc.position.add(hit.point);
238
- }
239
-
240
- if (disc.parent !== this.context.scene) {
241
- this.context.scene.add(disc);
242
- }
243
- }
244
- else {
245
- if (this._hitDiscs[i]) {
246
- this._hitDiscs[i].visible = false;
247
- }
248
- }
249
- }
250
- }
251
-
252
- protected hitPointRaycastFilter: RaycastTestObjectCallback = (obj: Object3D) => {
253
- // by default dont raycast ont skinned meshes using the hit point raycast (because it is a big performance hit and only a visual indicator)
254
- if (obj.type === "SkinnedMesh") return "continue in children";
255
- return true;
256
- }
257
-
258
- /** create an object to visualize hit points in the scene */
259
- protected createHitPointObject(): Object3D {
260
- var container = new Object3D();
261
- const disc = new Mesh(
262
- new RingGeometry(.3, 0.5, 32).rotateX(- Math.PI / 2),
263
- new MeshBasicMaterial({
264
- color: 0xeeeeee,
265
- opacity: .7,
266
- transparent: true,
267
- side: DoubleSide,
268
- })
269
- );
270
- disc.layers.disableAll();
271
- disc.layers.enable(2);
272
- container.add(disc);
273
-
274
- const disc2 = new Mesh(
275
- new RingGeometry(.43, 0.5, 32).rotateX(- Math.PI / 2),
276
- new MeshBasicMaterial({
277
- color: 0x000000,
278
- opacity: .2,
279
- transparent: true,
280
- side: DoubleSide,
281
- })
282
- );
283
- disc2.layers.disableAll();
284
- disc2.layers.enable(2);
285
- disc2.position.z -= .01;
286
- container.add(disc2);
287
- return container;
288
- }
289
-
290
- /** create an object to visualize controller rays */
291
- protected createRayLineObject() {
292
- const line = new Line2();
293
- line.layers.disableAll();
294
- line.layers.enable(2);
295
-
296
- const geometry = new LineGeometry();
297
- line.geometry = geometry;
298
-
299
- const positions = new Float32Array(9);
300
- positions.set([0, 0, .02, 0, 0, .4, 0, 0, 1]);
301
- geometry.setPositions(positions)
302
-
303
- const colors = new Float32Array(9);
304
- colors.set([1, 1, 1, .1, .1, .1, 0, 0, 0]);
305
- geometry.setColors(colors);
306
-
307
- const mat = new LineMaterial({
308
- color: 0xffffff,
309
- vertexColors: true,
310
- worldUnits: true,
311
- linewidth: .004,
312
-
313
- transparent: true,
314
- // TODO: this doesnt work with passthrough
315
- blending: AdditiveBlending,
316
- dashed: false,
317
- alphaToCoverage: true,
318
-
319
- });
320
- line.material = mat;
321
-
322
- return line;
323
- }
324
- }
325
-
326
-
327
- const up = new Vector3(0, 1, 0);
328
-
src/engine-components/webxr/XRFlag.ts DELETED
@@ -1,143 +0,0 @@
1
- import { serializable } from "../../engine/engine_serialization_decorator.js";
2
- import { getParam } from "../../engine/engine_utils.js";
3
- import { Behaviour, GameObject } from "../Component.js";
4
-
5
-
6
- const debug = getParam("debugxrflags");
7
- const disable = getParam("disablexrflags");
8
- if (disable) { console.warn("XRFlags are disabled") }
9
-
10
- export enum XRStateFlag {
11
- Never = 0,
12
- Browser = 1 << 0,
13
- AR = 1 << 1,
14
- VR = 1 << 2,
15
- FirstPerson = 1 << 3,
16
- ThirdPerson = 1 << 4,
17
- All = 0xffffffff
18
- }
19
-
20
- export class XRState {
21
-
22
- public static Global: XRState = new XRState();
23
-
24
- public Mask: XRStateFlag = XRStateFlag.Browser | XRStateFlag.ThirdPerson;
25
-
26
- public Has(state: XRStateFlag) {
27
- const res = (this.Mask & state);
28
- return res !== 0;
29
- }
30
-
31
- public Set(state: number) {
32
- if (debug) console.warn("Set XR flag state to", state)
33
- this.Mask = state as number;
34
- XRFlag.Apply();
35
- }
36
-
37
- public Enable(state: number) {
38
- this.Mask |= state;
39
- XRFlag.Apply();
40
- }
41
-
42
- public Disable(state: number) {
43
- this.Mask &= ~state;
44
- XRFlag.Apply();
45
- }
46
-
47
- public Toggle(state: number) {
48
- this.Mask ^= state;
49
- XRFlag.Apply();
50
- }
51
-
52
- public EnableAll() {
53
- this.Mask = 0xffffffff | 0;
54
- XRFlag.Apply();
55
- }
56
-
57
- public DisableAll() {
58
- this.Mask = 0;
59
- XRFlag.Apply();
60
- }
61
- }
62
-
63
- export class XRFlag extends Behaviour {
64
-
65
- private static registry: XRFlag[] = [];
66
-
67
- public static Apply() {
68
- for (const r of this.registry) r.UpdateVisible(XRState.Global);
69
- }
70
-
71
- private static firstApply: boolean;
72
- private static buffer: XRState = new XRState();
73
-
74
- @serializable()
75
- public visibleIn!: number;
76
-
77
- awake() {
78
- XRFlag.registry.push(this);
79
- }
80
-
81
- onEnable(): void {
82
- if (!XRFlag.firstApply) {
83
- XRFlag.firstApply = true;
84
- XRFlag.Apply();
85
- }
86
- else {
87
- this.UpdateVisible(XRState.Global);
88
- }
89
- }
90
-
91
- onDestroy(): void {
92
- const i = XRFlag.registry.indexOf(this);
93
- if (i >= 0)
94
- XRFlag.registry.splice(i, 1);
95
- }
96
-
97
- public get isOn(): boolean { return this.gameObject.visible; }
98
-
99
- public UpdateVisible(state: XRState | XRStateFlag | null = null) {
100
- if (disable) {
101
- return;
102
- }
103
- // XR flags set visibility of whole hierarchy which is like setting the whole object inactive
104
- // so we need to ignore the enabled state of the XRFlag component
105
- // if(!this.enabled) return;
106
- let res: boolean | undefined = undefined;
107
-
108
- const flag = state as number;
109
- if (flag && typeof flag === "number") {
110
- console.assert(typeof flag === "number", "XRFlag.UpdateVisible: state must be a number", flag);
111
- if (debug)
112
- console.log(flag);
113
- XRFlag.buffer.Mask = flag;
114
- state = XRFlag.buffer;
115
- }
116
-
117
- if (state instanceof XRState) {
118
- if (debug)
119
- console.warn(this.name, "use passed in mask", state.Mask, this.visibleIn)
120
- res = state.Has(this.visibleIn);
121
- }
122
- else {
123
- if (debug)
124
- console.log(this.name, "use global mask")
125
- XRState.Global.Has(this.visibleIn);
126
- }
127
- if (res === undefined) return;
128
- if (res) {
129
- if (debug)
130
- console.log(this.name, "is visible", this.gameObject.uuid)
131
- // this.gameObject.visible = true;
132
- GameObject.setActive(this.gameObject, true);
133
- } else {
134
- if (debug)
135
- console.log(this.name, "is not visible", this.gameObject.uuid);
136
- const isVisible = this.gameObject.visible;
137
- if (!isVisible) return;
138
- this.gameObject.visible = false;
139
- // console.trace("DISABLE", this.name);
140
- // GameObject.setActive(this.gameObject, false);
141
- }
142
- }
143
- }
src/engine/xr/XRRig.ts DELETED
@@ -1,9 +0,0 @@
1
- import { IComponent } from "../engine_types.js";
2
-
3
-
4
- export interface IXRRig extends Pick<IComponent, "gameObject"> {
5
- isXRRig(): boolean;
6
- get isActive(): boolean;
7
- /** The rig with the highest priority will be chosen */
8
- priority?: number;
9
- }
src/engine-components/webxr/WebXRController.ts ADDED
@@ -0,0 +1,1168 @@
1
+ import { BoxHelper, BufferGeometry, Color, Euler, Group, type Intersection, Layers, Line, LineBasicMaterial, Material, Mesh, MeshBasicMaterial, Object3D, PerspectiveCamera, Quaternion, Ray, SphereGeometry, Vector2, Vector3 } from "three";
2
+ import { OculusHandModel } from 'three/examples/jsm/webxr/OculusHandModel.js';
3
+ import { OculusHandPointerModel } from 'three/examples/jsm/webxr/OculusHandPointerModel.js';
4
+ import { XRControllerModel, XRControllerModelFactory } from 'three/examples/jsm/webxr/XRControllerModelFactory.js';
5
+ import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
6
+
7
+ import { InstancingUtil } from "../../engine/engine_instancing.js";
8
+ import { Mathf } from "../../engine/engine_math.js";
9
+ import { RaycastOptions } from "../../engine/engine_physics.js";
10
+ import { getWorldPosition, getWorldQuaternion, setWorldPosition, setWorldQuaternion } from "../../engine/engine_three_utils.js";
11
+ import { getParam, resolveUrl } from "../../engine/engine_utils.js";
12
+ import { addDracoAndKTX2Loaders } from "../../engine/engine_loaders.js";
13
+
14
+ import { Avatar_POI } from "../avatar/Avatar_Brain_LookAt.js";
15
+ import { Behaviour, GameObject } from "../Component.js";
16
+ import { Interactable, UsageMarker } from "../Interactable.js";
17
+ import { Rigidbody } from "../RigidBody.js";
18
+ import { SyncedTransform } from "../SyncedTransform.js";
19
+ import { UIRaycastUtils } from "../ui/RaycastUtils.js";
20
+ import { WebXR } from "./WebXR.js";
21
+ import { XRRig } from "./WebXRRig.js";
22
+ import { InputEvents, NEPointerEvent, PointerType } from "../../engine/engine_input.js";
23
+
24
+ const debug = getParam("debugwebxrcontroller");
25
+
26
+ export enum ControllerType {
27
+ PhysicalDevice = 0,
28
+ Touch = 1,
29
+ }
30
+
31
+ export enum ControllerEvents {
32
+ SelectStart = "select-start",
33
+ SelectEnd = "select-end",
34
+ Update = "update",
35
+ }
36
+
37
+ export class TeleportTarget extends Behaviour {
38
+
39
+ }
40
+
41
+ export class WebXRController extends Behaviour {
42
+
43
+ public static Factory: XRControllerModelFactory = new XRControllerModelFactory();
44
+
45
+ private static raycastColor: Color = new Color(.9, .3, .3);
46
+ private static raycastNoHitColor: Color = new Color(.6, .6, .6);
47
+ private static geometry = new BufferGeometry().setFromPoints([new Vector3(0, 0, 0), new Vector3(0, 0, -1)]);
48
+ private static handModels: { [index: number]: OculusHandPointerModel } = {};
49
+
50
+ private static CreateRaycastLine(): Line {
51
+ const line = new Line(this.geometry);
52
+ const mat = line.material as LineBasicMaterial;
53
+ mat.color = this.raycastColor;
54
+ // mat.linewidth = 10;
55
+ line.layers.set(2);
56
+ line.name = 'line';
57
+ line.scale.z = 1;
58
+ return line;
59
+ }
60
+
61
+ private static CreateRaycastHitPoint(): Mesh {
62
+ const geometry = new SphereGeometry(.5, 22, 22);
63
+ const material = new MeshBasicMaterial({ color: this.raycastColor });
64
+ const sphere = new Mesh(geometry, material);
65
+ sphere.visible = false;
66
+ sphere.layers.set(2);
67
+ return sphere;
68
+ }
69
+
70
+ public static Create(owner: WebXR, index: number, addTo: GameObject, type: ControllerType): WebXRController {
71
+ const ctrl = addTo ? GameObject.addNewComponent(addTo, WebXRController, false) : new WebXRController();
72
+
73
+ ctrl.webXR = owner;
74
+ ctrl.index = index;
75
+ ctrl.type = type;
76
+
77
+ const context = owner.context;
78
+ // from https://github.com/mrdoob/js/blob/master/examples/webxr_vr_dragging.html
79
+ // controllers
80
+ ctrl.controller = context.renderer.xr.getController(index);
81
+ ctrl.controllerGrip = context.renderer.xr.getControllerGrip(index);
82
+ ctrl.controllerModel = this.Factory.createControllerModel(ctrl.controller);
83
+ ctrl.controllerGrip.add(ctrl.controllerModel);
84
+
85
+ ctrl.hand = context.renderer.xr.getHand(index);
86
+
87
+ const loader = new GLTFLoader();
88
+ addDracoAndKTX2Loaders(loader, context);
89
+ if (ctrl.webXR.handModelPath && ctrl.webXR.handModelPath !== "")
90
+ loader.setPath(resolveUrl(owner.sourceId, ctrl.webXR.handModelPath));
91
+ else
92
+ // from XRHandMeshModel.js
93
+ loader.setPath('https://cdn.jsdelivr.net/npm/@webxr-input-profiles/[email protected]/dist/profiles/generic-hand/');
94
+ //@ts-ignore
95
+ const hand = new OculusHandModel(ctrl.hand, loader);
96
+
97
+ ctrl.hand.add(hand);
98
+ ctrl.hand.traverse(x => x.layers.set(2));
99
+
100
+ ctrl.handPointerModel = new OculusHandPointerModel(ctrl.hand, ctrl.controller);
101
+
102
+
103
+ // TODO remove all these once https://github.com/mrdoob/js/pull/23279 lands
104
+ ctrl.controller.addEventListener('connected', (_) => {
105
+ ctrl.setControllerLayers(ctrl.controllerModel, 2);
106
+ ctrl.setControllerLayers(ctrl.controllerGrip, 2);
107
+ ctrl.setControllerLayers(ctrl.hand, 2);
108
+ setTimeout(() => {
109
+ ctrl.setControllerLayers(ctrl.controllerModel, 2);
110
+ ctrl.setControllerLayers(ctrl.controllerGrip, 2);
111
+ ctrl.setControllerLayers(ctrl.hand, 2);
112
+ }, 1000);
113
+ });
114
+
115
+ // TODO: unsubscribe! this should be moved into onenable and ondisable!
116
+ // TODO remove all these once https://github.com/mrdoob/js/pull/23279 lands
117
+ ctrl.hand.addEventListener('connected', (event) => {
118
+ const xrInputSource = event.data;
119
+ if (xrInputSource.hand) {
120
+ if (owner.Rig) owner.Rig.add(ctrl.hand);
121
+ ctrl.type = ControllerType.PhysicalDevice;
122
+ ctrl.handPointerModel.traverse(x => x.layers.set(2)); // ignore raycast
123
+ ctrl.handPointerModel.pointerObject?.traverse(x => x.layers.set(2));
124
+
125
+ // when exiting and re-entering xr the joints are not parented to the hand anymore
126
+ // this is a workaround to fix that temporarely
127
+ // see https://github.com/needle-tools/needle-tiny-playground/issues/123
128
+ const jnts = ctrl.hand["joints"];
129
+ if (jnts) {
130
+ for (const key of Object.keys(jnts)) {
131
+ const joint = jnts[key];
132
+ if (joint.parent) continue;
133
+ ctrl.hand.add(joint);
134
+ }
135
+ }
136
+ }
137
+ });
138
+
139
+ return ctrl;
140
+ }
141
+
142
+ // TODO: replace with component events
143
+ public static addEventListener(evt: ControllerEvents, callback: (controller: WebXRController, args: any) => void) {
144
+ const list = this.eventSubs[evt] ?? [];
145
+ list.push(callback);
146
+ this.eventSubs[evt] = list;
147
+ }
148
+
149
+ // TODO: replace with component events
150
+ public static removeEventListener(evt: ControllerEvents, callback: (controller: WebXRController, args: any) => void) {
151
+ if (!callback) return;
152
+ const list = this.eventSubs[evt] ?? [];
153
+ const idx = list.indexOf(callback);
154
+ if (idx >= 0) list.splice(idx, 1);
155
+ this.eventSubs[evt] = list;
156
+ }
157
+
158
+ private static eventSubs: { [key: string]: Function[] } = {};
159
+
160
+ public webXR?: WebXR;
161
+ public index: number = -1;
162
+ public controllerModel!: XRControllerModel;
163
+ public controller!: Group;
164
+ public controllerGrip!: Group;
165
+ public hand!: Group;
166
+ public handPointerModel!: OculusHandPointerModel;
167
+ public grabbed: AttachedObject | null = null;
168
+ public input: XRInputSource | null = null;
169
+ public type: ControllerType = ControllerType.PhysicalDevice;
170
+ public showRaycastLine: boolean = true;
171
+ public enableRaycasts: boolean = true;
172
+ public enableDefaultControls: boolean = true;
173
+
174
+ get isUsingHands(): boolean {
175
+ const r = this.input?.hand;
176
+ return r !== null && r !== undefined;
177
+ }
178
+
179
+ get wrist(): Object3D | null {
180
+ if (!this.hand) return null;
181
+ const jnts = this.hand["joints"];
182
+ if (!jnts) return null;
183
+ return jnts["wrist"];
184
+ }
185
+
186
+ private _wristQuaternion: Quaternion | null = null;
187
+ getWristQuaternion(): Quaternion | null {
188
+ const wrist = this.wrist;
189
+ if (!wrist) return null;
190
+ if (!this._wristQuaternion) this._wristQuaternion = new Quaternion();
191
+ const wr = getWorldQuaternion(wrist).multiply(this._wristQuaternion.setFromEuler(new Euler(-Math.PI / 4, 0, 0)));
192
+ return wr;
193
+ }
194
+
195
+ private movementVector: Vector3 = new Vector3();
196
+ private worldRot: Quaternion = new Quaternion();
197
+ private joystick: Vector2 = new Vector2();
198
+ private didRotate: boolean = false;
199
+ private didTeleport: boolean = false;
200
+ private didChangeScale: boolean = false;
201
+ private static PreviousCameraFarDistance: number | undefined = undefined;
202
+ private static MovementSpeedFactor: number = 1;
203
+
204
+ private lastHit: Intersection | null = null;
205
+
206
+ private raycastLine: Line | null = null;
207
+ private _raycastHitPoint: Object3D | null = null;
208
+ private _connnectedCallback: any | null = null;
209
+ private _disconnectedCallback: any | null = null;
210
+ private _selectStartEvt: any | null = null;
211
+ private _selectEndEvt: any | null = null;
212
+
213
+ public get selectionDown(): boolean { return this._selectionPressed && !this._selectionPressedLastFrame; }
214
+ public get selectionUp(): boolean { return !this._selectionPressed && this._selectionPressedLastFrame; }
215
+ public get selectionPressed(): boolean { return this._selectionPressed; }
216
+ public get selectionClick(): boolean { return this._selectionEndTime - this._selectionStartTime < 0.3; }
217
+ public get raycastHitPoint(): Object3D | null { return this._raycastHitPoint; }
218
+
219
+ private _selectionPressed: boolean = false;
220
+ private _selectionPressedLastFrame: boolean = false;
221
+ private _selectionStartTime: number = 0;
222
+ private _selectionEndTime: number = 0;
223
+
224
+ public get useSmoothing(): boolean { return this._useSmoothing };
225
+ private _useSmoothing: boolean = true;
226
+
227
+ awake(): void {
228
+ if (!this.controller) {
229
+ console.warn("WebXRController: Missing controller object.", this);
230
+ return;
231
+ }
232
+ this._connnectedCallback = this.onSourceConnected.bind(this);
233
+ this._disconnectedCallback = this.onSourceDisconnected.bind(this);
234
+ this._selectStartEvt = this.onSelectStart.bind(this);
235
+ this._selectEndEvt = this.onSelectEnd.bind(this);
236
+ if (this.type === ControllerType.Touch) {
237
+ this.controllerGrip.addEventListener("connected", this._connnectedCallback);
238
+ this.controllerGrip.addEventListener("disconnected", this._disconnectedCallback);
239
+ this.controller.addEventListener('selectstart', this._selectStartEvt);
240
+ this.controller.addEventListener('selectend', this._selectEndEvt);
241
+ }
242
+ if (this.type === ControllerType.PhysicalDevice) {
243
+ this.controller.addEventListener('selectstart', this._selectStartEvt);
244
+ this.controller.addEventListener('selectend', this._selectEndEvt);
245
+ }
246
+ }
247
+
248
+ onDestroy(): void {
249
+ if (this.type === ControllerType.Touch) {
250
+ this.controllerGrip.removeEventListener("connected", this._connnectedCallback);
251
+ this.controllerGrip.removeEventListener("disconnected", this._disconnectedCallback);
252
+ this.controller.removeEventListener('selectstart', this._selectStartEvt);
253
+ this.controller.removeEventListener('selectend', this._selectEndEvt);
254
+ }
255
+ if (this.type === ControllerType.PhysicalDevice) {
256
+ this.controller.removeEventListener('selectstart', this._selectStartEvt);
257
+ this.controller.removeEventListener('selectend', this._selectEndEvt);
258
+ }
259
+
260
+ this.hand?.clear();
261
+ this.controllerGrip?.clear();
262
+ this.controller?.clear();
263
+ }
264
+
265
+ public onEnable(): void {
266
+ if (!this.webXR) {
267
+ console.warn("No WebXR component assigned to WebXRController.");
268
+ return;
269
+ }
270
+
271
+ if (this.hand)
272
+ this.hand.name = "Hand";
273
+ if (this.controllerGrip)
274
+ this.controllerGrip.name = "ControllerGrip";
275
+ if (this.controller)
276
+ this.controller.name = "Controller";
277
+ if (this.raycastLine)
278
+ this.raycastLine.name = "RaycastLine;"
279
+
280
+ if (this.webXR.Controllers.indexOf(this) < 0)
281
+ this.webXR.Controllers.push(this);
282
+
283
+ if (!this.raycastLine)
284
+ this.raycastLine = WebXRController.CreateRaycastLine();
285
+ if (!this._raycastHitPoint)
286
+ this._raycastHitPoint = WebXRController.CreateRaycastHitPoint();
287
+
288
+ this.webXR.Rig?.add(this.hand);
289
+ this.webXR.Rig?.add(this.controllerGrip);
290
+ this.webXR.Rig?.add(this.controller);
291
+ this.webXR.Rig?.add(this.raycastLine);
292
+ this.raycastLine?.add(this._raycastHitPoint);
293
+ this._raycastHitPoint.visible = false;
294
+ this.hand.add(this.handPointerModel);
295
+ if (debug)
296
+ console.log("ADDED TO RIG", this.webXR.Rig);
297
+
298
+ // // console.log("enable", this.index, this.controllerGrip.uuid)
299
+ }
300
+
301
+ onDisable(): void {
302
+ // console.log("XR controller disabled", this);
303
+ this.hand?.removeFromParent();
304
+ this.controllerGrip?.removeFromParent();
305
+ this.controller?.removeFromParent();
306
+ this.raycastLine?.removeFromParent();
307
+ this._raycastHitPoint?.removeFromParent();
308
+ // console.log("Disable", this._connnectedCallback, this._disconnectedCallback);
309
+ // this.controllerGrip.removeEventListener("connected", this._connnectedCallback);
310
+ // this.controllerGrip.removeEventListener("disconnected", this._disconnectedCallback);
311
+
312
+ if (this.webXR) {
313
+ const i = this.webXR.Controllers.indexOf(this);
314
+ if (i >= 0)
315
+ this.webXR.Controllers.splice(i, 1);
316
+ }
317
+ }
318
+
319
+ // onDestroy(): void {
320
+ // console.log("destroyed", this.index);
321
+ // }
322
+
323
+ private _isConnected: boolean = false;
324
+
325
+ private onSourceConnected(e: { data: XRInputSource, target: any }) {
326
+ if (this._isConnected) {
327
+ console.warn("Received connected event for controller that is already connected", this.index, e);
328
+ return;
329
+ }
330
+ this._isConnected = true;
331
+ this.input = e.data;
332
+
333
+ if (this.type === ControllerType.Touch) {
334
+ this.onSelectStart();
335
+ }
336
+ }
337
+
338
+ private onSourceDisconnected(_e: any) {
339
+ if (!this._isConnected) {
340
+ console.warn("Received discnnected event for controller that is not connected", _e);
341
+ return;
342
+ }
343
+ this._isConnected = false;
344
+ if (this.type === ControllerType.Touch) {
345
+ this.onSelectEnd();
346
+ }
347
+ this.input = null;
348
+ }
349
+
350
+ private createPointerEvent(type: string) {
351
+ switch (type) {
352
+ case "down":
353
+ this.context.input.createPointerDown(new NEPointerEvent(InputEvents.PointerDown, null, { clientX: 0, clientY: 0, button: this.index, pointerType: PointerType.Touch }));
354
+ break;
355
+ case "move":
356
+ break;
357
+ case "up":
358
+ this.context.input.createPointerUp(new NEPointerEvent(InputEvents.PointerUp, null, { clientX: 0, clientY: 0, button: this.index, pointerType: PointerType.Touch }));
359
+ break;
360
+ }
361
+ }
362
+
363
+ rayRotation: Quaternion = new Quaternion();
364
+
365
+ private raycastUpdate(raycastLine: Line, wp: Vector3) {
366
+ const allowRaycastLineVisible = this.showRaycastLine && this.type !== ControllerType.Touch;
367
+ if (this.type === ControllerType.Touch) {
368
+ raycastLine.visible = false;
369
+ }
370
+ else if (this.isUsingHands) {
371
+ raycastLine.visible = !this.grabbed && allowRaycastLineVisible;
372
+ setWorldPosition(raycastLine, wp);
373
+ const jnts = this.hand!['joints'];
374
+ if (jnts) {
375
+ const wrist = jnts['wrist'];
376
+ if (wrist && this.grabbed && this.grabbed.isCloseGrab) {
377
+ const wr = this.getWristQuaternion();
378
+ if (wr)
379
+ this.rayRotation.copy(wr);
380
+ // this.rayRotation.slerp(wr, this.useSmoothing ? t * 2 : 1);
381
+ }
382
+ }
383
+ setWorldQuaternion(raycastLine, this.rayRotation);
384
+ }
385
+ else {
386
+ raycastLine.visible = allowRaycastLineVisible;
387
+ setWorldQuaternion(raycastLine, this.rayRotation);
388
+ setWorldPosition(raycastLine, wp);
389
+ }
390
+ }
391
+
392
+ update(): void {
393
+ if (!this.webXR) return;
394
+
395
+ // TODO: we should wait until we actually have models, this is just a workaround
396
+ if (this.context.time.frameCount % 60 === 0) {
397
+ this.setControllerLayers(this.controller, 2);
398
+ this.setControllerLayers(this.controllerGrip, 2);
399
+ this.setControllerLayers(this.hand, 2);
400
+ }
401
+
402
+ const subs = WebXRController.eventSubs[ControllerEvents.Update];
403
+ if (subs && subs.length > 0) {
404
+ for (const sub of subs) {
405
+ sub(this);
406
+ }
407
+ }
408
+
409
+ let t = 1;
410
+ if (this.type === ControllerType.PhysicalDevice) t = this.context.time.deltaTime / .1;
411
+ else if (this.isUsingHands && this.handPointerModel.pinched) t = this.context.time.deltaTime / .3;
412
+ this.rayRotation.slerp(getWorldQuaternion(this.controller), this.useSmoothing ? t : 1.0);
413
+ const wp = getWorldPosition(this.controller);
414
+
415
+ // hide hand pointer model, it's giant and doesn't really help
416
+ if (this.isUsingHands && this.handPointerModel.cursorObject) {
417
+ this.handPointerModel.cursorObject.visible = false;
418
+ }
419
+
420
+ // perform raycasts
421
+ if(this.enableRaycasts)
422
+ {
423
+ if (this.raycastLine) {
424
+ this.raycastUpdate(this.raycastLine, wp);
425
+ }
426
+
427
+ this.lastHit = this.updateLastHit();
428
+
429
+ if (this.grabbed) {
430
+ this.grabbed.update();
431
+ }
432
+ }
433
+ else { // hide line when raycasting is disabled
434
+ if (this.raycastLine) {
435
+ this.raycastLine.visible = false;
436
+ }
437
+ }
438
+
439
+ this._selectionPressedLastFrame = this._selectionPressed;
440
+
441
+ if (this.selectStartCallback) {
442
+ this.selectStartCallback();
443
+ }
444
+ }
445
+
446
+ onUpdate(session: XRSession) {
447
+ this.lastHit = null;
448
+
449
+ if (!session || session.inputSources.length <= this.index) {
450
+ this.input = null;
451
+ return;
452
+ }
453
+ if (this.type === ControllerType.PhysicalDevice)
454
+ this.input = session.inputSources[this.index];
455
+ if (!this.input) return;
456
+ const rig = this.webXR!.Rig;
457
+ if (!rig) return;
458
+
459
+ if (this._didNotEndSelection && !this.handPointerModel.pinched) {
460
+ this._didNotEndSelection = false;
461
+ this.onSelectEnd();
462
+ }
463
+
464
+ this.updateStick(this.input);
465
+
466
+ const buttons = this.input?.gamepad?.buttons;
467
+
468
+ if(this.enableDefaultControls) {
469
+ switch (this.input.handedness) {
470
+ case "left":
471
+ this.movementUpdate(rig, buttons);
472
+ break;
473
+
474
+ case "right":
475
+ this.rotationUpdate(rig, buttons);
476
+ break;
477
+ }
478
+ }
479
+ }
480
+
481
+
482
+ private movementUpdate(rig: Object3D, buttons?: readonly GamepadButton[]) {
483
+ const speedFactor = 3 * WebXRController.MovementSpeedFactor;
484
+ const powFactor = 2;
485
+ const speed = Mathf.clamp01(this.joystick.length() * 2);
486
+
487
+ const sideDir = this.joystick.x > 0 ? 1 : -1;
488
+ let side = Math.pow(this.joystick.x, powFactor);
489
+ side *= sideDir;
490
+ side *= speed;
491
+
492
+
493
+ const forwardDir = this.joystick.y > 0 ? 1 : -1;
494
+ let forward = Math.pow(this.joystick.y, powFactor);
495
+ forward *= forwardDir;
496
+ side *= speed;
497
+
498
+ rig.getWorldQuaternion(this.worldRot);
499
+ this.movementVector.set(side, 0, forward);
500
+ this.movementVector.applyQuaternion(this.webXR!.TransformOrientation);
501
+ this.movementVector.y = 0;
502
+ this.movementVector.applyQuaternion(this.worldRot);
503
+ this.movementVector.multiplyScalar(speedFactor * this.context.time.deltaTime);
504
+ rig.position.add(this.movementVector);
505
+
506
+ if (this.isUsingHands)
507
+ this.runTeleport(rig, buttons);
508
+ }
509
+
510
+ private rotationUpdate(rig: Object3D, buttons?: readonly GamepadButton[]) {
511
+ const rotate = this.joystick.x;
512
+ const rotAbs = Math.abs(rotate);
513
+ if (rotAbs < 0.4) {
514
+ this.didRotate = false;
515
+ }
516
+ else if (rotAbs > .5 && !this.didRotate) {
517
+ const dir = rotate > 0 ? -1 : 1;
518
+ rig.rotateY(Mathf.toRadians(30 * dir));
519
+ this.didRotate = true;
520
+ }
521
+
522
+ this.runTeleport(rig, buttons);
523
+ }
524
+ private _pinchStartTime: number | undefined = undefined;
525
+
526
+ private runTeleport(rig: Object3D, buttons?: readonly GamepadButton[]) {
527
+ let teleport = -this.joystick.y;
528
+ if (this.hand?.visible && !this.grabbed) {
529
+ const pinched = this.handPointerModel.isPinched();
530
+ if (pinched && this._pinchStartTime === undefined) {
531
+ this._pinchStartTime = this.context.time.time;
532
+ }
533
+ if (pinched && this._pinchStartTime && this.context.time.time - this._pinchStartTime > .8) {
534
+ // hacky approach for basic hand teleportation -
535
+ // we teleport if we pinch and the back of the hand points down (open hand gesture)
536
+ // const v1 = new Vector3();
537
+ // const worldQuaternion = new Quaternion();
538
+ // this.controller.getWorldQuaternion(worldQuaternion);
539
+ // v1.copy(this.controller.up).applyQuaternion(worldQuaternion);
540
+ // const dotPr = -v1.dot(this.controller.up);
541
+ teleport = this.handPointerModel.isPinched() ? 1 : 0;
542
+ }
543
+ if (!pinched) this._pinchStartTime = undefined;
544
+ }
545
+ else this._pinchStartTime = undefined;
546
+
547
+ const inVR = this.webXR!.IsInVR;
548
+ const xrRig = this.webXR!.Rig;
549
+ let doTeleport = teleport > .5 && inVR;
550
+ let isInMiniatureMode = xrRig ? xrRig?.scale?.x < .999 : false;
551
+ let newRigScale: number | null = null;
552
+
553
+ if (buttons && this.input && !this.input.hand) {
554
+ for (let i = 0; i < buttons.length; i++) {
555
+ const btn = buttons[i];
556
+ // button[4] seems to be the A button if it exists. On hololens it's randomly pressed though for hands
557
+ // see https://www.w3.org/TR/webxr-gamepads-module-1/#xr-standard-gamepad-mapping
558
+ if (i === 4) {
559
+ if (btn.pressed && !this.didChangeScale && inVR) {
560
+ this.didChangeScale = true;
561
+ const rig = xrRig;
562
+ if (rig) {
563
+ const args = this.switchScale(rig, doTeleport, isInMiniatureMode, newRigScale);
564
+ doTeleport = args.doTeleport;
565
+ isInMiniatureMode = args.isInMiniatureMode;
566
+ newRigScale = args.newRigScale;
567
+ }
568
+ }
569
+ else if (!btn.pressed)
570
+ this.didChangeScale = false;
571
+ }
572
+ }
573
+ }
574
+
575
+ if (doTeleport) {
576
+ if (!this.didTeleport) {
577
+ const rc = this.raycast();
578
+ this.didTeleport = true;
579
+ if (rc && rc.length > 0) {
580
+ const hit = rc[0];
581
+ if (isInMiniatureMode || this.isValidTeleportTarget(hit.object)) {
582
+ const point = hit.point;
583
+ setWorldPosition(rig, point);
584
+ }
585
+ }
586
+ }
587
+ }
588
+ else if (teleport < .1) {
589
+ this.didTeleport = false;
590
+ }
591
+
592
+ if (newRigScale !== null) {
593
+ rig.scale.set(newRigScale, newRigScale, newRigScale);
594
+ rig.updateMatrixWorld();
595
+ }
596
+ }
597
+
598
+
599
+ private isValidTeleportTarget(obj: Object3D): boolean {
600
+ return GameObject.getComponentInParent(obj, TeleportTarget) != null;
601
+ }
602
+
603
+ private switchScale(rig: Object3D, doTeleport: boolean, isInMiniatureMode: boolean, newRigScale: number | null) {
604
+ if (!isInMiniatureMode) {
605
+ isInMiniatureMode = true;
606
+ doTeleport = true;
607
+ newRigScale = .1;
608
+ WebXRController.MovementSpeedFactor = newRigScale * 2;
609
+ const cam = this.context.mainCamera as PerspectiveCamera;
610
+ WebXRController.PreviousCameraFarDistance = cam.far;
611
+ cam.far /= newRigScale;
612
+ }
613
+ else {
614
+ isInMiniatureMode = false;
615
+ rig.scale.set(1, 1, 1);
616
+ newRigScale = 1;
617
+ WebXRController.MovementSpeedFactor = 1;
618
+ const cam = this.context.mainCamera as PerspectiveCamera;
619
+ if (WebXRController.PreviousCameraFarDistance)
620
+ cam.far = WebXRController.PreviousCameraFarDistance;
621
+ }
622
+ return { doTeleport, isInMiniatureMode, newRigScale }
623
+ }
624
+
625
+ private updateStick(inputSource: XRInputSource) {
626
+ if (!inputSource || !inputSource.gamepad || inputSource.gamepad.axes?.length < 4) return;
627
+ this.joystick.x = inputSource.gamepad.axes[2];
628
+ this.joystick.y = inputSource.gamepad.axes[3];
629
+ }
630
+
631
+ private updateLastHit(): Intersection | null {
632
+ const rc = this.raycast();
633
+ const hit = rc ? rc[0] : null;
634
+ this.lastHit = hit;
635
+ let factor = 1;
636
+ if (this.webXR!.Rig) {
637
+ factor /= this.webXR!.Rig.scale.x;
638
+ }
639
+ // if (!hit) factor = 0;
640
+
641
+ if (this.raycastLine) {
642
+ this.raycastLine.scale.z = factor * (this.lastHit?.distance ?? 9999);
643
+ const mat = this.raycastLine.material as LineBasicMaterial;
644
+ if (hit != null) mat.color = WebXRController.raycastColor;
645
+ else mat.color = WebXRController.raycastNoHitColor;
646
+ }
647
+ if (this._raycastHitPoint) {
648
+ if (this.lastHit != null) {
649
+ this._raycastHitPoint.position.z = -1;// -this.lastHit.distance;
650
+ const scale = Mathf.clamp(this.lastHit.distance * .01 * factor, .015, .1);
651
+ this._raycastHitPoint.scale.set(scale, scale, scale);
652
+ }
653
+ this._raycastHitPoint.visible = this.lastHit !== null && this.lastHit !== undefined;
654
+ }
655
+ return hit;
656
+ }
657
+
658
+ private onSelectStart() {
659
+ if (!this.context.connection.allowEditing) return;
660
+ // console.log("SELECT START", _event);
661
+ // if we process the event immediately the controller
662
+ // world positions are not yet correctly updated and we have info from the last frame
663
+ // so we delay the event processing one frame
664
+ // only necessary for AR - ideally we can get it to work right here
665
+ // but should be fine as a workaround for now
666
+ this.selectStartCallback = () => this.onHandleSelectStart();
667
+ }
668
+
669
+ private selectStartCallback: Function | null = null;
670
+ private lastSelectStartObject: Object3D | null = null;;
671
+
672
+ private onHandleSelectStart() {
673
+ this.selectStartCallback = null;
674
+ this._selectionPressed = true;
675
+ this._selectionStartTime = this.context.time.time;
676
+ this._selectionEndTime = 1000;
677
+ // console.log("DOWN", this.index, WebXRController.eventSubs);
678
+
679
+ // let maxDistance = this.isUsingHands ? .1 : undefined;
680
+ let intersections: Intersection[] | null = null;
681
+ let closeGrab: boolean = false;
682
+ if (this.isUsingHands) {
683
+ intersections = this.overlap();
684
+ if (intersections.length <= 0) {
685
+ intersections = this.raycast();
686
+ closeGrab = false;
687
+ }
688
+ else {
689
+ closeGrab = true;
690
+ }
691
+ }
692
+ else intersections = this.raycast();
693
+
694
+ if (debug)
695
+ console.log("onHandleSelectStart", "close grab? " + closeGrab, "intersections", intersections);
696
+
697
+ if (intersections && intersections.length > 0) {
698
+ for (const intersection of intersections) {
699
+ const object = intersection.object;
700
+ this.lastSelectStartObject = object;
701
+ const args = { selected: object, grab: object };
702
+ const subs = WebXRController.eventSubs[ControllerEvents.SelectStart];
703
+ if (subs && subs.length > 0) {
704
+ for (const sub of subs) {
705
+ sub(this, args);
706
+ }
707
+ }
708
+ if (args.grab !== object && debug)
709
+ console.log("Grabbed object changed", "original", object, "new", args.grab);
710
+ if (args.grab) {
711
+ this.grabbed = AttachedObject.TryTake(this, args.grab, intersection, closeGrab);
712
+ }
713
+ break;
714
+ }
715
+ }
716
+ else {
717
+ const subs = WebXRController.eventSubs[ControllerEvents.SelectStart];
718
+ const args = { selected: null, grab: null };
719
+ if (subs && subs.length > 0) {
720
+ for (const sub of subs) {
721
+ sub(this, args);
722
+ }
723
+ }
724
+ }
725
+ }
726
+
727
+ private _didNotEndSelection: boolean = false;
728
+
729
+ private onSelectEnd() {
730
+ if (this.isUsingHands) {
731
+ if (this.handPointerModel.pinched) {
732
+ this._didNotEndSelection = true;
733
+ return;
734
+ }
735
+ }
736
+
737
+ if (!this._selectionPressed) return;
738
+ this.selectStartCallback = null;
739
+ this._selectionPressed = false;
740
+ this._selectionEndTime = this.context.time.time;
741
+
742
+ const args = { grab: this.grabbed?.selected ?? this.lastSelectStartObject };
743
+ const subs = WebXRController.eventSubs[ControllerEvents.SelectEnd];
744
+ if (subs && subs.length > 0) {
745
+ for (const sub of subs) {
746
+ sub(this, args);
747
+ }
748
+ }
749
+
750
+ if (this.grabbed) {
751
+ this.grabbed.free();
752
+ this.grabbed = null;
753
+ }
754
+ }
755
+
756
+ private testIsVisible(obj: Object3D | null): boolean {
757
+ if (!obj) return false;
758
+ if (GameObject.isActiveInHierarchy(obj) === false) return false;
759
+ if (UIRaycastUtils.isInteractable(obj) === false) {
760
+ return false;
761
+ }
762
+ return true;
763
+ // if (!obj.visible) return false;
764
+ // return this.testIsVisible(obj.parent);
765
+ }
766
+
767
+ private setControllerLayers(obj: Object3D, layer: number) {
768
+ if (!obj) return;
769
+ obj.layers.set(layer);
770
+ if (obj.children) {
771
+ for (const ch of obj.children) {
772
+ if (this.grabbed?.selected === ch || this.grabbed?.selectedMesh === ch) {
773
+ continue;
774
+ }
775
+ this.setControllerLayers(ch, layer);
776
+ }
777
+ }
778
+ }
779
+
780
+ public getRay(): Ray {
781
+ const ray = new Ray();
782
+ // this.tempMatrix.identity().extractRotation(this.controller.matrixWorld);
783
+ // ray.origin.setFromMatrixPosition(this.controller.matrixWorld);
784
+ ray.origin.copy(getWorldPosition(this.controller));
785
+ ray.direction.set(0, 0, -1).applyQuaternion(this.rayRotation);
786
+ return ray;
787
+ }
788
+
789
+ private closeGrabBoundingBoxHelper?: BoxHelper;
790
+
791
+ public overlap(): Intersection[] {
792
+ const overlapCenter = (this.isUsingHands && this.handPointerModel) ? this.handPointerModel.pointerObject : this.controllerGrip;
793
+
794
+ if (debug) {
795
+ if (!this.closeGrabBoundingBoxHelper && overlapCenter) {
796
+ this.closeGrabBoundingBoxHelper = new BoxHelper(overlapCenter, 0xffff00);
797
+ this.scene.add(this.closeGrabBoundingBoxHelper);
798
+ }
799
+
800
+ if (this.closeGrabBoundingBoxHelper && overlapCenter) {
801
+ this.closeGrabBoundingBoxHelper.setFromObject(overlapCenter);
802
+ }
803
+ }
804
+
805
+ if (!overlapCenter)
806
+ return new Array<Intersection>();
807
+
808
+ const wp = getWorldPosition(overlapCenter).clone();
809
+ return this.context.physics.sphereOverlap(wp, .02);
810
+ }
811
+
812
+ public raycast(): Intersection[] {
813
+ const opts = new RaycastOptions();
814
+ opts.layerMask = new Layers();
815
+ opts.layerMask.enableAll();
816
+ opts.layerMask.disable(2);
817
+ opts.ray = this.getRay();
818
+ const hits = this.context.physics.raycast(opts);
819
+ for (let i = 0; i < hits.length; i++) {
820
+ const hit = hits[i];
821
+ const obj = hit.object;
822
+ if (!this.testIsVisible(obj)) {
823
+ hits.splice(i, 1);
824
+ i--;
825
+ continue;
826
+ }
827
+ hit.object = UIRaycastUtils.getObject(obj);
828
+ break;
829
+ }
830
+ // console.log(...hits);
831
+ return hits;
832
+ }
833
+ }
834
+
835
+
836
+ export enum AttachedObjectEvents {
837
+ WillTake = "WillTake",
838
+ DidTake = "DidTake",
839
+ WillFree = "WillFree",
840
+ DidFree = "DidFree",
841
+ }
842
+
843
+ export class AttachedObject {
844
+
845
+ public static Events: { [key: string]: Function[] } = {};
846
+ public static AddEventListener(event: AttachedObjectEvents, callback: Function): Function {
847
+ if (!AttachedObject.Events[event]) AttachedObject.Events[event] = [];
848
+ AttachedObject.Events[event].push(callback);
849
+ return callback;
850
+ }
851
+ public static RemoveEventListener(event: AttachedObjectEvents, callback: Function | null) {
852
+ if (!callback) return;
853
+ if (!AttachedObject.Events[event]) return;
854
+ const idx = AttachedObject.Events[event].indexOf(callback);
855
+ if (idx >= 0) AttachedObject.Events[event].splice(idx, 1);
856
+ }
857
+
858
+
859
+ public static Current: AttachedObject[] = [];
860
+
861
+ private static Register(obj: AttachedObject) {
862
+
863
+ if (!this.Current.find(x => x === obj)) {
864
+ this.Current.push(obj);
865
+ }
866
+ }
867
+
868
+ private static Remove(obj: AttachedObject) {
869
+ const i = this.Current.indexOf(obj);
870
+ if (i >= 0) {
871
+ this.Current.splice(i, 1);
872
+ }
873
+ }
874
+
875
+ public static TryTake(controller: WebXRController, candidate: Object3D, intersection: Intersection, closeGrab: boolean): AttachedObject | null {
876
+ const interactable = GameObject.getComponentInParent(candidate, Interactable);
877
+ if (!interactable) {
878
+ if (debug)
879
+ console.warn("Prevented taking object that is not interactable", candidate);
880
+ return null;
881
+ }
882
+ else candidate = interactable.gameObject;
883
+
884
+
885
+ let objectToAttach = candidate;
886
+ const sync = GameObject.getComponentInParent(candidate, SyncedTransform);
887
+ if (sync) {
888
+ sync.requestOwnership();
889
+ objectToAttach = sync.gameObject;
890
+ }
891
+
892
+ for (const o of this.Current) {
893
+ if (o.selected === objectToAttach) {
894
+ if (o.controller === controller) return o;
895
+ o.free();
896
+ o.Take(controller, objectToAttach, candidate, sync, interactable, intersection, closeGrab);
897
+ return o;
898
+ }
899
+ }
900
+
901
+ const att = new AttachedObject();
902
+ att.Take(controller, objectToAttach, candidate, sync, interactable, intersection, closeGrab);
903
+ return att;
904
+ }
905
+
906
+
907
+ public sync: SyncedTransform | null = null;
908
+ public selected: Object3D | null = null;
909
+ public selectedParent: Object3D | null = null;
910
+ public selectedMesh: Mesh | null = null;
911
+ public controller: WebXRController | null = null;
912
+ public grabTime: number = 0;
913
+ public grabUUID: string = "";
914
+ public isCloseGrab: boolean = false; // when taken via sphere cast with hands
915
+
916
+ private originalMaterial: Material | Material[] | null = null;
917
+ private usageMarker: UsageMarker | null = null;
918
+ private rigidbodies: Rigidbody[] | null = null;
919
+ private didReparent: boolean = false;
920
+ private grabDistance: number = 0;
921
+ private interactable: Interactable | null = null;
922
+ private positionSource: Object3D | null = null;
923
+
924
+ private Take(controller: WebXRController, take: Object3D, hit: Object3D, sync: SyncedTransform | null, _interactable: Interactable,
925
+ intersection: Intersection, closeGrab: boolean)
926
+ : AttachedObject {
927
+ console.assert(take !== null, "Expected object to be taken but was", take);
928
+
929
+ if (controller.isUsingHands) {
930
+ this.positionSource = closeGrab ? controller.wrist : controller.controller;
931
+ }
932
+ else {
933
+ this.positionSource = controller.controller;
934
+ }
935
+ if (!this.positionSource) {
936
+ console.warn("No position source");
937
+ return this;
938
+ }
939
+
940
+ const args = { controller, take, hit, sync, interactable: _interactable };
941
+ AttachedObject.Events.WillTake?.forEach(x => x(this, args));
942
+
943
+
944
+ const mesh = hit as Mesh;
945
+ if (mesh?.material) {
946
+ this.originalMaterial = mesh.material;
947
+ if (!Array.isArray(mesh.material)) {
948
+ mesh.material = (mesh.material as Material).clone();
949
+ if (mesh.material && mesh.material["emissive"])
950
+ mesh.material["emissive"].b = .2;
951
+ }
952
+ }
953
+
954
+ this.selected = take;
955
+ if (!this.selectedParent) {
956
+ this.selectedParent = take.parent;
957
+ }
958
+ this.selectedMesh = mesh;
959
+ this.controller = controller;
960
+ this.interactable = _interactable;
961
+ this.isCloseGrab = closeGrab;
962
+ // if (interactable.canGrab) {
963
+ // this.didReparent = true;
964
+ // this.device.controller.attach(take);
965
+ // }
966
+ // else
967
+ this.didReparent = false;
968
+
969
+
970
+ this.sync = sync;
971
+ this.grabTime = controller.context.time.time;
972
+ this.grabUUID = Date.now().toString();
973
+ this.usageMarker = GameObject.addNewComponent(this.selected, UsageMarker);
974
+ this.rigidbodies = GameObject.getComponentsInChildren(this.selected, Rigidbody);
975
+ getWorldPosition(this.positionSource, this.lastControllerWorldPos);
976
+ const getGrabPoint = () => closeGrab ? this.lastControllerWorldPos.clone() : intersection.point.clone();
977
+ this.grabDistance = getGrabPoint().distanceTo(this.lastControllerWorldPos);
978
+ this.totalChangeAlongDirection = 0.0;
979
+
980
+ // we're storing position relative to the grab point
981
+ // we're storing rotation relative to the ray
982
+ this.localPositionOffsetToGrab = this.selected.worldToLocal(getGrabPoint());
983
+ const rot = controller.isUsingHands && closeGrab ? this.controller.getWristQuaternion()!.clone() : controller.rayRotation.clone();
984
+ getWorldQuaternion(this.selected, this.localQuaternionToGrab).premultiply(rot.invert());
985
+
986
+ const rig = this.controller.webXR!.Rig;
987
+ if (rig)
988
+ this.rigPositionLastFrame.copy(getWorldPosition(rig))
989
+
990
+ Avatar_POI.Add(controller.context, this.selected);
991
+ AttachedObject.Register(this);
992
+
993
+ if (this.sync) {
994
+ this.sync.fastMode = true;
995
+ }
996
+
997
+ AttachedObject.Events.DidTake?.forEach(x => x(this, args));
998
+
999
+ return this;
1000
+ }
1001
+
1002
+ public free(): void {
1003
+ if (!this.selected) return;
1004
+
1005
+ const args = { controller: this.controller, take: this.selected, hit: this.selected, sync: this.sync, interactable: null };
1006
+ AttachedObject.Events.WillFree?.forEach(x => x(this, args));
1007
+
1008
+ Avatar_POI.Remove(this.controller!.context, this.selected);
1009
+ AttachedObject.Remove(this);
1010
+
1011
+ if (this.sync) {
1012
+ this.sync.fastMode = false;
1013
+ }
1014
+
1015
+ const mesh = this.selectedMesh;
1016
+ if (mesh && this.originalMaterial && mesh.material) {
1017
+ mesh.material = this.originalMaterial;
1018
+ }
1019
+
1020
+ const object = this.selected;
1021
+ // only attach the object back if it has a parent
1022
+ // no parent means it was destroyed while holding it!
1023
+ if (this.didReparent && object.parent) {
1024
+ const prevParent = this.selectedParent;
1025
+ if (prevParent) prevParent.attach(object);
1026
+ else this.controller?.context.scene.attach(object);
1027
+ }
1028
+
1029
+ this.usageMarker?.destroy();
1030
+
1031
+ if (this.controller)
1032
+ this.controller.grabbed = null;
1033
+ this.selected = null;
1034
+ this.selectedParent = null;
1035
+ this.selectedMesh = null;
1036
+ this.sync = null;
1037
+
1038
+
1039
+ // TODO: make throwing work again
1040
+ if (this.rigidbodies) {
1041
+ for (const rb of this.rigidbodies) {
1042
+ rb.wakeUp();
1043
+ rb.setVelocity(rb.smoothedVelocity);
1044
+ }
1045
+ }
1046
+ this.rigidbodies = null;
1047
+
1048
+ this.localPositionOffsetToGrab = null;
1049
+ this.quaternionLerp = null;
1050
+
1051
+ AttachedObject.Events.DidFree?.forEach(x => x(this, args));
1052
+ }
1053
+
1054
+ public grabPoint: Vector3 = new Vector3();
1055
+
1056
+ private localPositionOffsetToGrab: Vector3 | null = null;
1057
+ private localPositionOffsetToGrab_worldSpace: Vector3 = new Vector3();
1058
+ private localQuaternionToGrab: Quaternion = new Quaternion(0, 0, 0, 1);
1059
+ private targetDir: Vector3 | null = null;
1060
+ private quaternionLerp: Quaternion | null = null;
1061
+
1062
+ private controllerDir = new Vector3();
1063
+ private controllerWorldPos = new Vector3();
1064
+ private lastControllerWorldPos = new Vector3();
1065
+ private controllerPosDelta = new Vector3();
1066
+ private totalChangeAlongDirection = 0.0;
1067
+ private rigPositionLastFrame = new Vector3();
1068
+
1069
+ private controllerMovementSinceLastFrame() {
1070
+ if (!this.positionSource || !this.controller) return 0.0;
1071
+
1072
+ // controller direction
1073
+ this.controllerDir.set(0, 0, -1);
1074
+ this.controllerDir.applyQuaternion(this.controller.rayRotation);
1075
+
1076
+ // controller delta
1077
+ getWorldPosition(this.positionSource, this.controllerWorldPos);
1078
+ this.controllerPosDelta.copy(this.controllerWorldPos);
1079
+ this.controllerPosDelta.sub(this.lastControllerWorldPos);
1080
+ this.lastControllerWorldPos.copy(this.controllerWorldPos);
1081
+ const rig = this.controller.webXR!.Rig;
1082
+ if (rig) {
1083
+ const rigPos = getWorldPosition(rig);
1084
+ const rigDelta = this.rigPositionLastFrame.sub(rigPos);
1085
+ this.controllerPosDelta.add(rigDelta);
1086
+ this.rigPositionLastFrame.copy(rigPos);
1087
+ }
1088
+
1089
+ // calculate delta along direction
1090
+ const changeAlongControllerDirection = this.controllerDir.dot(this.controllerPosDelta);
1091
+
1092
+ return changeAlongControllerDirection;
1093
+ }
1094
+
1095
+ public update() {
1096
+ if (this.rigidbodies)
1097
+ for (const rb of this.rigidbodies)
1098
+ rb.resetVelocities();
1099
+ // TODO: add/use sync lost ownership event
1100
+ if (this.sync && this.controller && this.controller.context.connection.isInRoom) {
1101
+ const td = this.controller.context.time.time - this.grabTime;
1102
+ // if (time.frameCount % 60 === 0) {
1103
+ // console.log("ownership?", this.selected.name, this.sync.hasOwnership(), td)
1104
+ // }
1105
+ if (td > 3) {
1106
+ // if (time.frameCount % 60 === 0) {
1107
+ // console.log(this.sync.hasOwnership())
1108
+ // }
1109
+ if (this.sync.hasOwnership() === false) {
1110
+ console.log("no ownership, will leave", this.sync.guid);
1111
+ this.free();
1112
+ }
1113
+ }
1114
+ }
1115
+ if (this.interactable && !this.interactable.canGrab) return;
1116
+
1117
+ if (!this.didReparent && this.selected && this.controller) {
1118
+
1119
+ const rigScale = this.controller.webXR!.Rig?.scale.x ?? 1.0;
1120
+
1121
+ this.totalChangeAlongDirection += this.controllerMovementSinceLastFrame();
1122
+ // console.log(this.totalChangeAlongDirection);
1123
+
1124
+ // alert("yo: " + this.controller.webXR.Rig?.scale.x); // this is 0.1 on Hololens
1125
+ let currentDist = 1.0;
1126
+ if (this.controller.type === ControllerType.PhysicalDevice) // only for controllers and not on touch (AR touches are controllers)
1127
+ {
1128
+ currentDist = Math.max(0.0, 1 + this.totalChangeAlongDirection * 2.0 / rigScale);
1129
+ currentDist = currentDist * currentDist * currentDist;
1130
+ }
1131
+ if (this.grabDistance / rigScale < 0.8) currentDist = 1.0; // don't accelerate if this is a close grab, want full control
1132
+
1133
+ if (!this.targetDir) {
1134
+ this.targetDir = new Vector3();
1135
+ }
1136
+ this.targetDir.set(0, 0, -this.grabDistance * currentDist);
1137
+ const target = this.targetDir.applyQuaternion(this.controller.rayRotation).add(this.controllerWorldPos);
1138
+
1139
+ // apply rotation
1140
+ const targetQuat = this.controller.rayRotation.clone().multiplyQuaternions(this.controller.rayRotation, this.localQuaternionToGrab);
1141
+ if (!this.quaternionLerp) {
1142
+ this.quaternionLerp = targetQuat.clone();
1143
+ }
1144
+ this.quaternionLerp.slerp(targetQuat, this.controller.useSmoothing ? this.controller.context.time.deltaTime / .03 : 1.0);
1145
+ setWorldQuaternion(this.selected, this.quaternionLerp);
1146
+ this.selected.updateWorldMatrix(false, false); // necessary so that rotation is correct for the following position update
1147
+
1148
+ // apply position
1149
+ this.grabPoint.copy(target);
1150
+ // apply local grab offset
1151
+ if (this.localPositionOffsetToGrab) {
1152
+ this.localPositionOffsetToGrab_worldSpace.copy(this.localPositionOffsetToGrab);
1153
+ this.selected.localToWorld(this.localPositionOffsetToGrab_worldSpace).sub(getWorldPosition(this.selected));
1154
+ target.sub(this.localPositionOffsetToGrab_worldSpace);
1155
+ }
1156
+ setWorldPosition(this.selected, target);
1157
+ }
1158
+
1159
+
1160
+ if (this.rigidbodies != null) {
1161
+ for (const rb of this.rigidbodies) {
1162
+ rb.wakeUp();
1163
+ }
1164
+ }
1165
+
1166
+ InstancingUtil.markDirty(this.selected, true);
1167
+ }
1168
+ }
src/engine-components/webxr/WebXRGrabRendering.ts ADDED
@@ -0,0 +1,151 @@
1
+ import { getWorldPosition, setWorldPosition, setWorldPositionXYZ } from "../../engine/engine_three_utils.js";
2
+ import { Behaviour, GameObject } from "../Component.js";
3
+ import { AttachedObject, AttachedObjectEvents } from "./WebXRController.js";
4
+ import { Object3D, Vector3 } from "three";
5
+ import { PlayerColor } from "../PlayerColor.js";
6
+ import { Context } from "../../engine/engine_setup.js";
7
+ import { type IModel, SendQueue } from "../../engine/engine_networking_types.js";
8
+
9
+ enum XRGrabEvent {
10
+ StartOrUpdate = "xr-grab-visual-start-or-update",
11
+ End = "xr-grab-visual-end",
12
+ }
13
+
14
+ export class XRGrabModel implements IModel {
15
+ guid!: any;
16
+ dontSave: boolean = true;
17
+
18
+ userId : string | null | undefined;
19
+ point: { x: number, y: number, z: number } = { x: 0, y: 0, z: 0 };
20
+ source: { x: number, y: number, z: number } = { x: 0, y: 0, z: 0 };
21
+ target: string | undefined;
22
+
23
+ update(context : Context, point: Vector3, source: Vector3, target: string | undefined = undefined) {
24
+ this.userId = context.connection.connectionId;
25
+ this.point.x = point.x;
26
+ this.point.y = point.y;
27
+ this.point.z = point.z;
28
+ this.source.x = source.x;
29
+ this.source.y = source.y;
30
+ this.source.z = source.z;
31
+ this.target = target;
32
+ }
33
+ }
34
+
35
+ // sends grab info to other users and creates rendering instances
36
+ export class XRGrabRendering extends Behaviour {
37
+ prefab: Object3D | null = null;
38
+
39
+ private _grabModels: Array<XRGrabModel> = [];
40
+ private _grabModelsUpdateTime: Array<number> = [];
41
+ private _addOrUpdateSub: Function | null = null;
42
+ private _endSub: Function | null = null;
43
+ private _freeSub: Function | null = null;
44
+ private _instances: { [key: string]: {instance:Object3D, model:XRGrabModel} } = {};
45
+
46
+ awake(): void {
47
+ if(this.prefab) this.prefab.visible = false;
48
+ }
49
+
50
+ onEnable(): void {
51
+ this._addOrUpdateSub = this.context.connection.beginListen(XRGrabEvent.StartOrUpdate, this.onRemoteGrabStartOrUpdate.bind(this));
52
+ this._endSub = this.context.connection.beginListen(XRGrabEvent.End, this.onRemoteGrabEnd.bind(this));
53
+ this._freeSub = AttachedObject.AddEventListener(AttachedObjectEvents.WillFree, this.onAttachedObjectFree.bind(this));
54
+ }
55
+
56
+ onDisable(): void {
57
+ this.context.connection.stopListen(XRGrabEvent.StartOrUpdate, this._addOrUpdateSub);
58
+ this.context.connection.stopListen(XRGrabEvent.End, this._endSub);
59
+ AttachedObject.RemoveEventListener(AttachedObjectEvents.WillFree, this._freeSub);
60
+ }
61
+
62
+ addOrUpdateGrab(model: XRGrabModel) {
63
+ this.context.connection.send(XRGrabEvent.StartOrUpdate, model, SendQueue.Queued);
64
+ }
65
+
66
+ endGrab(model: XRGrabModel) {
67
+ this.context.connection.send(XRGrabEvent.End, model, SendQueue.Queued);
68
+ }
69
+
70
+ private onRemoteGrabStartOrUpdate(data: XRGrabModel) {
71
+ if(!this.prefab) return;
72
+ const inst = this._instances[data.guid];
73
+ if(!inst)
74
+ {
75
+ const instance = GameObject.instantiate(this.prefab) as Object3D;
76
+ instance.visible = true;
77
+ this._instances[data.guid] = {instance, model:data};
78
+ if(data.userId){
79
+ const playerColor = GameObject.getComponentsInChildren(instance, PlayerColor);
80
+ if(playerColor?.length > 0)
81
+ {
82
+ for(const pl of playerColor){
83
+ pl.assignUserColor(data.userId)
84
+ }
85
+ }
86
+ }
87
+ return;
88
+ }
89
+ inst.model = data;
90
+ }
91
+
92
+ private onRemoteGrabEnd(data: XRGrabModel) {
93
+ if (!data) return;
94
+ const id = data.guid;
95
+ if(this._instances[id])
96
+ {
97
+ GameObject.destroy(this._instances[id].instance);
98
+ delete this._instances[id];
99
+ }
100
+ }
101
+
102
+ private onAttachedObjectFree(att: AttachedObject) {
103
+ if (this._grabModels.length <= 0) return;
104
+ const mod = this._grabModels[0];
105
+ this.updateModel(mod, att);
106
+ this.endGrab(mod);
107
+ }
108
+
109
+ onBeforeRender() {
110
+ this.updateRendering();
111
+
112
+ if (!this.prefab) return;
113
+ this.prefab.visible = false;
114
+ if (this.context.time.frameCount % 10 !== 0) return;
115
+ for (let i = 0; i < AttachedObject.Current.length; i++) {
116
+ const att = AttachedObject.Current[i];
117
+
118
+ if (!att.controller || !att.selected) continue;
119
+
120
+ if (this._grabModels.length <= i) {
121
+ this._grabModels.push(new XRGrabModel());
122
+ this._grabModelsUpdateTime.push(0);
123
+ }
124
+ this._grabModelsUpdateTime[i] = this.context.time.time;
125
+ const model = this._grabModels[i];
126
+ this.updateModel(model, att);
127
+ this.addOrUpdateGrab(model);
128
+ }
129
+ }
130
+
131
+ private updateModel(model: XRGrabModel, att: AttachedObject) {
132
+ if (!att.controller || !att.selected) return;
133
+ model.guid = att.grabUUID;
134
+ const targetObject = att.selected["guid"];
135
+ model.update(this.context, att.grabPoint, att.controller.worldPosition, targetObject);
136
+ }
137
+
138
+ private temp : Vector3 = new Vector3();
139
+ private updateRendering() {
140
+ const step = this.context.time.deltaTime / .5;
141
+ for(const key in this._instances){
142
+ const { instance, model } = this._instances[key];
143
+ if(!instance || !model) continue;
144
+ const { point } = model;
145
+ const wp = getWorldPosition(instance);
146
+ this.temp.set(point.x, point.y, point.z);
147
+ wp.lerp(this.temp, step);
148
+ setWorldPosition(instance, wp);
149
+ }
150
+ }
151
+ }
src/engine-components/webxr/WebXRSync.ts ADDED
@@ -0,0 +1,463 @@
1
+ import { Behaviour, GameObject } from "../Component.js";
2
+ import { RoomEvents, OwnershipModel, NetworkConnection } from "../../engine/engine_networking.js";
3
+ import { WebXR, WebXREvent } from "./WebXR.js";
4
+ import { Group, Quaternion, Vector3, Vector4, WebXRManager } from "three";
5
+ import { getParam } from "../../engine/engine_utils.js";
6
+ import { Voip } from "../Voip.js";
7
+ import { Builder, Long } from "flatbuffers";
8
+ import { VrUserStateBuffer } from "../../engine-schemes/vr-user-state-buffer.js";
9
+ import { Vec3 } from "../../engine-schemes/vec3.js";
10
+ import { registerBinaryType } from "../../engine-schemes/schemes.js";
11
+ import { Vec4 } from "../../engine-schemes/vec4.js";
12
+ import { WebXRAvatar } from "./WebXRAvatar.js";
13
+
14
+ // for debug GUI
15
+ // import { GUI } from "three/examples/jsm/libs/lil-gui.module.min.js";
16
+ // import { HTMLMesh } from 'three/examples/jsm/interactive/HTMLMesh.js';
17
+ // import { InteractiveGroup } from 'three/examples/jsm/interactive/InteractiveGroup.js';
18
+ // import { renderer, sceneData } from "../engine/engine_setup.js";
19
+
20
+ const debugLogs = getParam("debugxr");
21
+ const debugAvatar = getParam("debugavatar");
22
+ // const debugAvatarVoip = getParam("debugavatarvoip");
23
+
24
+ enum WebXRSyncEvent {
25
+ WebXR_UserJoined = "webxr-user-joined",
26
+ WebXR_UserLeft = "webxr-user-left",
27
+ VRSessionStart = "vr-session-started",
28
+ VRSessionEnd = "vr-session-ended",
29
+ VRSessionUpdate = "vr-session-update",
30
+ }
31
+
32
+ enum XRMode {
33
+ VR = "vr",
34
+ AR = "ar",
35
+ }
36
+
37
+ const VRUserStateBufferIdentifier = "VRUS";
38
+ registerBinaryType(VRUserStateBufferIdentifier, VrUserStateBuffer.getRootAsVrUserStateBuffer);
39
+
40
+ function getTimeStampNow() {
41
+ return new Date().getTime(); // avoid sending millis in flatbuffer
42
+ }
43
+
44
+ function flatbuffers_long_from_number(num: number): Long {
45
+ const low = num & 0xffffffff
46
+ const high = (num / Math.pow(2, 32)) & 0xfffff
47
+ return Long.create(low, high);
48
+ }
49
+
50
+ export class VRUserState {
51
+ public guid: string;
52
+ public time!: number;
53
+ public avatarId!: string;
54
+ public position: Vector3 = new Vector3();
55
+ public rotation: Vector4 = new Vector4();
56
+ public scale: number = 1;
57
+
58
+ public posLeftHand = new Vector3();
59
+ public posRightHand = new Vector3();
60
+
61
+ public rotLeftHand = new Quaternion();
62
+ public rotRightHand = new Quaternion();
63
+
64
+ public constructor(guid: string) {
65
+ this.guid = guid;
66
+ }
67
+
68
+ private static invertRotation: Quaternion = new Quaternion().setFromAxisAngle(new Vector3(0, 1, 0), Math.PI);
69
+
70
+ public update(rig: Group, pos: DOMPointReadOnly, rot: DOMPointReadOnly, webXR: WebXR, avatarId: string) {
71
+ this.time = getTimeStampNow();
72
+ this.avatarId = avatarId;
73
+ this.position.set(pos.x, pos.y, pos.z);
74
+ if (rig)
75
+ this.position.applyMatrix4(rig.matrixWorld);
76
+
77
+ let q0 = VRUserState.quat0;
78
+ const q1 = VRUserState.quat1;
79
+ q0.set(rot.x, rot.y, rot.z, rot.w);
80
+ q0 = q0.multiplyQuaternions(q0, VRUserState.invertRotation);
81
+
82
+ if (rig) {
83
+ rig.getWorldQuaternion(q1);
84
+ q0.multiplyQuaternions(q1, q0);
85
+ }
86
+
87
+ this.rotation.set(q0.x, q0.y, q0.z, q0.w);
88
+ this.scale = rig.scale.x;
89
+
90
+ // for controllers, it seems we need grip pose
91
+ const ctrl0 = webXR.LeftController?.controllerGrip;
92
+ if (ctrl0) {
93
+ ctrl0.getWorldPosition(this.posLeftHand);
94
+ ctrl0.getWorldQuaternion(this.rotLeftHand);
95
+ }
96
+ const ctrl1 = webXR.RightController?.controllerGrip;
97
+ if (ctrl1) {
98
+ ctrl1.getWorldPosition(this.posRightHand);
99
+ ctrl1.getWorldQuaternion(this.rotRightHand);
100
+ }
101
+
102
+ // if this is a hand, we need to get the root bone of that / use that for position/rotation
103
+ if (webXR.LeftController?.hand?.visible) {
104
+ const wrist = webXR.LeftController.wrist;
105
+ if (wrist) {
106
+ wrist.getWorldPosition(this.posLeftHand);
107
+ wrist.getWorldQuaternion(this.rotLeftHand);
108
+ }
109
+ }
110
+
111
+ if (webXR.RightController?.hand?.visible) {
112
+ const wrist = webXR.RightController.wrist;
113
+ if (wrist) {
114
+ wrist.getWorldPosition(this.posRightHand);
115
+ wrist.getWorldQuaternion(this.rotRightHand);
116
+ }
117
+ }
118
+ }
119
+
120
+ private static quat0: Quaternion = new Quaternion();
121
+ private static quat1: Quaternion = new Quaternion();
122
+
123
+ public sendAsBuffer(builder: Builder, net: NetworkConnection) {
124
+ builder.clear();
125
+ const guid = builder.createString(this.guid);
126
+ const id = builder.createString(this.avatarId);
127
+ VrUserStateBuffer.startVrUserStateBuffer(builder);
128
+ VrUserStateBuffer.addGuid(builder, guid);
129
+ VrUserStateBuffer.addTime(builder, flatbuffers_long_from_number(this.time));
130
+ VrUserStateBuffer.addAvatarId(builder, id);
131
+ VrUserStateBuffer.addPosition(builder, Vec3.createVec3(builder, this.position.x, this.position.y, this.position.z));
132
+ VrUserStateBuffer.addRotation(builder, Vec4.createVec4(builder, this.rotation.x, this.rotation.y, this.rotation.z, this.rotation.w));
133
+ VrUserStateBuffer.addScale(builder, this.scale);
134
+ VrUserStateBuffer.addPosLeftHand(builder, Vec3.createVec3(builder, this.posLeftHand.x, this.posLeftHand.y, this.posLeftHand.z));
135
+ VrUserStateBuffer.addPosRightHand(builder, Vec3.createVec3(builder, this.posRightHand.x, this.posRightHand.y, this.posRightHand.z));
136
+ VrUserStateBuffer.addRotLeftHand(builder, Vec4.createVec4(builder, this.rotLeftHand.x, this.rotLeftHand.y, this.rotLeftHand.z, this.rotLeftHand.w));
137
+ VrUserStateBuffer.addRotRightHand(builder, Vec4.createVec4(builder, this.rotRightHand.x, this.rotRightHand.y, this.rotRightHand.z, this.rotRightHand.w));
138
+ const res = VrUserStateBuffer.endVrUserStateBuffer(builder);
139
+ builder.finish(res, VRUserStateBufferIdentifier);
140
+ const arr = builder.asUint8Array();
141
+ net.sendBinary(arr);
142
+ }
143
+
144
+ public setFromBuffer(guid: string, state: VrUserStateBuffer) {
145
+ if (!guid) return;
146
+ this.guid = guid;
147
+ this.time = state.time().toFloat64();
148
+ const id = state.avatarId();
149
+ if (id)
150
+ this.avatarId = id;
151
+ const pos = state.position();
152
+ if (pos)
153
+ this.position.set(pos.x(), pos.y(), pos.z());
154
+ // TODO: maybe just send one float more instead of converting back and forth
155
+ const rot = state.rotation();
156
+ if (rot)
157
+ this.rotation.set(rot.x(), rot.y(), rot.z(), rot.w());
158
+ const posLeftHand = state.posLeftHand();
159
+ if (posLeftHand)
160
+ this.posLeftHand.set(posLeftHand.x(), posLeftHand.y(), posLeftHand.z());
161
+ const posRightHand = state.posRightHand();
162
+ if (posRightHand)
163
+ this.posRightHand.set(posRightHand.x(), posRightHand.y(), posRightHand.z());
164
+ const rotLeftHand = state.rotLeftHand();
165
+ if (rotLeftHand)
166
+ this.rotLeftHand.set(rotLeftHand.x(), rotLeftHand.y(), rotLeftHand.z(), rotLeftHand.w());
167
+ const rotRightHand = state.rotRightHand();
168
+ if (rotRightHand)
169
+ this.rotRightHand.set(rotRightHand.x(), rotRightHand.y(), rotRightHand.z(), rotRightHand.w());
170
+ this.scale = state.scale();
171
+ }
172
+ }
173
+
174
+ export class WebXRSync extends Behaviour {
175
+
176
+ webXR: WebXR | null = null;
177
+
178
+ // private allowCustomAvatars: boolean | null = true;
179
+
180
+ private debugAvatarUser: WebXRAvatar | null = null;
181
+ private voip: Voip | null = null;
182
+
183
+ async awake() {
184
+
185
+ if(!this.webXR) this.webXR = GameObject.getComponent(this.gameObject, WebXR);
186
+ if(!this.webXR) this.webXR = GameObject.findObjectOfType(WebXR, this.context);
187
+
188
+ if(!this.webXR)
189
+ {
190
+ this.webXR = GameObject.findObjectOfType(WebXR, this.context);
191
+ if(!this.webXR) {
192
+ console.warn("WebXRSync: Could not find WebXR component, won't sync.");
193
+ return;
194
+ }
195
+ }
196
+
197
+ if (!this.voip) this.voip = GameObject.findObjectOfType(Voip, this.context);
198
+
199
+ if (debugAvatar) {
200
+ const debugGuid = "debug-avatar-" + debugAvatar;
201
+ const newUser = new WebXRAvatar(this.context, debugGuid, this.webXR);
202
+ // newUser.isLocalAvatar = true;
203
+ this.debugAvatarUser = newUser;
204
+ if (typeof debugAvatar === "string" && debugAvatar.length > 0) {
205
+ if (await newUser.setAvatarOverride(debugAvatar)) {
206
+ const debugState = new VRUserState(debugGuid);
207
+ debugState.position.y += 1;
208
+ const off = .5;
209
+ debugState.posLeftHand.y += off;
210
+ debugState.posLeftHand.x += off;
211
+ debugState.posRightHand.y += off;
212
+ debugState.posRightHand.x -= off;
213
+ newUser.tryUpdate(debugState, 0);
214
+ }
215
+ else {
216
+ newUser.destroy();
217
+ }
218
+ }
219
+ }
220
+ }
221
+
222
+ onEnable() {
223
+ // const debugUser = new WebXRAvatar(this.context, "sorry-no-guid", this.webXR!);
224
+
225
+ if (!this.webXR) {
226
+ this.webXR = GameObject.getComponent(this.gameObject, WebXR);
227
+ if (!this.webXR) {
228
+ console.warn("Missing webxr component on " + this.gameObject.name);
229
+ return;
230
+ }
231
+ }
232
+
233
+ this.eventSub_WebXRStartEvent = this.onXRSessionStart.bind(this);
234
+ WebXR.addEventListener(WebXREvent.XRStarted, this.eventSub_WebXRStartEvent);
235
+ this.eventSub_WebXRUpdateEvent = this.onXRSessionUpdate.bind(this);
236
+ WebXR.addEventListener(WebXREvent.XRUpdate, this.eventSub_WebXRUpdateEvent);
237
+ this.eventSub_WebXREndEvent = this.onXRSessionEnded.bind(this);
238
+ WebXR.addEventListener(WebXREvent.XRStopped, this.eventSub_WebXREndEvent);
239
+
240
+ this.eventSub_ConnectionEvent = this.onConnected.bind(this);
241
+ this.context.connection.beginListen(RoomEvents.JoinedRoom, this.eventSub_ConnectionEvent);
242
+ this.context.connection.beginListen(WebXRSyncEvent.WebXR_UserJoined, _evt => {
243
+ console.log("webxr user joined evt");
244
+ });
245
+ this.context.connection.beginListen(WebXRSyncEvent.WebXR_UserLeft, evt => {
246
+ const hasId = evt.id !== null && evt.id !== undefined;
247
+ if (!hasId) return;
248
+ console.log("webxr user left evt");
249
+ if (hasId) {
250
+ const avatar = this.avatars[evt.id];
251
+ avatar?.destroy();
252
+ this.avatars[evt.id] = undefined;
253
+ }
254
+ });
255
+ this.context.connection.beginListenBinary(VRUserStateBufferIdentifier, (state: VrUserStateBuffer) => {
256
+ // console.log("BUFFER", state);
257
+ const guid = state.guid();
258
+ if (!guid) return;
259
+ const time = state.time().toFloat64();
260
+ const temp = this.tempState;
261
+ temp.setFromBuffer(guid, state);
262
+ // console.log(temp);
263
+ const user = this.onTryGetAvatar(guid, time);
264
+ user?.tryUpdate(temp, time);
265
+ });
266
+ this.context.connection.beginListen(WebXRSyncEvent.VRSessionUpdate, (state: VRUserState) => {
267
+ const guid = state.guid;
268
+ const time = state.time;
269
+ const user = this.onTryGetAvatar(guid, time);
270
+ user?.tryUpdate(state, time);
271
+ });
272
+ }
273
+
274
+ private tempState: VRUserState = new VRUserState("");
275
+
276
+ private onTryGetAvatar(guid: string, time: number) {
277
+ if (guid === this.context.connection.connectionId) return null; // ignore self in case we receive that also!
278
+ const timeDiff = new Date().getTime() - time;
279
+ if (timeDiff > 5000) {
280
+ if (debugLogs)
281
+ console.log("old data", timeDiff, guid)
282
+ return null;
283
+ }
284
+ if (!this.webXR) return null;
285
+ let user = this.avatars[guid];
286
+ if (user === undefined) {
287
+ try {
288
+ console.log("create new avatar");
289
+ const newUser = new WebXRAvatar(this.context, guid, this.webXR);
290
+ user = newUser;
291
+ this.avatars[guid] = newUser;
292
+ } catch (err) {
293
+ this.avatars[guid] = null;
294
+ console.error(err);
295
+ }
296
+ }
297
+ return user;
298
+ }
299
+
300
+ onDisable() {
301
+ if (this.eventSub_ConnectionEvent)
302
+ this.context.connection.stopListen(RoomEvents.JoinedRoom, this.eventSub_ConnectionEvent);
303
+ WebXR.removeEventListener(WebXREvent.XRStarted, this.eventSub_WebXRStartEvent);
304
+ WebXR.removeEventListener(WebXREvent.XRUpdate, this.eventSub_WebXRUpdateEvent);
305
+ WebXR.removeEventListener(WebXREvent.XRStopped, this.eventSub_WebXREndEvent);
306
+ }
307
+
308
+ update(): void {
309
+
310
+ const now = getTimeStampNow();
311
+
312
+ if (this.debugAvatarUser) {
313
+ this.debugAvatarUser.lastUpdate = now;
314
+ }
315
+
316
+ this.detectPotentiallyDisconnectedAvatarsAndRemove();
317
+
318
+ for (const key in this.avatars) {
319
+ const avatar = this.avatars[key];
320
+ if (!avatar) continue;
321
+ avatar.update();
322
+ }
323
+ }
324
+
325
+
326
+ private _removeAvatarsList: string[] = [];
327
+ private detectPotentiallyDisconnectedAvatarsAndRemove() {
328
+ const utcnow = getTimeStampNow();
329
+ for (const key in this.avatars) {
330
+ const avatar = this.avatars[key];
331
+ if (!avatar) {
332
+ this._removeAvatarsList.push(key);
333
+ continue;
334
+ }
335
+ if (utcnow - avatar.lastUpdate > 10_000) {
336
+ console.log("avatar timed out (didnt receive any updates in a while) - destroying it now");
337
+ avatar.destroy();
338
+ this.avatars[key] = undefined;
339
+ }
340
+ }
341
+ for (const rem of this._removeAvatarsList) {
342
+ delete this.avatars[rem];
343
+ }
344
+ this._removeAvatarsList.length = 0;
345
+ }
346
+
347
+ private buildLocalAvatar() {
348
+ if (this.localAvatar || !this.webXR) return;
349
+ const connectionId = this.context.connection?.connectionId ?? this.k_LocalAvatarNoNetworkingGuid;
350
+ this.localAvatar = new WebXRAvatar(this.context, connectionId, this.webXR);
351
+ this.localAvatar.isLocalAvatar = true;
352
+ this.localAvatar.setAvatarOverride(this.getAvatarId());
353
+ this.avatars[this.localAvatar.guid] = this.localAvatar;
354
+ }
355
+
356
+
357
+ private eventSub_ConnectionEvent: Function | null = null;
358
+ private eventSub_WebXRStartEvent: Function | null = null;
359
+ private eventSub_WebXREndEvent: Function | null = null;
360
+ private eventSub_WebXRUpdateEvent: Function | null = null;
361
+ private avatars: { [key: string]: WebXRAvatar | undefined | null } = {}
362
+ private localAvatar: WebXRAvatar | null = null;
363
+ private k_LocalAvatarNoNetworkingGuid = "local";
364
+
365
+ private onConnected() {
366
+ // this event gets fired when we have joined a room and are ready to update
367
+ if (debugLogs)
368
+ console.log("Hey you are connected as " + this.context.connection.connectionId);
369
+
370
+ if (this.localAvatar?.guid === this.k_LocalAvatarNoNetworkingGuid) {
371
+ if (this.localAvatar) {
372
+ this.localAvatar?.destroy();
373
+ this.avatars[this.localAvatar.guid] = undefined;
374
+ }
375
+ this.localAvatar = null;
376
+ this.xrState = null;
377
+ this.ownership?.freeOwnership();
378
+ this.ownership = null;
379
+ }
380
+ }
381
+
382
+ private onXRSessionStart(_evt: { session: XRSession }) {
383
+ console.log("XR session started");
384
+ this.context.connection.send(WebXRSyncEvent.WebXR_UserJoined, { id: this.context.connection.connectionId, mode: XRMode.VR });
385
+
386
+ if (this.localAvatar) {
387
+ this.localAvatar?.destroy();
388
+ this.avatars[this.localAvatar.guid] = undefined;
389
+ this.localAvatar = null;
390
+ }
391
+ this.xrState = null;
392
+ this.ownership?.freeOwnership();
393
+ this.ownership = null;
394
+
395
+ if (this.avatars) {
396
+ for (const key in this.avatars) {
397
+ this.avatars[key]?.updateFlags();
398
+ }
399
+ }
400
+ }
401
+
402
+ private onXRSessionEnded(_evt: { session: XRSession }) {
403
+ console.log("XR session ended");
404
+ this.context.connection.send(WebXRSyncEvent.WebXR_UserLeft, { id: this.context.connection.connectionId, mode: XRMode.VR });
405
+ if(this.localAvatar){
406
+ this.localAvatar?.destroy();
407
+ this.avatars[this.localAvatar.guid] = undefined;
408
+ this.localAvatar = null;
409
+ }
410
+ }
411
+
412
+ private ownership: OwnershipModel | null = null;
413
+ private xrState: VRUserState | null = null;
414
+ private builder: Builder = new Builder(1024);
415
+
416
+ private onXRSessionUpdate(evt: { rig: Group, frame: XRFrame, xr: WebXRManager, input: XRInputSource[] }) {
417
+
418
+ this.xrState ??= new VRUserState(this.context.connection.connectionId ?? this.k_LocalAvatarNoNetworkingGuid);
419
+ this.ownership ??= new OwnershipModel(this.context.connection, this.context.connection.connectionId ?? this.k_LocalAvatarNoNetworkingGuid);
420
+ this.ownership.guid = this.context.connection.connectionId ?? this.k_LocalAvatarNoNetworkingGuid;
421
+ this.buildLocalAvatar();
422
+
423
+
424
+ const { frame, xr, rig } = evt;
425
+ const pose = frame.getViewerPose(xr.getReferenceSpace()!);
426
+ if (!pose) return; // e.g. if user is not wearing headset
427
+ const transform: XRRigidTransform = pose?.transform;
428
+ const pos = transform.position;
429
+ const rot = transform.orientation;
430
+ this.xrState.update(rig, pos, rot, this.webXR!, this.getAvatarId());
431
+
432
+ if (this.localAvatar) {
433
+ if (this.context.connection.connectionId) {
434
+ this.localAvatar.guid = this.context.connection.connectionId;
435
+ }
436
+ this.localAvatar.tryUpdate(this.xrState, 0);
437
+ }
438
+
439
+ if (this.ownership && !this.ownership.hasOwnership && this.context.connection.isConnected) {
440
+ if (this.context.time.frameCount % 120 === 0)
441
+ this.ownership.requestOwnership();
442
+ if (!this.ownership.hasOwnership) {
443
+ // console.log("NO OWNERSHIP", this.ownership.guid);
444
+ return;
445
+ }
446
+ }
447
+
448
+ if (!this.context.connection.isConnected || !this.context.connection.connectionId) {
449
+ return;
450
+ }
451
+
452
+ this.xrState.sendAsBuffer(this.builder, this.context.connection);
453
+
454
+ // this.context.connection.send(WebXRSyncEvent.VRSessionUpdate, this.xrState);
455
+
456
+ }
457
+
458
+ private getAvatarId() {
459
+ const urlAvatar = getParam("avatar") as string;
460
+ const avatarId = urlAvatar ?? null;
461
+ return avatarId;
462
+ }
463
+ }
src/engine-components/XRFlag.ts ADDED
@@ -0,0 +1,139 @@
1
+ import { Behaviour, GameObject } from "./Component.js";
2
+ import { getParam } from "../engine/engine_utils.js";
3
+ import { serializable } from "../engine/engine_serialization_decorator.js";
4
+
5
+
6
+ const debug = getParam("debugflags");
7
+
8
+ export enum XRStateFlag {
9
+ Never = 0,
10
+ Browser = 1 << 0,
11
+ AR = 1 << 1,
12
+ VR = 1 << 2,
13
+ FirstPerson = 1 << 3,
14
+ ThirdPerson = 1 << 4,
15
+ All = 0xffffffff
16
+ }
17
+
18
+ export class XRState {
19
+
20
+ public static Global: XRState = new XRState();
21
+
22
+ public Mask: XRStateFlag = XRStateFlag.Browser | XRStateFlag.ThirdPerson;
23
+
24
+ public Has(state: XRStateFlag) {
25
+ const res = (this.Mask & state);
26
+ return res !== 0;
27
+ }
28
+
29
+ public Set(state: number) {
30
+ if(debug) console.warn("Set XR flag state to", state)
31
+ this.Mask = state as number;
32
+ XRFlag.Apply();
33
+ }
34
+
35
+ public Enable(state: number) {
36
+ this.Mask |= state;
37
+ XRFlag.Apply();
38
+ }
39
+
40
+ public Disable(state: number) {
41
+ this.Mask &= ~state;
42
+ XRFlag.Apply();
43
+ }
44
+
45
+ public Toggle(state: number) {
46
+ this.Mask ^= state;
47
+ XRFlag.Apply();
48
+ }
49
+
50
+ public EnableAll() {
51
+ this.Mask = 0xffffffff | 0;
52
+ XRFlag.Apply();
53
+ }
54
+
55
+ public DisableAll() {
56
+ this.Mask = 0;
57
+ XRFlag.Apply();
58
+ }
59
+ }
60
+
61
+ export class XRFlag extends Behaviour {
62
+
63
+ private static registry: XRFlag[] = [];
64
+
65
+ public static Apply() {
66
+ for (const r of this.registry) r.UpdateVisible(XRState.Global);
67
+ }
68
+
69
+ private static firstApply: boolean;
70
+ private static buffer: XRState = new XRState();
71
+
72
+ @serializable()
73
+ public visibleIn!: number;
74
+
75
+ awake() {
76
+ XRFlag.registry.push(this);
77
+ }
78
+
79
+ onEnable(): void {
80
+ if (!XRFlag.firstApply) {
81
+ XRFlag.firstApply = true;
82
+ XRFlag.Apply();
83
+ }
84
+ else {
85
+ this.UpdateVisible(XRState.Global);
86
+ }
87
+ }
88
+
89
+ onDestroy(): void {
90
+ const i = XRFlag.registry.indexOf(this);
91
+ if (i >= 0)
92
+ XRFlag.registry.splice(i, 1);
93
+ }
94
+
95
+ public get isOn(): boolean { return this.gameObject.visible; }
96
+
97
+ public UpdateVisible(state: XRState | XRStateFlag | null = null) {
98
+ // XR flags set visibility of whole hierarchy which is like setting the whole object inactive
99
+ // so we need to ignore the enabled state of the XRFlag component
100
+ // if(!this.enabled) return;
101
+ let res: boolean | undefined = undefined;
102
+
103
+ const flag = state as number;
104
+ if (flag && typeof flag === "number") {
105
+ console.assert(typeof flag === "number", "XRFlag.UpdateVisible: state must be a number", flag);
106
+ if (debug)
107
+ console.log(flag);
108
+ XRFlag.buffer.Mask = flag;
109
+ state = XRFlag.buffer;
110
+ }
111
+
112
+ const st = state as XRState;
113
+ if (st) {
114
+ if (debug)
115
+ console.warn(this.name, "use passed in mask", st.Mask, this.visibleIn)
116
+ res = st.Has(this.visibleIn);
117
+ }
118
+ else {
119
+ if (debug)
120
+ console.log(this.name, "use global mask")
121
+ XRState.Global.Has(this.visibleIn);
122
+ }
123
+ if (res === undefined) return;
124
+ if (res) {
125
+ if (debug)
126
+ console.log(this.name, "is visible", this.gameObject.uuid)
127
+ // this.gameObject.visible = true;
128
+ GameObject.setActive(this.gameObject, true);
129
+ } else {
130
+ if (debug)
131
+ console.log(this.name, "is not visible", this.gameObject.uuid);
132
+ const isVisible = this.gameObject.visible;
133
+ if(!isVisible) return;
134
+ this.gameObject.visible = false;
135
+ // console.trace("DISABLE", this.name);
136
+ // GameObject.setActive(this.gameObject, false);
137
+ }
138
+ }
139
+ }