Needle Engine

Changes between version 3.31.0 and 3.32.0-alpha
Files changed (83) hide show
  1. src/engine-schemes/vrUserStateBuffer.fbs +0 -0
  2. src/engine-components/api.ts +1 -1
  3. src/engine/api.ts +1 -0
  4. src/engine-components/AudioSource.ts +1 -1
  5. src/engine-components/avatar/AvatarBlink_Simple.ts +1 -1
  6. src/engine-components/ui/BaseUIComponent.ts +2 -2
  7. src/engine-components/ui/Button.ts +2 -2
  8. src/engine-components/Camera.ts +2 -3
  9. src/engine-components/Component.ts +90 -6
  10. src/engine-components/codegen/components.ts +9 -14
  11. src/engine/debug/debug_console.ts +3 -2
  12. src/engine-components/DragControls.ts +931 -176
  13. src/engine-components/Duplicatable.ts +68 -88
  14. src/engine/engine_context.ts +64 -36
  15. src/engine/engine_create_objects.ts +11 -0
  16. src/engine/engine_element_loading.ts +22 -9
  17. src/engine/engine_element_overlay.ts +17 -0
  18. src/engine/engine_element.ts +28 -3
  19. src/engine/engine_gizmos.ts +56 -16
  20. src/engine/engine_input.ts +340 -169
  21. src/engine/engine_lifecycle_api.ts +27 -3
  22. src/engine/engine_mainloop_utils.ts +27 -1
  23. src/engine/engine_networking_instantiate.ts +7 -2
  24. src/engine/engine_networking_streams.ts +3 -3
  25. src/engine/engine_physics.ts +1 -1
  26. src/engine/engine_serialization_core.ts +2 -2
  27. src/engine/engine_three_utils.ts +15 -2
  28. src/engine/engine_types.ts +19 -1
  29. src/engine/engine_utils.ts +41 -4
  30. src/engine-components/ui/EventSystem.ts +98 -126
  31. src/engine-components/ui/Graphic.ts +2 -2
  32. src/engine-components/GroundProjection.ts +7 -2
  33. src/engine-components/webxr/index.ts +1 -2
  34. src/engine-components/Interactable.ts +6 -14
  35. src/engine-components/Light.ts +3 -7
  36. src/engine/extensions/NEEDLE_techniques_webgl.ts +2 -0
  37. src/engine-components/utils/OpenURL.ts +5 -37
  38. src/engine-components/OrbitControls.ts +3 -3
  39. src/engine-components/PlayerColor.ts +17 -13
  40. src/engine-components-experimental/networking/PlayerSync.ts +108 -21
  41. src/engine-components/ui/PointerEvents.ts +48 -23
  42. src/engine-components/ui/Raycaster.ts +25 -7
  43. src/engine/codegen/register_types.ts +15 -25
  44. src/engine-components/Renderer.ts +19 -25
  45. src/engine-components/RendererLightmap.ts +2 -2
  46. src/engine-components/SceneSwitcher.ts +9 -9
  47. src/engine-components/SpectatorCamera.ts +12 -23
  48. src/engine-components/SyncedCamera.ts +1 -2
  49. src/engine-components/SyncedTransform.ts +1 -0
  50. src/engine-components/ui/Text.ts +4 -4
  51. src/engine-components/export/usdz/USDZExporter.ts +6 -78
  52. src/engine-components/export/usdz/extensions/USDZUI.ts +1 -1
  53. src/engine-components/ui/Utils.ts +2 -1
  54. src/engine-schemes/vr-user-state-buffer.ts +37 -30
  55. src/engine-components/webxr/WebARCameraBackground.ts +37 -45
  56. src/engine-components/webxr/WebARSessionRoot.ts +387 -27
  57. src/engine-components/webxr/WebXR.ts +203 -674
  58. src/engine-components/webxr/WebXRAvatar.ts +8 -299
  59. src/engine-components/webxr/WebXRController.ts +0 -1168
  60. src/engine-components/webxr/WebXRGrabRendering.ts +0 -151
  61. src/engine-components/webxr/WebXRImageTracking.ts +63 -71
  62. src/engine-components/webxr/WebXRPlaneTracking.ts +52 -45
  63. src/engine-components/webxr/WebXRRig.ts +43 -8
  64. src/engine-components/webxr/WebXRSync.ts +0 -463
  65. src/engine-components/XRFlag.ts +0 -139
  66. src/engine-schemes/README.md +2 -0
  67. src/engine-components/webxr/Avatar.ts +220 -0
  68. src/engine/engine_xr.ts +2 -0
  69. src/engine/xr/index.ts +5 -0
  70. src/engine/xr/internal.ts +34 -0
  71. src/engine/xr/NeedleXRController.ts +558 -0
  72. src/engine/xr/NeedleXRSession.ts +1168 -0
  73. src/engine/xr/NeedleXRSync.ts +221 -0
  74. src/engine-components/webxr/TeleportTarget.ts +9 -0
  75. src/engine/xr/TempXRContext.ts +182 -0
  76. src/engine-components/webxr/types.ts +4 -0
  77. src/engine/xr/utils.ts +39 -0
  78. src/engine-components/webxr/WebXRButtons.ts +265 -0
  79. src/engine-components/webxr/controllers/XRControllerFollow.ts +58 -0
  80. src/engine-components/webxr/controllers/XRControllerModel.ts +252 -0
  81. src/engine-components/webxr/controllers/XRControllerMovement.ts +282 -0
  82. src/engine-components/webxr/XRFlag.ts +143 -0
  83. src/engine/xr/XRRig.ts +9 -0
src/engine-schemes/vrUserStateBuffer.fbs CHANGED
File without changes
src/engine-components/api.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  // We dont want to export everything in the extensions
5
5
  export * from "./js-extensions/RGBAColor.js";
6
6
  export * from "./js-extensions/Object3D.js";
7
- export * from "./XRFlag.js"
7
+ export * from "./webxr/XRFlag.js"
8
8
 
9
9
  export * from "./export/index.js"
10
10
  export * from "./postprocessing/index.js"
src/engine/api.ts CHANGED
@@ -47,6 +47,7 @@
47
47
  export * from "./engine_utils_screenshot.js";
48
48
  export * from "./engine_web_api.js";
49
49
  export * from "./engine_utils.js";
50
+ export * from "./engine_xr.js";
50
51
 
51
52
  export { TypeStore, registerType } from "./engine_typestore.js";
52
53
 
src/engine-components/AudioSource.ts CHANGED
@@ -411,7 +411,7 @@
411
411
  this._hasEnded = true;
412
412
  if (debug)
413
413
  console.log("Audio clip ended", this.clip);
414
- this.sound.dispatchEvent({ type: 'ended', target: this });
414
+ this.dispatchEvent(new CustomEvent("ended", { detail: this }));
415
415
  }
416
416
 
417
417
  // this.gameObject.position.x = Math.sin(time.time) * 2;
src/engine-components/avatar/AvatarBlink_Simple.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Object3D } from "three";
2
2
  import { Behaviour, GameObject } from "../Component.js";
3
- import { XRFlag, XRState } from "../XRFlag.js";
3
+ import { XRFlag, XRState } from "../webxr/XRFlag.js";
4
4
  import { serializable } from "../../engine/engine_serialization_decorator.js";
5
5
 
6
6
 
src/engine-components/ui/BaseUIComponent.ts CHANGED
@@ -38,7 +38,7 @@
38
38
  EventSystem.markUIDirty(this.context);
39
39
  }
40
40
 
41
- shadowComponent: ThreeMeshUI.Block | null = null;
41
+ shadowComponent: Object3D | null = null;
42
42
 
43
43
  private _controlsChildLayout = true;
44
44
  get controlsChildLayout(): boolean { return this._controlsChildLayout; }
@@ -148,7 +148,7 @@
148
148
  // })
149
149
  }
150
150
 
151
- protected setShadowComponentOwner(current: Object3D | null | undefined) {
151
+ protected setShadowComponentOwner(current: ThreeMeshUI.MeshUIBaseElement | Object3D | null | undefined) {
152
152
  if (!current) return;
153
153
  // TODO: only traverse our own hierarchy, we can stop if we find another owner
154
154
  if (current[$shadowDomOwner] === undefined || current[$shadowDomOwner] === this) {
src/engine-components/ui/Button.ts CHANGED
@@ -120,10 +120,10 @@
120
120
  }
121
121
 
122
122
  onPointerClick(args: PointerEventData) {
123
- if (!this.interactable || args.pointerId !== 0) return;
123
+ if (!this.interactable) return;
124
124
 
125
+ if (args.button !== 0 && args.event.pointerType === PointerType.Mouse) return;
125
126
  // 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
@@ -2,7 +2,7 @@
2
2
  import { getParam } from "../engine/engine_utils.js";
3
3
  import { serializable } from "../engine/engine_serialization_decorator.js";
4
4
  import { RGBAColor } from "./js-extensions/RGBAColor.js";
5
- import { Context, XRSessionMode } from "../engine/engine_setup.js";
5
+ import { Context } from "../engine/engine_setup.js";
6
6
  import type { ICamera } from "../engine/engine_types.js"
7
7
  import { isDevEnvironment, showBalloonMessage, showBalloonWarning } from "../engine/debug/index.js";
8
8
  import { getWorldPosition } from "../engine/engine_three_utils.js";
@@ -350,7 +350,6 @@
350
350
  if (this._backgroundBlurriness !== undefined)
351
351
  this.context.scene.backgroundBlurriness = this._backgroundBlurriness;
352
352
  if (this._backgroundIntensity !== undefined)
353
- //@ts-ignore
354
353
  this.context.scene.backgroundIntensity = this._backgroundIntensity;
355
354
 
356
355
  break;
@@ -392,7 +391,7 @@
392
391
  if (debug)
393
392
  showBalloonMessage("Environment blend mode: " + environmentBlendMode + " on " + navigator.userAgent);
394
393
  let transparent = environmentBlendMode === 'additive' || environmentBlendMode === 'alpha-blend';
395
- if (context.xrSessionMode === XRSessionMode.ImmersiveAR) {
394
+ if (context.isInAR) {
396
395
  if (environmentBlendMode === "opaque") {
397
396
  // workaround for Quest 2 returning opaque when it should be alpha-blend
398
397
  // check user agent if this is the Quest browser and return true if so
src/engine-components/Component.ts CHANGED
@@ -10,6 +10,8 @@
10
10
 
11
11
  import { Euler, Object3D, Quaternion, Scene, Vector3 } from "three";
12
12
  import { showBalloonWarning, isDevEnvironment } from "../engine/debug/index.js";
13
+ import { ControllerChangedEvt, INeedleXRSessionEventReceiver, NeedleXRControllerEventArgs, NeedleXREventArgs, NeedleXRSession } from "../engine/engine_xr.js";
14
+ import { IPointerEventHandler, PointerEventData } from "./ui/PointerEvents.js";
13
15
 
14
16
  // export interface ISerializationCallbackReceiver {
15
17
  // onBeforeSerialize?(): object | void;
@@ -294,7 +296,7 @@
294
296
  abstract set worldQuaternion(val: Quaternion);
295
297
  abstract get worldQuaternion(): Quaternion;
296
298
  abstract set worldRotation(val: Vector3);
297
- abstract get worldRotation(): Vector3;
299
+ abstract get worldRotation(): Vector3;
298
300
  abstract set worldScale(val: Vector3);
299
301
  abstract get worldScale(): Vector3;
300
302
 
@@ -305,17 +307,22 @@
305
307
 
306
308
 
307
309
 
308
- export class Component implements IComponent, EventTarget {
310
+ export abstract class Component implements IComponent, EventTarget,
311
+ Partial<INeedleXRSessionEventReceiver>,
312
+ Partial<IPointerEventHandler>
313
+ {
309
314
 
310
315
  get isComponent(): boolean { return true; }
311
316
 
312
317
  private __context: Context | undefined;
318
+ /** Use the context to get access to many Needle Engine features and use physics, timing, access the camera or scene */
313
319
  get context(): Context {
314
320
  return this.__context ?? Context.Current;
315
321
  }
316
322
  set context(context: Context) {
317
323
  this.__context = context;
318
324
  }
325
+ /** shorthand for `this.context.scene` */
319
326
  get scene(): Scene { return this.context.scene; }
320
327
 
321
328
  get layer(): number {
@@ -355,7 +362,7 @@
355
362
  return this.gameObject?.userData.hideFlags;
356
363
  }
357
364
 
358
-
365
+ /** @returns true if the object is enabled and active in the hierarchy */
359
366
  get activeAndEnabled(): boolean {
360
367
  if (this.destroyed) return false;
361
368
  if (this.__isEnabled === false) return false;
@@ -385,19 +392,27 @@
385
392
  this.gameObject[activeInHierarchyFieldName] = val;
386
393
  }
387
394
 
395
+ /** the object this component is attached to. Note that this is a threejs Object3D with some additional features */
388
396
  gameObject!: GameObject;
397
+ /** the unique identifier for this component */
389
398
  guid: string = "invalid";
399
+ /** 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) */
390
400
  sourceId?: SourceIdentifier;
391
401
  // transform: Object3D = nullObject;
392
402
 
393
403
  /** 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) */
394
404
  resolveGuids?(guidsMap: GuidsMap): void;
395
405
 
396
- /** called once when the component becomes active for the first time */
406
+ /** called once when the component becomes active for the first time (once per component)
407
+ * This is the first callback to be called */
397
408
  awake() { }
398
- /** called every time when the component gets enabled (this is invoked after awake and before start) */
409
+ /** called every time when the component gets enabled (this is invoked after awake and before start)
410
+ * or when it becomes active in the hierarchy (e.g. if a parent object or this.gameObject gets set to visible)
411
+ */
399
412
  onEnable() { }
413
+ /** called every time the component gets disabled or if a parent object (or this.gameObject) gets set to invisible */
400
414
  onDisable() { }
415
+ /** Called when the component gets destroyed */
401
416
  onDestroy() {
402
417
  this.__destroyed = true;
403
418
  }
@@ -409,11 +424,17 @@
409
424
  /** Called for all scripts when the context gets paused or unpaused */
410
425
  onPausedChanged?(isPaused: boolean, wasPaused: boolean): void;
411
426
 
427
+ /** called at the beginning of a frame (once per component) */
412
428
  start?(): void;
429
+ /** first callback in a frame (called every frame when implemented) */
413
430
  earlyUpdate?(): void;
431
+ /** regular callback in a frame (called every frame when implemented) */
414
432
  update?(): void;
433
+ /** late callback in a frame (called every frame when implemented) */
415
434
  lateUpdate?(): void;
435
+ /** called before the scene gets rendered in the main update loop */
416
436
  onBeforeRender?(frame: XRFrame | null): void;
437
+ /** called after the scene was rendered */
417
438
  onAfterRender?(): void;
418
439
 
419
440
  onCollisionEnter?(col: Collision);
@@ -424,18 +445,79 @@
424
445
  onTriggerStay?(col: ICollider);
425
446
  onTriggerExit?(col: ICollider);
426
447
 
448
+
449
+ /** Optional callback, you can implement this to only get callbacks for VR or AR sessions if necessary.
450
+ * @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)
451
+ */
452
+ supportsXR?(mode: XRSessionMode): boolean;
453
+ /** Called before the XR session is requested. Use this callback if you want to modify the session init features */
454
+ onBeforeXR?(mode: XRSessionMode, args: XRSessionInit): void;
455
+ /** Callback when this component joins a xr session (or becomes active in a running XR session) */
456
+ onEnterXR?(args: NeedleXREventArgs): void;
457
+ /** Callback when a xr session updates (while it is still active in XR session) */
458
+ onUpdateXR?(args: NeedleXREventArgs): void;
459
+ /** Callback when this component exists a xr session (or when it becomes inactive in a running XR session) */
460
+ onLeaveXR?(args: NeedleXREventArgs): void;
461
+ /** Callback when a controller is connected/added while in a XR session
462
+ * OR when the component joins a running XR session that has already connected controllers
463
+ * OR when the component becomes active during a running XR session that has already connected controllers */
464
+ onXRControllerAdded?(args: NeedleXRControllerEventArgs): void;
465
+ /** callback when a controller is removed while in a XR session
466
+ * OR when the component becomes inactive during a running XR session
467
+ */
468
+ onXRControllerRemoved?(args: NeedleXRControllerEventArgs): void;
469
+
470
+
471
+ /* IPointerEventReceiver */
472
+ /* @inheritdoc */
473
+ onPointerEnter?(args: PointerEventData);
474
+ onPointerMove?(args: PointerEventData);
475
+ onPointerExit?(args: PointerEventData);
476
+ onPointerDown?(args: PointerEventData);
477
+ onPointerUp?(args: PointerEventData);
478
+ onPointerClick?(args: PointerEventData);
479
+
480
+
481
+ /** starts a coroutine (javascript generator function)
482
+ * `yield` will wait for the next frame:
483
+ * - Use `yield WaitForSeconds(1)` to wait for 1 second.
484
+ * - Use `yield WaitForFrames(10)` to wait for 10 frames.
485
+ * - Use `yield new Promise(...)` to wait for a promise to resolve.
486
+ * @param routine generator function to start
487
+ * @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)
488
+ * @returns the generator function (use it to stop the coroutine with `stopCoroutine`)
489
+ * @example
490
+ * ```ts
491
+ * onEnable() { this.startCoroutine(this.myCoroutine()); }
492
+ * private *myCoroutine() {
493
+ * while(this.activeAndEnabled) {
494
+ * console.log("Hello World", this.context.time.frame);
495
+ * // wait for 5 frames
496
+ * for(let i = 0; i < 5; i++) yield;
497
+ * }
498
+ * }
499
+ * ```
500
+ */
427
501
  startCoroutine(routine: Generator, evt: FrameEvent = FrameEvent.Update): Generator {
428
502
  return this.context.registerCoroutineUpdate(this, routine, evt);
429
503
  }
430
-
504
+ /**
505
+ * Stop a coroutine that was previously started with `startCoroutine`
506
+ * @param routine the routine to be stopped
507
+ * @param evt the frame event to unregister the routine from (default: FrameEvent.Update)
508
+ */
431
509
  stopCoroutine(routine: Generator, evt: FrameEvent = FrameEvent.Update): void {
432
510
  this.context.unregisterCoroutineUpdate(routine, evt);
433
511
  }
434
512
 
513
+ /** @returns true if this component was destroyed (`this.destroy()`) or the whole object this component was part of */
435
514
  public get destroyed(): boolean {
436
515
  return this.__destroyed;
437
516
  }
438
517
 
518
+ /**
519
+ * Destroys this component (and removes it from the object)
520
+ */
439
521
  public destroy() {
440
522
  if (this.__destroyed) return;
441
523
  this.__internalDestroy();
@@ -666,5 +748,7 @@
666
748
  }
667
749
  }
668
750
 
751
+
752
+
669
753
  export class Behaviour extends Component {
670
754
  }
src/engine-components/codegen/components.ts CHANGED
@@ -11,11 +11,11 @@
11
11
  export { Animator } from "../Animator.js";
12
12
  export { AnimatorController } from "../AnimatorController.js";
13
13
  export { Antialiasing } from "../postprocessing/Effects/Antialiasing.js";
14
- export { AttachedObject } from "../webxr/WebXRController.js";
15
14
  export { AudioExtension } from "../export/usdz/extensions/behavior/AudioExtension.js";
16
15
  export { AudioListener } from "../AudioListener.js";
17
16
  export { AudioSource } from "../AudioSource.js";
18
17
  export { AudioTrackHandler } from "../timeline/TimelineTracks.js";
18
+ export { Avatar } from "../webxr/Avatar.js";
19
19
  export { Avatar_Brain_LookAt } from "../avatar/Avatar_Brain_LookAt.js";
20
20
  export { Avatar_MouthShapes } from "../avatar/Avatar_MouthShapes.js";
21
21
  export { Avatar_MustacheShake } from "../avatar/Avatar_MustacheShake.js";
@@ -51,7 +51,6 @@
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";
55
54
  export { ContactShadows } from "../ContactShadows.js";
56
55
  export { ControlTrackHandler } from "../timeline/TimelineTracks.js";
57
56
  export { CustomBranding } from "../export/usdz/USDZExporter.js";
@@ -88,7 +87,6 @@
88
87
  export { Image } from "../ui/Image.js";
89
88
  export { InheritVelocityModule } from "../ParticleSystemModules.js";
90
89
  export { InputField } from "../ui/InputField.js";
91
- export { Interactable } from "../Interactable.js";
92
90
  export { Light } from "../Light.js";
93
91
  export { LimitVelocityOverLifetimeModule } from "../ParticleSystemModules.js";
94
92
  export { LODGroup } from "../LODGroup.js";
@@ -102,6 +100,7 @@
102
100
  export { MeshRenderer } from "../Renderer.js";
103
101
  export { MinMaxCurve } from "../ParticleSystemModules.js";
104
102
  export { MinMaxGradient } from "../ParticleSystemModules.js";
103
+ export { NeedleWebXRHtmlElement } from "../webxr/WebXRButtons.js";
105
104
  export { NestedGltf } from "../NestedGltf.js";
106
105
  export { Networking } from "../Networking.js";
107
106
  export { NoiseModule } from "../ParticleSystemModules.js";
@@ -125,7 +124,6 @@
125
124
  export { PreliminaryAction } from "../export/usdz/extensions/behavior/BehaviourComponents.js";
126
125
  export { PreliminaryTrigger } from "../export/usdz/extensions/behavior/BehaviourComponents.js";
127
126
  export { RawImage } from "../ui/Image.js";
128
- export { Raycaster } from "../ui/Raycaster.js";
129
127
  export { Rect } from "../ui/RectTransform.js";
130
128
  export { RectTransform } from "../ui/RectTransform.js";
131
129
  export { ReflectionProbe } from "../ReflectionProbe.js";
@@ -153,6 +151,7 @@
153
151
  export { SizeOverLifetimeModule } from "../ParticleSystemModules.js";
154
152
  export { SkinnedMeshRenderer } from "../Renderer.js";
155
153
  export { SmoothFollow } from "../SmoothFollow.js";
154
+ export { SpatialGrabRaycaster } from "../ui/Raycaster.js";
156
155
  export { SpatialHtml } from "../ui/SpatialHtml.js";
157
156
  export { SpatialTrigger } from "../SpatialTrigger.js";
158
157
  export { SpatialTriggerReceiver } from "../SpatialTrigger.js";
@@ -167,7 +166,7 @@
167
166
  export { SyncedRoom } from "../SyncedRoom.js";
168
167
  export { SyncedTransform } from "../SyncedTransform.js";
169
168
  export { TapGestureTrigger } from "../export/usdz/extensions/behavior/BehaviourComponents.js";
170
- export { TeleportTarget } from "../webxr/WebXRController.js";
169
+ export { TeleportTarget } from "../webxr/TeleportTarget.js";
171
170
  export { TestRunner } from "../TestRunner.js";
172
171
  export { TestSimulateUserData } from "../TestRunner.js";
173
172
  export { Text } from "../ui/Text.js";
@@ -197,20 +196,16 @@
197
196
  export { Volume } from "../postprocessing/Volume.js";
198
197
  export { VolumeParameter } from "../postprocessing/VolumeParameter.js";
199
198
  export { VolumeProfile } from "../postprocessing/VolumeProfile.js";
200
- export { VRUserState } from "../webxr/WebXRSync.js";
201
- export { WebAR } from "../webxr/WebXR.js";
202
199
  export { WebARCameraBackground } from "../webxr/WebARCameraBackground.js";
203
200
  export { WebARSessionRoot } from "../webxr/WebARSessionRoot.js";
204
201
  export { WebXR } from "../webxr/WebXR.js";
205
- export { WebXRAvatar } from "../webxr/WebXRAvatar.js";
206
- export { WebXRController } from "../webxr/WebXRController.js";
207
202
  export { WebXRImageTracking } from "../webxr/WebXRImageTracking.js";
208
203
  export { WebXRImageTrackingModel } from "../webxr/WebXRImageTracking.js";
209
204
  export { WebXRPlaneTracking } from "../webxr/WebXRPlaneTracking.js";
210
- export { WebXRSync } from "../webxr/WebXRSync.js";
211
205
  export { WebXRTrackedImage } from "../webxr/WebXRImageTracking.js";
212
- export { XRFlag } from "../XRFlag.js";
213
- export { XRGrabModel } from "../webxr/WebXRGrabRendering.js";
214
- export { XRGrabRendering } from "../webxr/WebXRGrabRendering.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";
215
210
  export { XRRig } from "../webxr/WebXRRig.js";
216
- export { XRState } from "../XRFlag.js";
211
+ export { XRState } from "../webxr/XRFlag.js";
src/engine/debug/debug_console.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { getErrorCount } from "./debug_overlay.js";
2
- import { getParam, isMobileDevice } from "../engine_utils.js";
2
+ import { getParam, isMobileDevice, isQuest } from "../engine_utils.js";
3
3
  import { isLocalNetwork } from "../engine_networking_utils.js";
4
+ import { isDevEnvironment } from "./debug.js";
4
5
 
5
6
  let consoleInstance: any = null;
6
7
  let consoleHtmlElement: HTMLElement | null = null;
@@ -22,7 +23,7 @@
22
23
  currentUrl.searchParams.set("console", "1");
23
24
  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());
24
25
  }
25
- const isMobile = isMobileDevice();
26
+ const isMobile = isMobileDevice() || (isQuest() && isDevEnvironment());
26
27
  if (isMobile) {
27
28
  beginWatchingLogs();
28
29
  createConsole(true);
src/engine-components/DragControls.ts CHANGED
@@ -1,104 +1,125 @@
1
- import { GameObject } from "./Component.js";
1
+ import { Behaviour, GameObject } from "./Component.js";
2
2
  import { SyncedTransform } from "./SyncedTransform.js";
3
- import type { IPointerDownHandler, IPointerEnterHandler, IPointerEventHandler, IPointerExitHandler, IPointerUpHandler, PointerEventData } from "./ui/PointerEvents.js";
3
+ import type { IPointerEventHandler, PointerEventData } from "./ui/PointerEvents.js";
4
4
  import { Context } from "../engine/engine_setup.js";
5
- import { Interactable, UsageMarker } from "./Interactable.js";
5
+ import { UsageMarker } from "./Interactable.js";
6
6
  import { Rigidbody } from "./RigidBody.js";
7
- import { WebXR } from "./webxr/WebXR.js";
8
7
  import { Avatar_POI } from "./avatar/Avatar_Brain_LookAt.js";
9
8
  import { RaycastOptions } from "../engine/engine_physics.js";
10
9
  import { getWorldPosition, setWorldPosition } from "../engine/engine_three_utils.js";
11
- import type { KeyCode } from "../engine/engine_input.js";
12
- import { nameofFactory } from "../engine/engine_utils.js";
13
10
  import { InstancingUtil } from "../engine/engine_instancing.js";
14
11
  import { OrbitControls } from "./OrbitControls.js";
15
- import { BufferGeometry, Camera, Color, Line, LineBasicMaterial, Matrix4, Mesh, MeshBasicMaterial, Object3D, Plane, Ray, Raycaster, SphereGeometry, Vector2, Vector3 } from "three";
12
+ import { AxesHelper, Box3, BufferGeometry, Camera, Color, Event, Line, LineBasicMaterial, Matrix3, Matrix4, Mesh, MeshBasicMaterial, Object3D, Plane, PlaneHelper, Quaternion, Ray, Raycaster, SphereGeometry, Vector3 } from "three";
16
13
  import { ObjectRaycaster } from "./ui/Raycaster.js";
17
14
  import { serializable } from "../engine/engine_serialization_decorator.js";
15
+ import { IGameObject } from "../engine/engine_types.js";
16
+ import { Mathf } from "../engine/engine_math.js";
17
+ import { getParam } from "../engine/engine_utils.js";
18
+ import { Gizmos } from "../engine/engine_gizmos.js";
19
+ import { NeedleXRSession } from "../engine/engine_xr.js";
18
20
 
19
- const debug = false;
21
+ const debug = getParam("debugdrag");
20
22
 
21
- export enum DragEvents {
22
- SelectStart = "selectstart",
23
- SelectEnd = "selectend",
23
+ export enum DragMode {
24
+ /** Object stays at the same horizontal plane as it started. Commonly used for objects on the floor */
25
+ XZPlane = 0,
26
+ /** 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. */
27
+ Attached = 1,
28
+ /** Object is dragged along the initial raycast hit normal. */
29
+ HitNormal = 2,
30
+ /** Combination of XZ and Screen based on the viewing angle. Low angles result in Screen dragging and higher angles in XZ dragging. */
31
+ DynamicViewAngle = 3,
32
+ /** The drag plane is adjusted dynamically while dragging. */
33
+ SnapToSurfaces = 4,
24
34
  }
25
35
 
26
- interface SelectArgs {
27
- selected: Object3D;
28
- attached: Object3D | GameObject | null;
29
- }
36
+ export class DragControls extends Behaviour implements IPointerEventHandler {
30
37
 
38
+ // dragPlane (floor, object, view)
39
+ // snap to surface (snap orientation?)
40
+ // two-handed drag (scale, rotate, move)
41
+ // keep upright (no tilt)
31
42
 
32
- export interface IDragEventListener {
33
- onDragStart?();
34
- onDragEnd?();
35
- }
43
+ /** How and where the object is dragged along. */
44
+ @serializable()
45
+ public dragMode: DragMode = DragMode.DynamicViewAngle;
36
46
 
37
- export class DragControls extends Interactable implements IPointerEventHandler {
47
+ /** Snap dragged objects to a XYZ grid – 0 means: no snapping. */
48
+ @serializable()
49
+ public snapGridResolution: number = 0.0;
50
+
51
+ /** Keep the original rotation of the dragged object. */
52
+ @serializable()
53
+ public keepRotation: boolean = true;
54
+
55
+ /** How and where the object is dragged along while dragging in XR. */
56
+ @serializable()
57
+ public xrDragMode: DragMode = DragMode.Attached;
38
58
 
39
- private static _active: number = 0;
40
- public static get HasAnySelected(): boolean { return this._active > 0; }
59
+ /** Keep the original rotation of the dragged object while dragging in XR. */
60
+ @serializable()
61
+ public xrKeepRotation: boolean = false;
41
62
 
42
- /** Show's drag gizmos when enabled */
63
+ /** Accelerate dragging objects closer / further away when in XR */
43
64
  @serializable()
44
- public showGizmo: boolean = true;
65
+ public xrDistanceDragFactor: number = 1;
45
66
 
46
- /** When enabled DragControls will drag vertically when the object is viewed from a low angle */
67
+ /** When enabled, draws a line from the dragged object downwards to the next raycast hit. */
47
68
  @serializable()
48
- public useViewAngle: boolean = true;
69
+ public showGizmo: boolean = false;
49
70
 
50
- public transformSelf: boolean = true;
51
- // public transformGroup: boolean = true;
52
- // public targets: Object3D[] | null = null;
71
+ // future:
72
+ // constraints?
53
73
 
54
- // private controls: Control | null = null;
74
+ public static get HasAnySelected(): boolean { return this._active > 0; }
75
+ private static _active: number = 0;
76
+
77
+ /** The object to be dragged – we pass this to handlers when they are created */
78
+ private targetObject: GameObject | null = null;
55
79
  private orbit: OrbitControls | null = null;
80
+ private _dragHelper: LegacyDragVisualsHelper | null = null;
81
+ private static lastHovered: Object3D;
82
+ private _draggingRigidbodies: Rigidbody[] = [];
83
+ private _potentialDragStartEvt: PointerEventData | null = null;
84
+ private _dragHandlers: Map<Object3D, IDragHandler> = new Map();
85
+ private _totalMovement: Vector3 = new Vector3();
86
+ /** A marker is attached to components that are currently interacted with, to e.g. prevent them from being deleted. */
87
+ private _marker: UsageMarker | null = null;
88
+ private _isDragging: boolean = false;
89
+ private _didDrag: boolean = false;
56
90
 
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();
91
+ setTargetObject(obj: Object3D | null) {
92
+ this.targetObject = obj as GameObject;
93
+ for (const handler of this._dragHandlers.values()) {
94
+ handler.setTargetObject(obj);
95
+ }
66
96
  }
67
97
 
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
- }
98
+ awake() {
99
+ // initialize all data that may be cloned incorrectly otherwise
100
+ this._potentialDragStartEvt = null;
101
+ this._dragHandlers = new Map();
102
+ this._totalMovement = new Vector3();
103
+ this._marker = null;
104
+ this._isDragging = false;
105
+ this._didDrag = false;
106
+ this._dragHelper = null;
107
+ this._draggingRigidbodies = [];
79
108
  }
80
109
 
81
-
82
-
83
110
  start() {
84
111
  this.orbit = GameObject.findObjectOfType(OrbitControls, this.context);
85
- if (!this.gameObject.getComponentInParent(ObjectRaycaster)) {
112
+ if (!this.gameObject.getComponentInParent(ObjectRaycaster))
86
113
  this.gameObject.addNewComponent(ObjectRaycaster);
87
- }
88
114
  }
89
115
 
90
- private static lastHovered: Object3D;
91
- private _draggingRigidbodies: Rigidbody[] = [];
92
-
93
116
  private allowEdit(_obj: Object3D | null = null) {
94
117
  return this.context.connection.allowEditing;
95
118
  }
96
119
 
97
120
  onPointerEnter(evt: PointerEventData) {
98
121
  if (!this.allowEdit(this.gameObject)) return;
99
- if (WebXR.IsInWebXR) return;
100
- // const interactable = GameObject.getComponentInParent(evt.object, Interactable);
101
- // if (!interactable) return;
122
+ if (evt.mode !== "screen") return;
102
123
  const dc = GameObject.getComponentInParent(evt.object, DragControls);
103
124
  if (!dc || dc !== this) return;
104
125
  DragControls.lastHovered = evt.object;
@@ -107,83 +128,120 @@
107
128
 
108
129
  onPointerExit(evt: PointerEventData) {
109
130
  if (!this.allowEdit(this.gameObject)) return;
110
- if (WebXR.IsInWebXR) return;
131
+ if (evt.mode !== "screen") return;
111
132
  if (DragControls.lastHovered !== evt.object) return;
112
- // const interactable = GameObject.getComponentInParent(evt.object, Interactable);
113
- // if (!interactable) return;
114
133
  this.context.domElement.style.cursor = 'auto';
115
134
  }
116
135
 
117
- private _waitingForDragStart: PointerEventData | null = null;
118
-
119
136
  onPointerDown(args: PointerEventData) {
120
137
  if (!this.allowEdit(this.gameObject)) return;
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;
138
+ DragControls.lastHovered = args.object;
139
+
140
+ if (args.button === 0) {
141
+ if (this._dragHandlers.size === 0) {
142
+ this._didDrag = false;
143
+ this._totalMovement.set(0, 0, 0);
144
+ this._potentialDragStartEvt = args;
145
+ }
146
+
147
+ DragControls._active += 1;
148
+
149
+ const newDragHandler = new DragPointerHandler(this, this.targetObject || this.gameObject);
150
+ this._dragHandlers.set(args.event.space, newDragHandler);
151
+
152
+ // We need to turn off OrbitControls immediately, otherwise they still get data for a short moment
153
+ // and they don't properly handle being disabled while already processing data (smoothing happens when enabling again)
154
+ if (this.orbit) this.orbit.enabled = false;
155
+
156
+ newDragHandler.onDragStart(args);
157
+
158
+ if (this._dragHandlers.size === 2) {
159
+ const iterator = this._dragHandlers.values();
160
+ const a = iterator.next().value;
161
+ const b = iterator.next().value;
162
+ const mtHandler = new MultiTouchDragHandler(this, this.targetObject || this.gameObject, a, b);
163
+ this._dragHandlers.set(this.gameObject, mtHandler);
164
+
165
+ mtHandler.onDragStart(args);
166
+ }
167
+
168
+ args.use();
169
+ }
130
170
  }
131
171
 
132
172
  onPointerMove(args: PointerEventData) {
133
- if(this._isDragging || this._waitingForDragStart !== null) args.use();
173
+ if (this._isDragging || this._potentialDragStartEvt !== null) args.use();
134
174
  }
135
175
 
136
176
  onPointerUp(args: PointerEventData) {
137
- this._waitingForDragStart = null;
177
+
178
+ if(debug) Gizmos.DrawLabel(args.point ?? this.gameObject.worldPosition, "POINTERUP:" + args.pointerId + ", " + args.button, .03, 3);
179
+
138
180
  if (!this.allowEdit(this.gameObject)) return;
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;
181
+ if (args.button !== 0) return;
182
+ this._potentialDragStartEvt = null;
183
+
184
+ const handler = this._dragHandlers.get(args.event.space);
185
+ const mtHandler = this._dragHandlers.get(this.gameObject) as MultiTouchDragHandler;
186
+ if (mtHandler && (mtHandler.handlerA === handler || mtHandler.handlerB === handler)) {
187
+ // any of the two handlers has been released, so we can remove the multi-touch handler
188
+ this._dragHandlers.delete(this.gameObject);
189
+ mtHandler.onDragEnd(args);
190
+ }
191
+
192
+ if (handler) {
193
+ if (DragControls._active > 0)
194
+ DragControls._active -= 1;
195
+
196
+ if (handler.onDragEnd) handler.onDragEnd(args);
197
+ this._dragHandlers.delete(args.event.space);
198
+
199
+ if (this._dragHandlers.size === 0) {
200
+ this.onLastDragEnd(args);
201
+ }
202
+ args.use();
203
+ }
204
+
205
+ if (DragControls._active === 0) {
206
+ if (this.orbit) this.orbit.enabled = true;
207
+ }
145
208
  }
146
209
 
147
-
148
210
  update(): void {
149
- if (WebXR.IsInWebXR) return;
150
211
 
212
+ for (const handler of this._dragHandlers.values()) {
213
+ if (handler.collectMovementInfo) handler.collectMovementInfo();
214
+ // TODO this doesn't make sense, we should instead just use the max here
215
+ // or even better, each handler can decide on their own how to handle this
216
+ if (handler.getTotalMovement) this._totalMovement.add(handler.getTotalMovement());
217
+ }
218
+
151
219
  // drag start only after having dragged for some pixels
152
- if (this._waitingForDragStart) {
220
+ if (this._potentialDragStartEvt) {
153
221
  if (!this._didDrag) {
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)
222
+ // this is so we can e.g. process clicks without having a drag change the position, e.g. a click to call a method.
223
+ // TODO probably needs to be treated differently for spatial (3D motion) and screen (2D pixel motion) drags
224
+ if (this._totalMovement.length() > 0.0003)
160
225
  this._didDrag = true;
161
226
  else return;
162
227
  }
163
- const args = this._waitingForDragStart;
164
- this._waitingForDragStart = null;
165
- this.onDragStart(args);
228
+ const args = this._potentialDragStartEvt;
229
+ this._potentialDragStartEvt = null;
230
+ this.onFirstDragStart(args);
166
231
  }
167
232
 
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
- }
233
+ for (const handler of this._dragHandlers.values())
234
+ if (handler.onDragUpdate) handler.onDragUpdate(this._dragHandlers.size);
235
+
236
+ if (this._dragHelper && this._dragHelper.hasSelected)
237
+ this.onAnyDragUpdate();
175
238
  }
176
239
 
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) {
240
+ /** Called when the first pointer starts dragging on this object. Not called for subsequent pointers on the same object. */
241
+ private onFirstDragStart(evt: PointerEventData) {
184
242
  if (!this._dragHelper) {
185
243
  if (this.context.mainCamera)
186
- this._dragHelper = new DragHelper(this.context.mainCamera);
244
+ this._dragHelper = new LegacyDragVisualsHelper(this.context.mainCamera);
187
245
  else
188
246
  return;
189
247
  }
@@ -192,46 +250,17 @@
192
250
  const dc = GameObject.getComponentInParent(evt.object, DragControls);
193
251
  if (!dc || dc !== this) return;
194
252
 
253
+ const object = this.targetObject || this.gameObject;
195
254
 
196
- let object: Object3D = evt.object;
255
+ if (!object) return;
197
256
 
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;
228
257
  this._isDragging = true;
229
258
  this._dragHelper.setSelected(object, this.context);
230
259
  if (this.orbit) this.orbit.enabled = false;
231
260
 
232
261
  const sync = GameObject.getComponentInChildren(object, SyncedTransform);
233
- if (debug)
234
- console.log("DRAG START", sync, object);
262
+ if (debug) console.log("DRAG START", sync, object);
263
+
235
264
  if (sync) {
236
265
  sync.fastMode = true;
237
266
  sync?.requestOwnership();
@@ -239,30 +268,31 @@
239
268
 
240
269
  this._marker = GameObject.addNewComponent(object, UsageMarker);
241
270
 
242
- // console.log(object, this._marker);
243
-
244
271
  this._draggingRigidbodies.length = 0;
245
272
  const rbs = GameObject.getComponentsInChildren(object, Rigidbody);
246
273
  if (rbs)
247
274
  this._draggingRigidbodies.push(...rbs);
248
-
249
- const l = nameofFactory<IDragEventListener>();
250
- GameObject.invokeOnChildren(this._dragHelper.selected, l("onDragStart"));
251
275
  }
252
276
 
253
- private onUpdateDrag() {
277
+ /** Called each frame as long as any pointer is dragging this object. */
278
+ private onAnyDragUpdate() {
254
279
  if (!this._dragHelper) return;
255
280
  this._dragHelper.showGizmo = this.showGizmo;
256
- this._dragHelper.useViewAngle = this.useViewAngle;
257
281
 
258
282
  this._dragHelper.onUpdate(this.context);
259
283
  for (const rb of this._draggingRigidbodies) {
260
284
  rb.wakeUp();
261
285
  rb.resetVelocities();
286
+ rb.resetForcesAndTorques();
262
287
  }
288
+
289
+ const object = this.targetObject || this.gameObject;
290
+
291
+ InstancingUtil.markDirty(object);
263
292
  }
264
293
 
265
- private onDragEnd(evt: PointerEventData | null) {
294
+ /** Called when the last pointer has been removed from this object. */
295
+ private onLastDragEnd(evt: PointerEventData | null) {
266
296
  if (!this || !this._isDragging) return;
267
297
  this._isDragging = false;
268
298
  if (!this._dragHelper) return;
@@ -271,8 +301,7 @@
271
301
  }
272
302
  this._draggingRigidbodies.length = 0;
273
303
  const selected = this._dragHelper.selected;
274
- if (debug)
275
- console.log("DRAG END", selected, selected?.visible)
304
+ if (debug) console.log("DRAG END", selected, selected?.visible)
276
305
  this._dragHelper.setSelected(null, this.context);
277
306
  if (this.orbit) this.orbit.enabled = true;
278
307
  if (evt?.object) {
@@ -282,23 +311,751 @@
282
311
  // sync?.requestOwnership();
283
312
  }
284
313
  }
285
- if (this._marker) {
314
+ if (this._marker)
286
315
  this._marker.destroy();
316
+ }
317
+ }
318
+
319
+ /** Handles two touch points affecting one object. Allows movement, scale and rotation of objects. */
320
+ class MultiTouchDragHandler implements IDragHandler {
321
+
322
+ handlerA: DragPointerHandler;
323
+ handlerB: DragPointerHandler;
324
+
325
+ private context: Context;
326
+ private settings: DragControls;
327
+ private gameObject: GameObject;
328
+ private _handlerAAttachmentPoint: Vector3 = new Vector3();
329
+ private _handlerBAttachmentPoint: Vector3 = new Vector3();
330
+
331
+ private _followObject: GameObject;
332
+ private _manipulatorObject: GameObject;
333
+ private _deviceMode!: XRTargetRayMode;
334
+ private _followObjectStartWorldQuaternion: Quaternion = new Quaternion();
335
+
336
+ constructor(dragControls: DragControls, gameObject: GameObject, pointerA: DragPointerHandler, pointerB: DragPointerHandler) {
337
+ this.context = dragControls.context;
338
+ this.settings = dragControls;
339
+ this.gameObject = gameObject;
340
+ this.handlerA = pointerA;
341
+ this.handlerB = pointerB;
342
+
343
+ this._followObject = new Object3D() as GameObject;
344
+ this._manipulatorObject = new Object3D() as GameObject;
345
+
346
+ this.context.scene.add(this._manipulatorObject);
347
+
348
+ const rig = NeedleXRSession.active?.rig?.gameObject;
349
+
350
+ if (!this.handlerA || !this.handlerB || !this.handlerA.hitPointInLocalSpace || !this.handlerB.hitPointInLocalSpace) {
351
+ console.error("Invalid: MultiTouchDragHandler needs two valid DragPointerHandlers with hitPointInLocalSpace set.");
352
+ return;
287
353
  }
288
- // raise event
289
- for (const listener of this.selectEndEventListener) {
290
- listener(this);
354
+
355
+ this._tempVec1.copy(this.handlerA.hitPointInLocalSpace);
356
+ this._tempVec2.copy(this.handlerB.hitPointInLocalSpace);
357
+ this.gameObject.localToWorld(this._tempVec1);
358
+ this.gameObject.localToWorld(this._tempVec2);
359
+ if (rig) {
360
+ rig.worldToLocal(this._tempVec1);
361
+ rig.worldToLocal(this._tempVec2);
291
362
  }
363
+ this._initialDistance = this._tempVec1.distanceTo(this._tempVec2);
364
+
365
+ if (this._initialDistance < 0.02) {
366
+ if (debug) {
367
+ console.log("Finding alternative drag attachment points since initial distance is too low: " + this._initialDistance.toFixed(2));
368
+ }
369
+ // We want two reasonable pointer attachment points here.
370
+ // But if the hitPointInLocalSpace are very close to each other, we instead fall back to controller positions.
371
+ this.handlerA.followObject.parent!.getWorldPosition(this._tempVec1);
372
+ this.handlerB.followObject.parent!.getWorldPosition(this._tempVec2);
373
+ this._handlerAAttachmentPoint.copy(this._tempVec1);
374
+ this._handlerBAttachmentPoint.copy(this._tempVec2);
375
+ this.gameObject.worldToLocal(this._handlerAAttachmentPoint);
376
+ this.gameObject.worldToLocal(this._handlerBAttachmentPoint);
377
+ this._initialDistance = this._tempVec1.distanceTo(this._tempVec2);
292
378
 
293
- const l = nameofFactory<IDragEventListener>();
294
- GameObject.invokeOnChildren(selected, l("onDragEnd"));
379
+ if (this._initialDistance < 0.001) {
380
+ console.warn("Not supported right now – controller drag points for multitouch are too close!");
381
+ this._initialDistance = 1;
382
+ }
383
+ }
384
+ else {
385
+ this._handlerAAttachmentPoint.copy(this.handlerA.hitPointInLocalSpace);
386
+ this._handlerBAttachmentPoint.copy(this.handlerB.hitPointInLocalSpace);
387
+ }
388
+
389
+ this._tempVec3.lerpVectors(this._tempVec1, this._tempVec2, 0.5);
390
+ this._initialScale.copy(gameObject.scale);
391
+
392
+ if (debug) {
393
+ this._followObject.add(new AxesHelper(2));
394
+ this._manipulatorObject.add(new AxesHelper(5));
395
+
396
+ const formatVec = (v: Vector3) => `${v.x.toFixed(2)}, ${v.y.toFixed(2)}, ${v.z.toFixed(2)}`;
397
+ Gizmos.DrawLine(this._tempVec1, this._tempVec2, 0x00ffff, 0, false);
398
+ Gizmos.DrawLabel(this._tempVec3, "A:B " + this._initialDistance.toFixed(2) + "\n" + formatVec(this._tempVec1) + "\n" + formatVec(this._tempVec2), 0.03, 5);
399
+ }
295
400
  }
401
+
402
+ onDragStart(_args: PointerEventData): void {
403
+ // align _followObject with the object we want to drag
404
+ this.gameObject.add(this._followObject);
405
+ this._followObject.matrixAutoUpdate = false;
406
+ this._followObject.matrix.identity();
407
+ this._deviceMode = _args.mode;
408
+ this._followObjectStartWorldQuaternion.copy(this._followObject.worldQuaternion);
409
+
410
+ // align _manipulatorObject in the same way it would if this was a drag update
411
+ this.alignManipulator();
412
+
413
+ // and then parent it to the space object so it follows along.
414
+ this._manipulatorObject.attach(this._followObject);
415
+
416
+ // store offsets in local space
417
+ this._manipulatorPosOffset.copy(this._followObject.position);
418
+ this._manipulatorRotOffset.copy(this._followObject.quaternion);
419
+ this._manipulatorScaleOffset.copy(this._followObject.scale);
420
+ }
421
+
422
+ onDragEnd(_args: PointerEventData): void {
423
+ if (!this.handlerA || !this.handlerB) {
424
+ console.error("onDragEnd called on MultiTouchDragHandler without valid handlers. This is likely a bug.");
425
+ return;
426
+ }
427
+
428
+ // we want to initialize the drag points for these handlers again.
429
+ // one of them will be removed, but we don't know here which one
430
+ this.handlerA.recenter();
431
+ this.handlerB.recenter();
432
+
433
+ // destroy helper objects
434
+ this._manipulatorObject.removeFromParent();
435
+ this._followObject.removeFromParent();
436
+ this._manipulatorObject.destroy();
437
+ this._followObject.destroy();
438
+ }
439
+
440
+ private _manipulatorPosOffset: Vector3 = new Vector3();
441
+ private _manipulatorRotOffset: Quaternion = new Quaternion();
442
+ private _manipulatorScaleOffset: Vector3 = new Vector3();
443
+
444
+ private _tempVec1: Vector3 = new Vector3();
445
+ private _tempVec2: Vector3 = new Vector3();
446
+ private _tempVec3: Vector3 = new Vector3();
447
+ private tempLookMatrix: Matrix4 = new Matrix4();
448
+ private _initialScale: Vector3 = new Vector3();
449
+ private _initialDistance: number = 0;
450
+
451
+ private alignManipulator() {
452
+ this._tempVec1.copy(this._handlerAAttachmentPoint);
453
+ this._tempVec2.copy(this._handlerBAttachmentPoint);
454
+ this.handlerA.followObject.localToWorld(this._tempVec1);
455
+ this.handlerB.followObject.localToWorld(this._tempVec2);
456
+ this._tempVec3.lerpVectors(this._tempVec1, this._tempVec2, 0.5);
457
+
458
+ this._manipulatorObject.position.copy(this._tempVec3);
459
+
460
+ // - lookAt the second point on handlerB
461
+ const camera = this.context.mainCamera;
462
+ this.tempLookMatrix.lookAt(this._tempVec3, this._tempVec2, (camera as any as IGameObject).worldUp);
463
+ this._manipulatorObject.quaternion.setFromRotationMatrix(this.tempLookMatrix);
464
+
465
+ // - scale based on the distance between the two points
466
+ const dist = this._tempVec1.distanceTo(this._tempVec2);
467
+ this._manipulatorObject.scale.copy(this._initialScale).multiplyScalar(dist / this._initialDistance);
468
+
469
+ this._manipulatorObject.updateMatrix();
470
+ this._manipulatorObject.updateMatrixWorld(true);
471
+
472
+ if (debug) {
473
+ Gizmos.DrawLabel(this._tempVec3.clone().add(new Vector3(0,0.2,0)), "A:B " + dist.toFixed(2), 0.03);
474
+ Gizmos.DrawLine(this._tempVec1, this._tempVec2, 0x00ff00, 0, false);
475
+
476
+ // const wp = this._manipulatorObject.worldPosition;
477
+ // Gizmos.DrawWireSphere(wp, this._initialScale.length() * dist / this._initialDistance, 0x00ff00, 0, false);
478
+ }
479
+ }
480
+
481
+ onDragUpdate() {
482
+ // At this point we've run both the other handlers, but their effects have been suppressed because they can't handle
483
+ // two events at the same time. They're basically providing us with two Object3D's and we can combine these here
484
+ // into a reasonable two-handed translation/rotation/scale.
485
+ // One approach:
486
+ // - position our control object on the center between the two pointer control objects
487
+
488
+ // TODO close grab needs to be handled differently because there we don't have a hit point -
489
+ // Hit point is just the center of the object
490
+ // So probably we should fix that close grab has a better hit point approximation (point on bounds?)
491
+
492
+ this.alignManipulator();
493
+
494
+ // apply (smoothed) to the gameObject
495
+ const lerpStrength = 30;
496
+ const lerpFactor = 1.0;
497
+
498
+ this._followObject.position.copy(this._manipulatorPosOffset);
499
+ this._followObject.quaternion.copy(this._manipulatorRotOffset);
500
+ this._followObject.scale.copy(this._manipulatorScaleOffset);
501
+
502
+ const draggedObject = this.gameObject;
503
+ const targetObject = this._followObject;
504
+
505
+ targetObject.updateMatrix();
506
+ targetObject.updateMatrixWorld(true);
507
+
508
+ const isSpatialInput = this._deviceMode === "tracked-pointer";
509
+ const keepRotation = isSpatialInput ? this.settings.xrKeepRotation : this.settings.keepRotation;
510
+
511
+ // TODO refactor to a common place
512
+ // apply constraints (position grid snap, rotation, ...)
513
+ if (this.settings.snapGridResolution > 0) {
514
+ const wp = this._followObject.worldPosition;
515
+ const snap = this.settings.snapGridResolution;
516
+ wp.x = Math.round(wp.x / snap) * snap;
517
+ wp.y = Math.round(wp.y / snap) * snap;
518
+ wp.z = Math.round(wp.z / snap) * snap;
519
+ this._followObject.worldPosition = wp;
520
+ this._followObject.updateMatrix();
521
+ }
522
+ if (keepRotation) {
523
+ this._followObject.worldQuaternion = this._followObjectStartWorldQuaternion;
524
+ this._followObject.updateMatrix();
525
+ }
526
+
527
+ // TODO refactor to a common place
528
+ // TODO should use unscaled time here // some test for lerp speed depending on distance
529
+ const t = Mathf.clamp01(this.context.time.deltaTime * lerpStrength * lerpFactor);// / (currentDist - 1 + 0.01));
530
+
531
+ const wp = draggedObject.worldPosition;
532
+ wp.lerp(targetObject.worldPosition, t);
533
+ draggedObject.worldPosition = wp;
534
+
535
+ const rot = draggedObject.worldQuaternion;
536
+ rot.slerp(targetObject.worldQuaternion, t);
537
+ draggedObject.worldQuaternion = rot;
538
+
539
+ const scl = draggedObject.worldScale;
540
+ scl.lerp(targetObject.worldScale, t);
541
+ draggedObject.worldScale = scl;
542
+ }
543
+
544
+ setTargetObject(obj: Object3D | null): void {
545
+ this.gameObject = obj as GameObject;
546
+ }
296
547
  }
297
548
 
549
+ /** Common interface for pointer handlers (single touch and multi touch) */
550
+ interface IDragHandler {
551
+ /** Used to determine if a drag has happened for this handler */
552
+ getTotalMovement?(): Vector3;
553
+ /** Target object can change mid-flight (e.g. in Duplicatable), handlers should react properly to that */
554
+ setTargetObject(obj: Object3D | null): void;
555
+
556
+ /** Prewarms the drag – can already move internal points around here but should not move the object itself */
557
+ collectMovementInfo?(): void;
558
+ onDragStart?(args: PointerEventData): void;
559
+ onDragEnd?(args: PointerEventData): void;
560
+ /** The target object is moved around */
561
+ onDragUpdate?(numberOfPointers: number): void;
562
+ }
298
563
 
564
+ /** Handles a single pointer on an object. DragPointerHandlers are created on pointer enter,
565
+ * help with determining if there's actually a drag, and then perform operations based on spatial pointer data.
566
+ */
567
+ class DragPointerHandler implements IDragHandler {
299
568
 
300
- class DragHelper {
569
+ /** Absolute movement of the pointer. Used for determining if a motion/drag is happening.
570
+ * This is in world units, so very small for screens (near-plane space change) */
571
+ getTotalMovement(): Vector3 { return this._totalMovement; }
572
+ get followObject(): GameObject { return this._followObject; }
573
+ get hitPointInLocalSpace(): Vector3 { return this._hitPointInLocalSpace; }
301
574
 
575
+ private context: Context;
576
+ private gameObject: GameObject;
577
+ private settings: DragControls;
578
+ private _lastRig: IGameObject | undefined = undefined;
579
+
580
+ /** This object is placed at the pivot of the dragged object, and parented to the control space. */
581
+ private _followObject: GameObject;
582
+ private _totalMovement: Vector3 = new Vector3();
583
+ /** Motion along the pointer ray. On screens this doesn't change. In XR it can be used to determine how much
584
+ * effort someone is putting into moving an object closer or further away. */
585
+ private _totalMovementAlongRayDirection: number = 0;
586
+ /** Distance between _followObject and its parent at grab start, in local space */
587
+ private _grabStartDistance: number = 0;
588
+ private _deviceMode!: XRTargetRayMode;
589
+ private _followObjectStartPosition: Vector3 = new Vector3();
590
+ private _followObjectStartQuaternion: Quaternion = new Quaternion();
591
+ private _followObjectStartWorldQuaternion: Quaternion = new Quaternion();
592
+ private _lastDragPosRigSpace: Vector3 | undefined;
593
+ private _tempVec: Vector3 = new Vector3();
594
+ private _tempMat: Matrix4 = new Matrix4();
595
+
596
+ private _hitPointInLocalSpace: Vector3 = new Vector3();
597
+ private _hitNormalInLocalSpace: Vector3 = new Vector3();
598
+ private _bottomCenter = new Vector3();
599
+ private _backCenter = new Vector3();
600
+ private _backBottomCenter = new Vector3();
601
+ private _bounds = new Box3();
602
+ private _dragPlane = new Plane(new Vector3(0, 1, 0));
603
+ private _draggedOverObject: Object3D | null = null;
604
+ private _draggedOverObjectLastSetUp: Object3D | null = null;
605
+ private _draggedOverObjectLastNormal: Vector3 = new Vector3();
606
+ private _draggedOverObjectDuration: number = 0;
607
+
608
+ /** Allows overriding which object is dragged while a drag is already ongoing. Used for example by Duplicatable */
609
+ setTargetObject(obj: Object3D | null) {
610
+ this.gameObject = obj as GameObject;
611
+ }
612
+
613
+ constructor(dragControls: DragControls, gameObject: GameObject) {
614
+ this.settings = dragControls;
615
+ this.context = dragControls.context;
616
+ this.gameObject = gameObject;
617
+ this._followObject = new Object3D() as GameObject;
618
+ }
619
+
620
+ recenter() {
621
+ if (!this._followObject.parent) {
622
+ console.warn("Error: space follow object doesn't have parent but recenter() is called. This is likely a bug");
623
+ return;
624
+ }
625
+
626
+ const p = this._followObject.parent as GameObject;
627
+
628
+ this.gameObject.add(this._followObject);
629
+ this._followObject.matrixAutoUpdate = false;
630
+
631
+ this._followObject.position.set(0, 0, 0);
632
+ this._followObject.quaternion.set(0, 0, 0, 1);
633
+ this._followObject.scale.set(1, 1, 1);
634
+
635
+ this._followObject.updateMatrix();
636
+ this._followObject.updateMatrixWorld(true);
637
+
638
+ p.attach(this._followObject);
639
+
640
+ this._followObjectStartPosition.copy(this._followObject.position);
641
+ this._followObjectStartQuaternion.copy(this._followObject.quaternion);
642
+ this._followObjectStartWorldQuaternion.copy(this._followObject.worldQuaternion);
643
+
644
+ this._followObject.updateMatrix();
645
+ this._followObject.updateMatrixWorld(true);
646
+
647
+ const hitPointWP = this._hitPointInLocalSpace.clone();
648
+ this.gameObject.localToWorld(hitPointWP);
649
+ this._grabStartDistance = hitPointWP.distanceTo(p.worldPosition);
650
+ const rig = NeedleXRSession.active?.rig?.gameObject;
651
+ const rigScale = rig?.worldScale.x || 1;
652
+ this._grabStartDistance /= rigScale;
653
+
654
+ this._totalMovementAlongRayDirection = 0;
655
+ this._lastDragPosRigSpace = undefined;
656
+
657
+ if (debug)
658
+ {
659
+ Gizmos.DrawLine(hitPointWP, p.worldPosition, 0x00ff00, 0.5, false);
660
+ Gizmos.DrawLabel(p.worldPosition.add(new Vector3(0,0.1,0)), this._grabStartDistance.toFixed(2), 0.03, 0.5);
661
+ }
662
+ }
663
+
664
+ onDragStart(args: PointerEventData) {
665
+
666
+ args.event.space.add(this._followObject);
667
+
668
+ // prepare for drag, we will start dragging after an object has been dragged for a few centimeters
669
+ this._lastDragPosRigSpace = undefined;
670
+
671
+ if (args.point && args.normal) {
672
+ this._hitPointInLocalSpace.copy(args.point);
673
+ this.gameObject.worldToLocal(this._hitPointInLocalSpace);
674
+ this._hitNormalInLocalSpace.copy(args.normal);
675
+ }
676
+ else if (args) {
677
+ // 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
678
+ // convert controller world position to local space instead and use that as hit point
679
+ const controller = args.event.space as GameObject;
680
+ const controllerWp = controller.worldPosition;
681
+ this.gameObject.worldToLocal(controllerWp);
682
+ this._hitPointInLocalSpace.copy(controllerWp);
683
+
684
+ const controllerUp = controller.worldUp;
685
+ this._tempMat.copy(this.gameObject.matrixWorld).invert();
686
+ controllerUp.transformDirection(this._tempMat);
687
+ this._hitNormalInLocalSpace.copy(controllerUp);
688
+ }
689
+
690
+ this.recenter();
691
+
692
+ this._totalMovement.set(0, 0, 0);
693
+ this._deviceMode = args.mode;
694
+
695
+
696
+ const dragSource = this._followObject.parent as IGameObject;
697
+ const rayDirection = dragSource.worldForward;
698
+
699
+ const isSpatialInput = this._deviceMode === "tracked-pointer";
700
+ const dragMode = isSpatialInput ? this.settings.xrDragMode : this.settings.dragMode;
701
+
702
+ // set up drag plane; we don't really know the normal yet but we can already set the point
703
+ const hitWP = this._hitPointInLocalSpace.clone();
704
+ this.gameObject.localToWorld(hitWP);
705
+
706
+ switch (dragMode) {
707
+ case DragMode.XZPlane:
708
+ const up = new Vector3(0,1,0);
709
+ if (this.gameObject.parent) {
710
+ // TODO in this case _dragPlane should be in parent space, not world space,
711
+ // otherwise dragging the parent and this object at the same time doesn't keep the plane constrained
712
+ up.transformDirection(this.gameObject.parent.matrixWorld.clone().invert());
713
+ }
714
+ this._dragPlane.setFromNormalAndCoplanarPoint(up, hitWP);
715
+ break;
716
+ case DragMode.HitNormal:
717
+ const hitNormal = this._hitNormalInLocalSpace.clone();
718
+ hitNormal.transformDirection(this.gameObject.matrixWorld);
719
+ this._dragPlane.setFromNormalAndCoplanarPoint(hitNormal, hitWP);
720
+ break;
721
+ case DragMode.Attached:
722
+ this._dragPlane.setFromNormalAndCoplanarPoint(rayDirection, hitWP);
723
+ break;
724
+ case DragMode.DynamicViewAngle:
725
+ const v0 = new Vector3(0, 1, 0);
726
+ const v1 = rayDirection;
727
+ const angle = v0.angleTo(v1);
728
+ const angleThreshold = 0.5;
729
+ if (angle > Math.PI / 2 + angleThreshold || angle < Math.PI / 2 - angleThreshold)
730
+ this._dragPlane.setFromNormalAndCoplanarPoint(new Vector3(0, 1, 0), hitWP);
731
+ else
732
+ this._dragPlane.setFromNormalAndCoplanarPoint(rayDirection, hitWP);
733
+ break;
734
+ }
735
+
736
+ // calculate bounding box and snapping points. We want to either snap the "back" point or the "bottom" point.
737
+ const bbox = new Box3();
738
+ const p = this.gameObject.parent;
739
+ const localP = this.gameObject.position.clone();
740
+ const localQ = this.gameObject.quaternion.clone();
741
+ const localS = this.gameObject.scale.clone();
742
+ if (p) p.remove(this.gameObject);
743
+ this.gameObject.position.set(0, 0, 0);
744
+ this.gameObject.quaternion.set(0, 0, 0, 1);
745
+ this.gameObject.scale.set(1, 1, 1);
746
+ bbox.setFromObject(this.gameObject);
747
+
748
+ // get front center point of the bbox. basically (0, 0, 1) in local space
749
+ const bboxCenter = new Vector3();
750
+ bbox.getCenter(bboxCenter);
751
+ const bboxSize = new Vector3();
752
+ bbox.getSize(bboxSize);
753
+
754
+ // attachment points for dragging
755
+ this._bottomCenter.copy(bboxCenter.clone().add(new Vector3(0, -bboxSize.y / 2, 0)));
756
+ this._backCenter.copy(bboxCenter.clone().add(new Vector3(0, 0, bboxSize.z / 2)));
757
+ this._backBottomCenter.copy(bboxCenter.clone().add(new Vector3(0, -bboxSize.y / 2, bboxSize.z / 2)));
758
+
759
+ this._bounds.copy(bbox);
760
+
761
+ // restore original transform
762
+ if (p) p.add(this.gameObject);
763
+ this.gameObject.position.copy(localP);
764
+ this.gameObject.quaternion.copy(localQ);
765
+ this.gameObject.scale.copy(localS);
766
+
767
+ // surface snapping
768
+ this._draggedOverObject = null;
769
+ this._draggedOverObjectLastSetUp = null;
770
+ this._draggedOverObjectLastNormal.set(0, 1, 0);
771
+ this._draggedOverObjectDuration = 0;
772
+ }
773
+
774
+ collectMovementInfo() {
775
+ // we're dragging - there is a controlling object
776
+ if (!this._followObject.parent) return;
777
+
778
+ // TODO This should all be handled properly per-pointer
779
+ // and we want to have a chance to react to multiple pointers being on the same object.
780
+ // some common stuff (calculating of movement offsets, etc) could be done by default
781
+ // and then the main thing to override is the actual movement of the object based on N _followObjects
782
+
783
+ const dragSource = this._followObject.parent as IGameObject;
784
+
785
+ // modify _followObject with constraints, e.g.
786
+ // - dragging on a plane, e.g. the floor (keeping the distance to the floor plane constant)
787
+ /* TODO fix jump on drag start
788
+ const p0 = this._followObject.parent as GameObject;
789
+ const ray = new Ray(p0.worldPosition, p0.worldForward.multiplyScalar(-1));
790
+ const p = new Vector3();
791
+ const t0 = ray.intersectPlane(new Plane(new Vector3(0, 1, 0)), p);
792
+ if (t0 !== null)
793
+ this._followObject.worldPosition = t0;
794
+ */
795
+
796
+ this._followObject.updateMatrix();
797
+ const dragPosRigSpace = dragSource.worldPosition;
798
+ const rig = NeedleXRSession.active?.rig?.gameObject;
799
+ if (rig)
800
+ rig.worldToLocal(dragPosRigSpace);
801
+
802
+ // sum up delta
803
+ // TODO We need to do all/most of these calculations in Rig Space instead of world space
804
+ // moving the rig while holding an object should not affect _rayDelta / _dragDelta
805
+ if (this._lastDragPosRigSpace === undefined || rig != this._lastRig) {
806
+ this._lastDragPosRigSpace = dragPosRigSpace.clone();
807
+ this._lastRig = rig;
808
+ }
809
+ this._tempVec.copy(dragPosRigSpace).sub(this._lastDragPosRigSpace);
810
+
811
+ const rayDirectionRigSpace = dragSource.worldForward;
812
+ if (rig) {
813
+ this._tempMat.copy(rig.matrixWorld).invert();
814
+ rayDirectionRigSpace.transformDirection(this._tempMat);
815
+ }
816
+ // sum up delta movement along ray
817
+ this._totalMovementAlongRayDirection += rayDirectionRigSpace.dot(this._tempVec);
818
+ this._tempVec.x = Math.abs(this._tempVec.x);
819
+ this._tempVec.y = Math.abs(this._tempVec.y);
820
+ this._tempVec.z = Math.abs(this._tempVec.z);
821
+
822
+ // sum up absolute total movement
823
+ this._totalMovement.add(this._tempVec);
824
+ this._lastDragPosRigSpace.copy(dragPosRigSpace);
825
+
826
+ if (debug) {
827
+ let wp = dragPosRigSpace;
828
+ // ray direction of the input source object
829
+ if (rig) {
830
+ wp = wp.clone();
831
+ wp.transformDirection(rig.matrixWorld);
832
+ }
833
+ Gizmos.DrawRay(wp, rayDirectionRigSpace, 0x0000ff);
834
+ }
835
+ }
836
+
837
+ onDragUpdate(numberOfPointers: number) {
838
+
839
+ // can only handle a single pointer
840
+ // if there's more, we defer to multi-touch drag handlers
841
+ if (numberOfPointers > 1) return;
842
+
843
+ const draggedObject = this.gameObject as IGameObject;
844
+ const dragSource = this._followObject.parent as IGameObject;
845
+ this._followObject.updateMatrix();
846
+ const dragSourceWP = dragSource.worldPosition;
847
+ const rayDirection = dragSource.worldForward;
848
+
849
+
850
+ // Actually move and rotate draggedObject
851
+ const isSpatialInput = this._deviceMode === "tracked-pointer";
852
+ const keepRotation = isSpatialInput ? this.settings.xrKeepRotation : this.settings.keepRotation;
853
+ const dragMode = isSpatialInput ? this.settings.xrDragMode : this.settings.dragMode;
854
+
855
+ const lerpStrength = 10;
856
+ // - keeping rotation constant during dragging
857
+ if (keepRotation) this._followObject.worldQuaternion = this._followObjectStartWorldQuaternion;
858
+ this._followObject.updateMatrix();
859
+ this._followObject.updateMatrixWorld(true);
860
+
861
+ // Acceleration for moving the object - move followObject along the ray distance by _totalMovementAlongRayDirection
862
+ let currentDist = 1.0;
863
+ let lerpFactor = 1.0;
864
+ if (this._deviceMode === "tracked-pointer" && this._grabStartDistance > 0.5) // hands and controllers, but not touches
865
+ {
866
+ const factor = 1 + this._totalMovementAlongRayDirection * (2 * this.settings.xrDistanceDragFactor);
867
+ currentDist = Math.max(0.0, factor);
868
+ currentDist = currentDist * currentDist * currentDist;
869
+ }
870
+ else if (this._grabStartDistance <= 0.5)
871
+ {
872
+ // TODO there's still a frame delay between dragged objects and the hand models
873
+ lerpFactor = 3.0;
874
+ }
875
+
876
+ // reset _followObject to its original position and rotation
877
+ this._followObject.position.copy(this._followObjectStartPosition);
878
+ if (!keepRotation)
879
+ this._followObject.quaternion.copy(this._followObjectStartQuaternion);
880
+
881
+ // TODO restore previous functionality:
882
+ // When distance dragging, the HIT POINT should move along the ray until it reaches the controller;
883
+ // NOT the pivot point of the dragged object. E.g. grabbing a large cube and pulling towards you should at most
884
+ // move the grabbed point to your head and not slap the cube in your head.
885
+ this._followObject.position.multiplyScalar(currentDist);
886
+ this._followObject.updateMatrix();
887
+
888
+ const ray = new Ray(dragSourceWP, rayDirection);
889
+
890
+ // Surface snapping.
891
+ // Feels quite weird in VR right now!
892
+ if (dragMode == DragMode.SnapToSurfaces) {
893
+ // Idea: Do a sphere cast if we're still in the proximity of the current draggedObject.
894
+ // This would allow dragging slightly out of the object's bounds and still continue snapping to it.
895
+ // Do a regular raycast (without the dragged object) to determine if we should change what is dragged onto.
896
+ const opts = new RaycastOptions();
897
+ opts.ignore = [draggedObject];
898
+ const hits = this.context.physics.raycastFromRay(ray, opts);
899
+
900
+ if (hits.length > 0) {
901
+ const hit = hits[0];
902
+ // if we're above the same surface for a specified time, adjust drag options:
903
+ // - set that surface as the drag "plane". We will follow that object's surface instead now (raycast onto only that)
904
+ // - if the drag plane is an object, we also want to
905
+ // - calculate an initial rotation offset matching what surface/face the user originally started the drag on
906
+ // - rotate the dragged object to match the surface normal
907
+ if (this._draggedOverObject === hit.object)
908
+ this._draggedOverObjectDuration += this.context.time.deltaTime;
909
+ else {
910
+ this._draggedOverObject = hit.object;
911
+ this._draggedOverObjectDuration = 0;
912
+ }
913
+
914
+ if (hit.face) {
915
+ // Adjust drag plane if we're dragging over a different object (for a certain amount of time)
916
+ // or if the surface normal changed
917
+ if (this._draggedOverObjectDuration > 0.15 &&
918
+ (this._draggedOverObjectLastSetUp !== this._draggedOverObject ||
919
+ this._draggedOverObjectLastNormal.dot(hit.face.normal) < 0.999999)
920
+ ) {
921
+ this._draggedOverObjectLastSetUp = this._draggedOverObject;
922
+ this._draggedOverObjectLastNormal.copy(hit.face.normal);
923
+
924
+ const center = new Vector3();
925
+ const size = new Vector3();
926
+
927
+ this._bounds.getCenter(center);
928
+ this._bounds.getSize(size);
929
+ center.sub(size.multiplyScalar(0.5).multiply(hit.face.normal));
930
+ this._hitPointInLocalSpace.copy(center);
931
+ this._hitNormalInLocalSpace.copy(hit.face.normal);
932
+
933
+ // ensure plane is far enough up that we don't drag into the surface
934
+ // Which offset we use here depends on the face normal direction we hit
935
+ // If we hit the bottom, we want to use the top, and vice versa
936
+ // To do this dynamically, we can find the intersection between our local bounds and the hit face normal (which is already in local space)
937
+ this._bounds.getCenter(center);
938
+ this._bounds.getSize(size);
939
+ center.add(size.multiplyScalar(0.5).multiply(hit.face.normal));
940
+
941
+ const offset = this._hitPointInLocalSpace.clone().add(center);
942
+ this._followObject.localToWorld(offset);
943
+ const offsetWP = this._followObject.worldPosition.sub(offset);
944
+
945
+ this._dragPlane.setFromNormalAndCoplanarPoint(hit.face.normal, hit.point.sub(offsetWP));
946
+ }
947
+ }
948
+ }
949
+ }
950
+
951
+ // Objects could also serve as "slots" for dragging other objects into. In that case, we don't want to snap to the surface,
952
+ // we want to snap to the pivot of that object. These dragged-over objects could also need to be invisible (a "slot")
953
+ // Raycast along the ray to the drag plane and move _followObject so that the grabbed point stays at the hit point
954
+ if (dragMode !== DragMode.Attached && ray.intersectPlane(this._dragPlane, this._tempVec)) {
955
+
956
+ this._followObject.worldPosition = this._tempVec;
957
+ this._followObject.updateMatrix();
958
+ this._followObject.updateMatrixWorld(true);
959
+
960
+ const newWP = this._hitPointInLocalSpace.clone();
961
+ this._followObject.localToWorld(newWP);
962
+
963
+ if (debug) {
964
+ Gizmos.DrawLine(newWP, this._tempVec, 0x00ffff, 0, false);
965
+ }
966
+
967
+ this._followObject.worldPosition = this._tempVec.multiplyScalar(2).sub(newWP);
968
+ this._followObject.updateMatrix();
969
+
970
+ /*
971
+ // TODO figure out nicer look rotation here
972
+ const normal = this._dragPlane.normal;
973
+ const lookPoint = normal.clone().multiplyScalar(1000).add(this._tempVec);
974
+ if (lookPoint) {
975
+ this._followObject.lookAt(lookPoint);
976
+ this._followObject.rotateX(Math.PI / 2);
977
+ }
978
+ */
979
+ this._followObject.updateMatrix();
980
+ }
981
+
982
+ // TODO refactor to a common place
983
+ // apply constraints (position grid snap, rotation, ...)
984
+ if (this.settings.snapGridResolution > 0) {
985
+ const wp = this._followObject.worldPosition;
986
+ const snap = this.settings.snapGridResolution;
987
+ wp.x = Math.round(wp.x / snap) * snap;
988
+ wp.y = Math.round(wp.y / snap) * snap;
989
+ wp.z = Math.round(wp.z / snap) * snap;
990
+ this._followObject.worldPosition = wp;
991
+ this._followObject.updateMatrix();
992
+ }
993
+ if (keepRotation) {
994
+ this._followObject.worldQuaternion = this._followObjectStartWorldQuaternion;
995
+ this._followObject.updateMatrix();
996
+ }
997
+
998
+ // TODO refactor to a common place
999
+ // TODO should use unscaled time here // some test for lerp speed depending on distance
1000
+ const t = Mathf.clamp01(this.context.time.deltaTime * lerpStrength * lerpFactor);// / (currentDist - 1 + 0.01));
1001
+
1002
+ const wp = draggedObject.worldPosition;
1003
+ wp.lerp(this._followObject.worldPosition, t);
1004
+ draggedObject.worldPosition = wp;
1005
+
1006
+ const rot = draggedObject.worldQuaternion;
1007
+ rot.slerp(this._followObject.worldQuaternion, t);
1008
+ draggedObject.worldQuaternion = rot;
1009
+
1010
+
1011
+ if (debug)
1012
+ {
1013
+ const hitPointWP = this._hitPointInLocalSpace.clone();
1014
+ draggedObject.localToWorld(hitPointWP);
1015
+ // draw grab attachment point and normal. They are in grabbed object space
1016
+ Gizmos.DrawSphere(hitPointWP, 0.02, 0xff0000);
1017
+ const hitNormalWP = this._hitNormalInLocalSpace.clone();
1018
+ hitNormalWP.applyQuaternion(rot);
1019
+ Gizmos.DrawRay(hitPointWP, hitNormalWP, 0xff0000);
1020
+
1021
+ // debug info
1022
+ Gizmos.DrawLabel(wp.add(new Vector3(0, 0.25, 0)),
1023
+ `Distance: ${this._totalMovement.length().toFixed(2)}\n
1024
+ Along Ray: ${this._totalMovementAlongRayDirection.toFixed(2)}\n
1025
+ Session: ${!!NeedleXRSession.active}\n
1026
+ Device: ${this._deviceMode}\n
1027
+ `,
1028
+ 0.03
1029
+ );
1030
+
1031
+ // draw bottom/back snap points
1032
+ const bottomCenter = this._bottomCenter.clone();
1033
+ const backCenter = this._backCenter.clone();
1034
+ const backBottomCenter = this._backBottomCenter.clone();
1035
+ draggedObject.localToWorld(bottomCenter);
1036
+ draggedObject.localToWorld(backCenter);
1037
+ draggedObject.localToWorld(backBottomCenter);
1038
+ Gizmos.DrawSphere(bottomCenter, 0.01, 0x00ff00, 0, false);
1039
+ Gizmos.DrawSphere(backCenter, 0.01, 0x0000ff, 0, false);
1040
+ Gizmos.DrawSphere(backBottomCenter, 0.01, 0xff00ff, 0, false);
1041
+ Gizmos.DrawLine(bottomCenter, backBottomCenter, 0x00ffff, 0, false);
1042
+ Gizmos.DrawLine(backBottomCenter, backCenter, 0x00ffff, 0, false);
1043
+ }
1044
+ }
1045
+
1046
+ onDragEnd(args: PointerEventData) {
1047
+ console.assert(this._followObject.parent === args.event.space, "Drag end: _followObject is not parented to the space object");
1048
+ this._followObject.removeFromParent();
1049
+ this._followObject.destroy();
1050
+ this._lastDragPosRigSpace = undefined;
1051
+ }
1052
+ }
1053
+
1054
+ /** Currently does _only_ provide visuals support for DragControls operations.
1055
+ * Previously it also provided the actual drag functionality, but that has been moved to DragControls for now.
1056
+ */
1057
+ class LegacyDragVisualsHelper {
1058
+
302
1059
  showGizmo: boolean = true;
303
1060
  useViewAngle: boolean = true;
304
1061
 
@@ -336,13 +1093,12 @@
336
1093
  constructor(camera: Camera) {
337
1094
  this._camera = camera;
338
1095
 
339
- const line = new Line(DragHelper.geometry);
1096
+ const line = new Line(LegacyDragVisualsHelper.geometry);
340
1097
  const mat = line.material as LineBasicMaterial;
341
1098
  mat.color = new Color(.4, .4, .4);
342
1099
  line.layers.set(2);
343
1100
  line.name = 'line';
344
1101
  line.scale.y = 1;
345
- // line.matrixAutoUpdate = false;
346
1102
  this._groundLine = line;
347
1103
 
348
1104
  const geometry = new SphereGeometry(.5, 22, 22);
@@ -357,13 +1113,12 @@
357
1113
  if (this._selected && context) {
358
1114
  for (const rb of this._rbs) {
359
1115
  rb.wakeUp();
360
- // if (!rb.smoothedVelocity) continue;
361
1116
  rb.setVelocity(0, 0, 0);
362
1117
  }
363
1118
  }
364
1119
 
365
1120
  if (this._selected) {
366
-
1121
+ // TODO move somewhere else
367
1122
  Avatar_POI.Remove(context, this._selected);
368
1123
  }
369
1124
 
@@ -385,6 +1140,8 @@
385
1140
  console.error("DragHelper: no context");
386
1141
  return;
387
1142
  }
1143
+
1144
+ // TODO move somewhere else
388
1145
  Avatar_POI.Add(context, this._selected, null);
389
1146
 
390
1147
  this._groundOffsetFactor = 0;
@@ -392,7 +1149,6 @@
392
1149
  this._groundOffset.set(0, 0, 0);
393
1150
  this._requireUpdateGroundPlane = true;
394
1151
 
395
- // this._rbs = GameObject.getComponentsInChildren(this._selected, Rigidbody);
396
1152
  this.onUpdateScreenSpacePlane();
397
1153
  }
398
1154
  }
@@ -402,6 +1158,16 @@
402
1158
  private _didDragOnGroundPlaneLastFrame: boolean = false;
403
1159
 
404
1160
  onUpdate(_context: Context) {
1161
+
1162
+ if (!this._selected) return;
1163
+
1164
+ const wp = getWorldPosition(this._selected);
1165
+ this.onUpdateWorldPosition(wp, this._groundPlanePoint, false);
1166
+ this.onUpdateGroundPlane();
1167
+ this._didDragOnGroundPlaneLastFrame = true;
1168
+ this._hasGroundPlane = true;
1169
+
1170
+ /*
405
1171
  if (!this._context) return;
406
1172
 
407
1173
  const mainKey: KeyCode = "Space";
@@ -488,6 +1254,7 @@
488
1254
  this.onDidUpdate();
489
1255
  }
490
1256
  }
1257
+ */
491
1258
  }
492
1259
 
493
1260
  private onUpdateWorldPosition(wp: Vector3, pointOnPlane: Vector3 | null, heightOnly: boolean) {
@@ -549,18 +1316,6 @@
549
1316
  this._groundOffset.copy(this._intersection).sub(wp);
550
1317
  }
551
1318
 
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
-
564
1319
  private contains(obj: Object3D, toSearch: Object3D): boolean {
565
1320
  if (obj === toSearch) return true;
566
1321
  if (obj.children) {
src/engine-components/Duplicatable.ts CHANGED
@@ -1,22 +1,24 @@
1
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";
2
+ import { DragControls } from "./DragControls.js";
6
3
  import { Vector3, Quaternion, Object3D } from "three";
7
4
  import { serializable } from "../engine/engine_serialization_decorator.js";
8
5
  import { InstantiateOptions } from "../engine/engine_gameobject.js";
6
+ import { IPointerEventHandler, ObjectRaycaster, PointerEventData } from "./api.js";
9
7
 
10
- export class Duplicatable extends Interactable {
8
+ export class Duplicatable extends Behaviour implements IPointerEventHandler {
11
9
 
10
+ /** Duplicates will be parented into the set object. If not defined, this GameObject will be used as parent. */
12
11
  @serializable(Object3D)
13
12
  parent: GameObject | null = null;
13
+
14
+ /** The object to be duplicated */
14
15
  @serializable(Object3D)
15
16
  object: GameObject | null = null;
16
17
 
17
18
  // limit max object spawn count per interval
18
19
  @serializable()
19
20
  limitCount = 10;
21
+
20
22
  @serializable()
21
23
  limitInterval = 60;
22
24
 
@@ -24,17 +26,7 @@
24
26
  private _startPosition: THREE.Vector3 | null = null;
25
27
  private _startQuaternion: THREE.Quaternion | null = null;
26
28
 
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);
29
+ start(): void {
38
30
  if (this.object) {
39
31
  if (this.object as any === this.gameObject) {
40
32
  console.error("Can not duplicate self");
@@ -48,32 +40,43 @@
48
40
  this._startQuaternion = this.object.quaternion?.clone() ?? new Quaternion(0, 0, 0, 1);
49
41
  }
50
42
 
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
- });
43
+ // legacy – DragControls was required for duplication and so often the component is still there; we work around that by disabling it here
44
+ const dragControls = this.gameObject.getComponent(DragControls);
45
+ if (dragControls) {
46
+ console.warn("Please remove DragControls from object with Duplicatable component, it's not needed anymore.");
47
+ dragControls.enabled = false;
64
48
  }
65
- else console.warn("Could no find drag controls in parent", this.name);
49
+
50
+ if (!this.gameObject.getComponentInParent(ObjectRaycaster))
51
+ this.gameObject.addNewComponent(ObjectRaycaster);
66
52
 
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;
53
+ this.cloneLimitIntervalFn();
54
+ }
55
+
56
+ private _forwardPointerEvents: Map<Object3D, DragControls> = new Map();
57
+
58
+ onPointerDown(args: PointerEventData) {
59
+ if (!this.object) return;
60
+ if (!this.context.connection.allowEditing) return;
61
+ if (args.button !== 0) return;
62
+
63
+ const res = this.handleDuplication();
64
+ if (res) {
65
+ const dragControls = GameObject.getComponent(res, DragControls);
66
+ if (!dragControls) console.warn("Duplicated object does not have DragControls");
67
+ else {
68
+ dragControls.onPointerDown(args);
69
+ this._forwardPointerEvents.set(args.event.space, dragControls);
71
70
  }
72
- const res = this.handleDuplication(args.selected);
73
- if (res) args.grab = res;
74
- });
71
+ }
72
+ }
75
73
 
76
- this.cloneLimitIntervalFn();
74
+ onPointerUp(args: PointerEventData) {
75
+ const dragControls = this._forwardPointerEvents.get(args.event.space);
76
+ if (dragControls) {
77
+ dragControls.onPointerUp(args);
78
+ this._forwardPointerEvents.delete(args.event.space);
79
+ }
77
80
  }
78
81
 
79
82
  private cloneLimitIntervalFn() {
@@ -86,62 +89,39 @@
86
89
  }, (this.limitInterval / this.limitCount) * 1000);
87
90
  }
88
91
 
89
- private handleDuplication(selected: THREE.Object3D): THREE.Object3D | null {
92
+ private handleDuplication(): THREE.Object3D | null {
93
+ if (!this.object) return null;
90
94
  if (this._currentCount >= this.limitCount) return null;
91
- if (!this.object) return null;
92
- if (selected === this.gameObject || this.handleMultiObject(selected)) {
95
+ if (this.object as any === this.gameObject) return null;
93
96
 
94
- if (this.object as any === this.gameObject) return null;
95
- this.object.visible = true;
97
+ this.object.visible = true;
96
98
 
97
- if (this._startPosition)
98
- this.object.position.copy(this._startPosition);
99
- if (this._startQuaternion)
100
- this.object.quaternion.copy(this._startQuaternion);
99
+ if (this._startPosition)
100
+ this.object.position.copy(this._startPosition);
101
+ if (this._startQuaternion)
102
+ this.object.quaternion.copy(this._startQuaternion);
101
103
 
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;
104
+ const opts = new InstantiateOptions();
105
+ if (!this.parent) this.parent = this.gameObject.parent as GameObject;
106
+ if (this.parent) {
107
+ opts.parent = this.parent.guid ?? this.parent.userData?.guid;
108
+ opts.keepWorldPosition = true;
109
+ }
110
+ opts.position = this.worldPosition;
111
+ opts.rotation = this.worldQuaternion;
112
+ opts.context = this.context;
113
+ this._currentCount += 1;
112
114
 
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;
115
+ const newInstance = GameObject.instantiateSynced(this.object as GameObject, opts) as GameObject;
116
+ console.assert(newInstance !== this.object, "Duplicated object is original");
117
+ this.object.visible = false;
116
118
 
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);
119
+ // see if this fixes object being offset when duplicated and dragged - it looks like three clone has shared position/quaternion objects?
120
+ if (this._startPosition)
121
+ this.object.position.clone().copy(this._startPosition);
122
+ if (this._startQuaternion)
123
+ this.object.quaternion.clone().copy(this._startQuaternion);
122
124
 
123
- return newInstance;
124
- }
125
- return null;
125
+ return newInstance;
126
126
  }
127
-
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
- }
133
-
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
- }
146
-
147
127
  }
src/engine/engine_context.ts CHANGED
@@ -36,6 +36,7 @@
36
36
  import { isLocalNetwork } from './engine_networking_utils.js';
37
37
  import { WaitForPromise } from './engine_coroutine.js';
38
38
  import { invokeLifecycleFunctions } from './engine_lifecycle_functions_internal.js';
39
+ import type { INeedleXRSessionEventReceiver } from './engine_xr.js';
39
40
 
40
41
 
41
42
  const debug = utils.getParam("debugcontext");
@@ -101,11 +102,6 @@
101
102
  Undefined = -1,
102
103
  }
103
104
 
104
- export enum XRSessionMode {
105
- ImmersiveVR = "immersive-vr",
106
- ImmersiveAR = "immersive-ar",
107
- }
108
-
109
105
  /** threejs callback event signature */
110
106
  export declare type OnRenderCallback = (renderer: WebGLRenderer, scene: Scene, camera: Camera, geometry: BufferGeometry, material: Material, group: Group) => void
111
107
 
@@ -213,6 +209,7 @@
213
209
  private _boundingClientRectFrame: number = -1;
214
210
  private _boundingClientRect: DOMRect | null = null;
215
211
  private _domX; private _domY;
212
+ /** update bounding rects + domX, domY */
216
213
  private calculateBoundingClientRect() {
217
214
  // workaround for mozilla webXR viewer
218
215
  if (this.isInAR) {
@@ -227,30 +224,40 @@
227
224
  this._domY = this._boundingClientRect.y;
228
225
  }
229
226
 
227
+ /** The width of the `<needle-engine>` element on the website */
230
228
  get domWidth(): number {
231
229
  // for mozilla XR
232
230
  if (this.isInAR) return window.innerWidth;
233
231
  return this.domElement.clientWidth;
234
232
  }
233
+ /** The height of the `<needle-engine>` element on the website */
235
234
  get domHeight(): number {
236
235
  // for mozilla XR
237
236
  if (this.isInAR) return window.innerHeight;
238
237
  return this.domElement.clientHeight;
239
238
  }
239
+ /** the X position of the Needle Engine element on the website */
240
240
  get domX(): number {
241
241
  this.calculateBoundingClientRect();
242
242
  return this._domX;
243
243
  }
244
+ /** the Y position of the Needlee Engine element on the website */
244
245
  get domY(): number {
245
246
  this.calculateBoundingClientRect();
246
247
  return this._domY;
247
248
  }
248
249
  get isInXR() { return this.renderer?.xr?.isPresenting || false; }
250
+ // TODO: can we get the session mode from the xr session without relying on the initiator to set it?
249
251
  xrSessionMode: XRSessionMode | undefined = undefined;
250
- get isInVR() { return this.xrSessionMode === XRSessionMode.ImmersiveVR; }
251
- get isInAR() { return this.xrSessionMode === XRSessionMode.ImmersiveAR; }
252
+ get isInVR() { return this.xrSessionMode === "immersive-vr"; }
253
+ get isInAR() { return this.xrSessionMode === "immersive-ar"; }
254
+ /** access the raw `XRSession` object (shorthand for `context.renderer.xr.getSession()`). For more control use `NeedleXRSession.active` */
252
255
  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
+ */
253
259
  get xrFrame() { return this._xrFrame }
260
+ /** @returns the current WebXR camera (shorthand for `context.renderer.xr.getCamera()`) */
254
261
  get xrCamera(): WebXRArrayCamera | undefined { return this.renderer?.xr?.getCamera(); }
255
262
  private _xrFrame: XRFrame | null = null;
256
263
  get arOverlayElement(): HTMLElement {
@@ -270,17 +277,30 @@
270
277
  composer: EffectComposer | null = null;
271
278
 
272
279
  // all scripts
273
- scripts: IComponent[] = [];
274
- scripts_pausedChanged: IComponent[] = [];
280
+ readonly scripts: IComponent[] = [];
281
+ readonly scripts_pausedChanged: IComponent[] = [];
275
282
  // scripts with update event
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> } = {}
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> } = {}
283
292
 
293
+ readonly post_setup_callbacks: Function[] = [];
294
+ readonly pre_update_callbacks: Function[] = [];
295
+ readonly pre_render_callbacks: Array<(frame: XRFrame | null) => void> = [];
296
+ readonly post_render_callbacks: Function[] = [];
297
+
298
+ readonly new_scripts: IComponent[] = [];
299
+ readonly new_script_start: IComponent[] = [];
300
+ readonly new_scripts_pre_setup_callbacks: Function[] = [];
301
+ readonly new_scripts_post_setup_callbacks: Function[] = [];
302
+ readonly new_scripts_xr: INeedleXRSessionEventReceiver[] = [];
303
+
284
304
  mainCameraComponent: ICamera | undefined;
285
305
 
286
306
  private _camera: Camera | null = null;
@@ -300,20 +320,13 @@
300
320
  this._camera = cam;
301
321
  }
302
322
 
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
-
313
323
  application: Application;
324
+ /** access timings (current frame number, deltaTime, timeScale, ...) */
314
325
  time: Time;
315
326
  input: Input;
327
+ /** access physics related methods (e.g. raycasting). To access the phyiscs engine use `context.physics.engine` */
316
328
  physics: Physics;
329
+ /** access networking methods (use it to send or listen to messages or join a networking backend) */
317
330
  connection: NetworkConnection;
318
331
  /**
319
332
  * @deprecated AssetDataBase is deprecated
@@ -412,6 +425,8 @@
412
425
  this.renderer.outputColorSpace = SRGBColorSpace;
413
426
  // https://github.com/mrdoob/three.js/pull/25556
414
427
  this.renderer.useLegacyLights = false;
428
+
429
+ this.input.bindEvents();
415
430
  }
416
431
 
417
432
 
@@ -423,10 +438,13 @@
423
438
 
424
439
  private _disposeCallbacks: Function[] = [];
425
440
 
426
- // private _requestSizeUpdate : boolean = false;
441
+
442
+ /** will request a renderer size update the next render call (will call updateSize the next update) */
443
+ requestSizeUpdate() { this._sizeChanged = true; }
427
444
 
428
- updateSize() {
429
- if (!this.isManagedExternally && this.renderer.xr?.isPresenting === false) {
445
+ /** update the renderer and canvas size */
446
+ updateSize(force: boolean = false) {
447
+ if (force || (!this.isManagedExternally && this.renderer.xr?.isPresenting === false)) {
430
448
  this._sizeChanged = false;
431
449
  const scaleFactor = this.resolutionScaleFactor;
432
450
  const width = this.domWidth * scaleFactor;
@@ -530,11 +548,11 @@
530
548
  if (this.renderer) {
531
549
  this.renderer.setClearAlpha(0);
532
550
  this.renderer.clear();
551
+ if (!this.isManagedExternally) {
552
+ if (debug) console.log("Disposing renderer");
553
+ this.renderer.dispose();
554
+ }
533
555
  }
534
- if (!this.isManagedExternally) {
535
- if(debug) console.log("Disposing renderer");
536
- this.renderer.dispose();
537
- }
538
556
  this.scene = null!;
539
557
  this.renderer = null!;
540
558
  this.input.dispose();
@@ -552,6 +570,10 @@
552
570
  this._isCreated = false;
553
571
  ContextRegistry.dispatchCallback(ContextEvent.ContextDestroyed, this);
554
572
  ContextRegistry.unregister(this);
573
+ if (Context.Current === this) {
574
+ //@ts-ignore
575
+ Context.Current = null;
576
+ }
555
577
  }
556
578
 
557
579
  registerCoroutineUpdate(script: IComponent, coroutine: Generator, evt: FrameEvent): Generator {
@@ -810,6 +832,8 @@
810
832
  }
811
833
  }
812
834
 
835
+ this.input.bindEvents();
836
+
813
837
  Context.Current = this;
814
838
  looputils.processNewScripts(this);
815
839
 
@@ -852,7 +876,7 @@
852
876
  this._dispatchReadyAfterFrame = true;
853
877
  const res = ContextRegistry.dispatchCallback(ContextEvent.ContextCreated, this, { files: loadedFiles });
854
878
  if (res) {
855
- if("internalSetLoadingMessage" in this.domElement && typeof this.domElement.internalSetLoadingMessage === "function")
879
+ if ("internalSetLoadingMessage" in this.domElement && typeof this.domElement.internalSetLoadingMessage === "function")
856
880
  this.domElement?.internalSetLoadingMessage("finish loading");
857
881
  await res;
858
882
  }
@@ -1007,7 +1031,11 @@
1007
1031
 
1008
1032
  private internalOnBeforeRender(timestamp: DOMHighResTimeStamp, frame: XRFrame | null) {
1009
1033
 
1034
+ const sessionStarted = frame !== null && this._xrFrame === null;
1010
1035
  this._xrFrame = frame;
1036
+ if (sessionStarted) {
1037
+ this.domElement.dispatchEvent(new CustomEvent("xr-session-started", { detail: { context: this, session: this.xrSession, frame: frame } }));
1038
+ }
1011
1039
 
1012
1040
  this._currentFrameEvent = FrameEvent.Undefined;
1013
1041
 
@@ -1128,7 +1156,7 @@
1128
1156
 
1129
1157
  if (this.pre_render_callbacks) {
1130
1158
  for (const i in this.pre_render_callbacks) {
1131
- this.pre_render_callbacks[i]();
1159
+ this.pre_render_callbacks[i](frame);
1132
1160
  }
1133
1161
  }
1134
1162
 
@@ -1206,8 +1234,8 @@
1206
1234
  }
1207
1235
  this._isRendering = true;
1208
1236
  this.renderRequiredTextures();
1209
-
1210
1237
 
1238
+
1211
1239
  if (this.composer && !this.isInXR) {
1212
1240
  this.composer.render(this.time.deltaTime);
1213
1241
  }
src/engine/engine_create_objects.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { PlaneGeometry, MeshBasicMaterial, DoubleSide, Mesh, Material, MeshStandardMaterial, BoxGeometry, SphereGeometry, ColorRepresentation } from "three"
2
+ import { Vec3 } from "./engine_types.js";
2
3
 
3
4
  export enum PrimitiveType {
4
5
  Quad = 0,
@@ -9,6 +10,10 @@
9
10
  export type ObjectOptions = {
10
11
  name?: string,
11
12
  material?: Material,
13
+ position?: Vec3,
14
+ /** euler */
15
+ rotation?: Vec3,
16
+ scale?: Vec3,
12
17
  }
13
18
 
14
19
  export class ObjectUtils {
@@ -35,6 +40,12 @@
35
40
  }
36
41
  if (opts?.name)
37
42
  obj.name = opts.name;
43
+ if (opts?.position)
44
+ obj.position.set(opts.position.x, opts.position.y, opts.position.z);
45
+ if (opts?.rotation)
46
+ obj.rotation.set(opts.rotation.x, opts.rotation.y, opts.rotation.z);
47
+ if (opts?.scale)
48
+ obj.scale.set(opts.scale.x, opts.scale.y, opts.scale.z);
38
49
  return obj;
39
50
  }
40
51
  }
src/engine/engine_element_loading.ts CHANGED
@@ -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 = "2px";
236
+ loadingBarContainer.style.height = "3px";
237
237
  if (loadingStyle === "light")
238
238
  loadingBarContainer.style.backgroundColor = "rgba(0,0,0,.2)"
239
239
  else
@@ -247,6 +247,15 @@
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
+ }
250
259
  logo.src = logoSVG;
251
260
  let isUsingCustomLogo = false;
252
261
  if (hasLicense && this._element) {
@@ -323,6 +332,16 @@
323
332
  // if it's the case then we don't need to perform a runtime check
324
333
  if (commercialLicense) return;
325
334
 
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
+
326
345
  // Use the runtime license check
327
346
  if (runtimeLicenseCheckPromise) {
328
347
  if (debugLicense) console.log("Waiting for runtime license check");
@@ -330,13 +349,7 @@
330
349
  commercialLicense = hasCommercialLicense();
331
350
  }
332
351
  if (commercialLicense) return;
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);
352
+ nonCommercialContainer.style.transition = "opacity .5s ease-in-out";
353
+ nonCommercialContainer.style.opacity = "1";
341
354
  }
342
355
  }
src/engine/engine_element_overlay.ts CHANGED
@@ -16,6 +16,7 @@
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;
19
20
 
20
21
  requestEndAR = () => {
21
22
  this.onRequestedEndAR();
@@ -34,6 +35,22 @@
34
35
  this._reparentedObjects.push({ el: el, previousParent: el.parentElement });
35
36
  this.arContainer?.appendChild(el);
36
37
  }
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
+ }
37
54
  }
38
55
  this.ensureQuitARButton(this.arContainer);
39
56
  }
src/engine/engine_element.ts CHANGED
@@ -143,12 +143,15 @@
143
143
  }
144
144
  :host .quit-ar-button {
145
145
  position: absolute;
146
- top: 40px;
146
+ // top: env(titlebar-area-y); /** this doesnt work **/
147
+ top: 60px; /** camera access needs a bit more space **/
147
148
  right: 20px;
148
149
  z-index: 9999;
149
150
  }
150
151
  </style>
151
- <canvas></canvas>
152
+ <div> <!-- this wrapper is necessary for WebXR https://github.com/meta-quest/immersive-web-emulator/issues/55 -->
153
+ <canvas></canvas>
154
+ </div>
152
155
  <div class="content">
153
156
  <slot class="overlay-content"></slot>
154
157
  </div>
@@ -167,6 +170,7 @@
167
170
  console.log("<needle-engine> connected");
168
171
  }
169
172
 
173
+ this.addEventListener("xr-session-started", this.onXRSessionStarted);
170
174
  this.onSetupDesktop();
171
175
 
172
176
  if (!this.getAttribute("src")) {
@@ -196,6 +200,8 @@
196
200
  }
197
201
 
198
202
  disconnectedCallback() {
203
+ this.removeEventListener("xr-session-started", this.onXRSessionStarted);
204
+
199
205
  this._didFullyLoad = false;
200
206
  const keepAlive = this.getAttribute("keep-alive");
201
207
  const dispose = keepAlive == undefined || (keepAlive?.length > 0 && keepAlive !== "true" && keepAlive !== "1");
@@ -384,6 +390,23 @@
384
390
  }));
385
391
  }
386
392
 
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
+
387
410
  /** called by the context when the first frame has been rendered */
388
411
  private onReady = () => this._loadingView?.onLoadingFinished();
389
412
  private onError = () => this._loadingView?.setMessage("Loading failed!");
@@ -474,8 +497,10 @@
474
497
  return null;
475
498
  }
476
499
 
477
- onEnterAR(session: XRSession, overlayContainer: HTMLElement) {
500
+ onEnterAR(session: XRSession) {
478
501
  this.onSetupAR();
502
+ const overlayContainer = this.getAROverlayContainer();
503
+ console.log("onEnterAR", session, overlayContainer);
479
504
  this._overlay_ar.onBegin(this._context!, overlayContainer, session);
480
505
  this.dispatchEvent(new CustomEvent("enter-ar", { detail: { session: session, context: this._context, htmlContainer: this._overlay_ar?.ARContainer } }));
481
506
  }
src/engine/engine_gizmos.ts CHANGED
@@ -6,6 +6,7 @@
6
6
  import { getParam } from './engine_utils.js';
7
7
  import { type Options } from 'three-mesh-ui/build/types/core/elements/MeshUIBaseElement.js';
8
8
  import { isDestroyed } from './engine_gameobject.js';
9
+ import { NeedleXRSession } from './engine_xr.js';
9
10
 
10
11
  const _tmp = new Vector3();
11
12
  const _tmp2 = new Vector3();
@@ -21,6 +22,15 @@
21
22
 
22
23
  export class Gizmos {
23
24
 
25
+ /**
26
+ * Allow creating gizmos
27
+ * If disabled then no gizmos will be added to the scene anymore
28
+ */
29
+ static enabled = true;
30
+
31
+ /**
32
+ * Returns true if a given object is a gizmo
33
+ */
24
34
  static isGizmo(obj: Object3D) {
25
35
  return obj[$cacheSymbol] !== undefined;
26
36
  }
@@ -29,10 +39,12 @@
29
39
  * Draw a label in the scene or attached to an object (if a parent is provided)
30
40
  * @returns a handle to the label that can be used to change the text
31
41
  */
32
- static DrawLabel(position: Vec3, text: string, size: number = .1, duration: number = 9999, color?: ColorRepresentation, backgroundColor?: ColorRepresentation | ColorWithAlpha, parent?: Object3D,) {
42
+ static DrawLabel(position: Vec3, text: string, size: number = .1, duration: number = 0, color?: ColorRepresentation, backgroundColor?: ColorRepresentation | ColorWithAlpha, parent?: Object3D,) {
43
+ if (!Gizmos.enabled) return null;
33
44
  if (!color) color = defaultColor;
34
- const element = Internal.getTextLabel(duration, text, size, color, backgroundColor);
35
- if (parent instanceof Object3D) parent.add(element);
45
+ const rigScale = NeedleXRSession.active?.rigScale ?? 1;
46
+ const element = Internal.getTextLabel(duration, text, size * rigScale, color, backgroundColor);
47
+ if (parent instanceof Object3D) parent.add(element as any);
36
48
  element.position.x = position.x;
37
49
  element.position.y = position.y;
38
50
  element.position.z = position.z;
@@ -40,6 +52,7 @@
40
52
  }
41
53
 
42
54
  static DrawRay(origin: Vec3, dir: Vec3, color: ColorRepresentation = defaultColor, duration: number = 0, depthTest: boolean = true) {
55
+ if (!Gizmos.enabled) return;
43
56
  const obj = Internal.getLine(duration);
44
57
  const positions = obj.geometry.getAttribute("position");
45
58
  positions.setXYZ(0, origin.x, origin.y, origin.z);
@@ -52,6 +65,7 @@
52
65
  }
53
66
 
54
67
  static DrawDirection(pt: Vec3, direction: Vec3 | Vec4, color: ColorRepresentation = defaultColor, duration: number = 0, depthTest: boolean = true, lengthFactor: number = 1) {
68
+ if (!Gizmos.enabled) return;
55
69
  const obj = Internal.getLine(duration);
56
70
  const positions = obj.geometry.getAttribute("position");
57
71
  positions.setXYZ(0, pt.x, pt.y, pt.z);
@@ -73,8 +87,8 @@
73
87
  }
74
88
 
75
89
  static DrawLine(pt0: Vec3, pt1: Vec3, color: ColorRepresentation = defaultColor, duration: number = 0, depthTest: boolean = true) {
90
+ if (!Gizmos.enabled) return;
76
91
  const obj = Internal.getLine(duration);
77
-
78
92
  const positions = obj.geometry.getAttribute("position");
79
93
  positions.setXYZ(0, pt0.x, pt0.y, pt0.z);
80
94
  positions.setXYZ(1, pt1.x, pt1.y, pt1.z);
@@ -85,6 +99,7 @@
85
99
  }
86
100
 
87
101
  static DrawWireSphere(center: Vec3, radius: number, color: ColorRepresentation = defaultColor, duration: number = 0, depthTest: boolean = true) {
102
+ if (!Gizmos.enabled) return;
88
103
  const obj = Internal.getSphere(radius, duration, true);
89
104
  setWorldPositionXYZ(obj, center.x, center.y, center.z);
90
105
  obj.material["color"].set(color);
@@ -93,6 +108,7 @@
93
108
  }
94
109
 
95
110
  static DrawSphere(center: Vec3, radius: number, color: ColorRepresentation = defaultColor, duration: number = 0, depthTest: boolean = true) {
111
+ if (!Gizmos.enabled) return;
96
112
  const obj = Internal.getSphere(radius, duration, false);
97
113
  setWorldPositionXYZ(obj, center.x, center.y, center.z);
98
114
  obj.material["color"].set(color);
@@ -101,6 +117,7 @@
101
117
  }
102
118
 
103
119
  static DrawWireBox(center: Vec3, size: Vec3, color: ColorRepresentation = defaultColor, duration: number = 0, depthTest: boolean = true) {
120
+ if (!Gizmos.enabled) return;
104
121
  const obj = Internal.getBox(duration);
105
122
  obj.position.set(center.x, center.y, center.z);
106
123
  obj.scale.set(size.x, size.y, size.z);
@@ -111,6 +128,7 @@
111
128
  }
112
129
 
113
130
  static DrawWireBox3(box: Box3, color: ColorRepresentation = defaultColor, duration: number = 0, depthTest: boolean = true) {
131
+ if (!Gizmos.enabled) return;
114
132
  const obj = Internal.getBox(duration);
115
133
  obj.position.copy(box.getCenter(_tmp));
116
134
  obj.scale.copy(box.getSize(_tmp));
@@ -122,6 +140,7 @@
122
140
 
123
141
  private static _up = new Vector3(0, 1, 0);
124
142
  static DrawArrow(pt0: Vec3, pt1: Vec3, color: ColorRepresentation = defaultColor, duration: number = 0, depthTest: boolean = true, wireframe: boolean = false) {
143
+ if (!Gizmos.enabled) return;
125
144
  const obj = Internal.getArrowHead(duration);
126
145
  obj.position.set(pt1.x, pt1.y, pt1.z);
127
146
  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());
@@ -194,6 +213,7 @@
194
213
  textContent: text,
195
214
  borderRadius: 1 * size,
196
215
  padding: 1 * size,
216
+ whiteSpace: 'pre',
197
217
  };
198
218
 
199
219
  if (!element) {
@@ -201,7 +221,7 @@
201
221
  const global = this;
202
222
  const labelHandle = element as LabelHandle & Text;
203
223
  labelHandle.setText = function (str: string) {
204
- this.set({ textContent: str, whiteSpace: 'pre' });
224
+ this.set({ textContent: str });
205
225
  global.tmuiNeedsUpdate = true;
206
226
  };
207
227
  }
@@ -211,9 +231,8 @@
211
231
  // handle.setText(text);
212
232
  }
213
233
  this.tmuiNeedsUpdate = true;
214
- element.layers.disableAll();
215
- element.layers.enable(2);
216
- this.registerTimedObject(Context.Current, element, duration, this.textLabelCache);
234
+ element.layers.enableAll();
235
+ this.registerTimedObject(Context.Current, element as any, duration, this.textLabelCache as any);
217
236
  return element as Text & LabelHandle;
218
237
  }
219
238
 
@@ -269,20 +288,41 @@
269
288
  private static textLabelCache: Array<Text> = [];
270
289
 
271
290
  private static registerTimedObject(context: Context, object: Object3D, duration: number, cache: Array<Object3D>) {
272
- if (!this.contextPostRenderCallbacks.get(context)) {
291
+ const beforeRender = this.contextBeforeRenderCallbacks.get(context);
292
+ const postRender = this.contextPostRenderCallbacks.get(context);
293
+
294
+ if (!beforeRender) {
295
+ const cb = () => { this.onBeforeRender(context, this.timedObjectsBuffer) };
296
+ this.contextBeforeRenderCallbacks.set(context, cb);
297
+ context.pre_render_callbacks.push(cb);
298
+ }
299
+ // make sure gizmo pre render is the last one being called
300
+ else if (context.pre_render_callbacks[context.pre_render_callbacks.length - 1] !== beforeRender) {
301
+ const index = context.pre_render_callbacks.indexOf(beforeRender);
302
+ if (index >= 0) {
303
+ context.pre_render_callbacks.splice(index, 1);
304
+ }
305
+ context.pre_render_callbacks.push(beforeRender);
306
+ }
307
+
308
+ if (!postRender) {
273
309
  const cb = () => { this.onPostRender(context, this.timedObjectsBuffer, this.timesBuffer) };
274
310
  this.contextPostRenderCallbacks.set(context, cb);
275
311
  context.post_render_callbacks.push(cb);
276
312
  }
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);
313
+ // make sure gizmo post render is the last one being called
314
+ else if (context.post_render_callbacks[context.post_render_callbacks.length - 1] !== postRender) {
315
+ const index = context.post_render_callbacks.indexOf(postRender);
316
+ if (index >= 0) {
317
+ context.post_render_callbacks.splice(index, 1);
318
+ }
319
+ context.post_render_callbacks.push(postRender);
281
320
  }
282
321
 
283
- object.renderOrder = 999999;
284
322
  object.layers.disableAll();
285
323
  object.layers.enable(2);
324
+
325
+ object.renderOrder = 999999;
286
326
  object[$cacheSymbol] = cache;
287
327
  this.timedObjectsBuffer.push(object);
288
328
  this.timesBuffer.push(Context.Current.time.realtimeSinceStartup + duration);
@@ -304,13 +344,13 @@
304
344
  for (let i = 0; i < objects.length; i++) {
305
345
  const obj = objects[i];
306
346
  if (ctx.mainCamera && obj instanceof ThreeMeshUI.MeshUIBaseElement) {
307
- if (isDestroyed(obj)) {
347
+ if (isDestroyed(obj as any)) {
308
348
  continue;
309
349
  }
310
350
  const isInXR = ctx.isInVR;
311
351
  const keepUp = isInXR;
312
352
  const copyRotation = !isInXR;
313
- lookAtObject(obj, ctx.mainCamera, keepUp, copyRotation);
353
+ lookAtObject(obj as any, ctx.mainCamera, keepUp, copyRotation);
314
354
  }
315
355
  }
316
356
  }
src/engine/engine_input.ts CHANGED
@@ -1,22 +1,63 @@
1
- import { Vector2 } from 'three';
1
+ import { Matrix4, Object3D, Ray, Vector2, Vector3 } from 'three';
2
2
  import { showBalloonMessage, showBalloonWarning } from './debug/debug.js';
3
3
  import { Context } from './engine_setup.js';
4
- import type { IInput, Vec2 } from './engine_types.js';
5
- import { getParam } from './engine_utils.js';
4
+ import type { ButtonName, IGameObject, IInput, MouseButtonName, Vec2 } from './engine_types.js';
5
+ import { EnumToPrimitiveUnion, getParam, isMozillaXR } from './engine_utils.js';
6
6
 
7
7
  const debug = getParam("debuginput");
8
8
 
9
+ export declare type NEPointerEventInit = PointerEventInit &
10
+ {
11
+ pointerId: number;
12
+ mode: XRTargetRayMode,
13
+ ray?: Ray;
14
+ /** The control object for this input. In the case of spatial devices the controller,
15
+ * otherwise a generated object in screen space. The object may not be in the scene. */
16
+ device: Object3D;
17
+ buttonName: ButtonName | "none";
18
+ }
19
+
20
+
9
21
  export class NEPointerEvent extends PointerEvent {
22
+ /** the browser event that triggered this event (if any) */
10
23
  readonly source: Event | null;
11
24
 
12
- constructor(type: InputEvents, source: Event | null, init: PointerEventInit) {
25
+ readonly mode: XRTargetRayMode;
26
+ /** A ray in worldspace for the event */
27
+ readonly ray?: Ray;
28
+ /** The device space (this object is not necessarily rendered in the scene but you can access or copy the matrix) */
29
+ readonly space: Object3D;
30
+
31
+ isClick: boolean = false;
32
+ isDoubleClick: boolean = false;
33
+
34
+ constructor(type: InputEvents, source: Event | null, init: NEPointerEventInit) {
13
35
  super(type, init)
14
36
  this.source = source;
37
+ this.mode = init.mode;
38
+ this.ray = init.ray;
39
+ this.space = init.device;
15
40
  }
41
+
42
+ private _immediatePropagationStopped = false;
43
+ get immediatePropagationStopped() {
44
+ return this._immediatePropagationStopped;
45
+ }
46
+ private _propagationStopped = false;
47
+ get propagationStopped() {
48<