Needle Engine

Changes between version 3.32.6-alpha and 3.31.1
Files changed (92) hide show
  1. src/engine-schemes/vrUserStateBuffer.fbs +0 -0
  2. src/engine-schemes/README.md +0 -2
  3. src/engine-components/api.ts +1 -1
  4. src/engine/api.ts +0 -1
  5. src/engine-components/AudioSource.ts +1 -1
  6. src/engine-components/webxr/Avatar.ts +0 -220
  7. src/engine-components/avatar/AvatarBlink_Simple.ts +1 -1
  8. src/engine-components/ui/BaseUIComponent.ts +2 -2
  9. src/engine-components/ui/Button.ts +2 -2
  10. src/engine-components/Camera.ts +3 -2
  11. src/engine-components/ui/Canvas.ts +11 -28
  12. src/engine-components/Component.ts +6 -90
  13. src/engine-components/codegen/components.ts +14 -9
  14. src/engine/debug/debug_console.ts +4 -8
  15. src/engine/debug/debug_overlay.ts +5 -6
  16. src/engine-components/DragControls.ts +176 -931
  17. src/engine-components/Duplicatable.ts +88 -69
  18. src/engine/engine_context.ts +43 -89
  19. src/engine/engine_create_objects.ts +0 -11
  20. src/engine/engine_element_loading.ts +9 -22
  21. src/engine/engine_element_overlay.ts +0 -17
  22. src/engine/engine_element.ts +3 -28
  23. src/engine/engine_gizmos.ts +16 -56
  24. src/engine/engine_input.ts +177 -369
  25. src/engine/engine_lifecycle_api.ts +3 -27
  26. src/engine/engine_mainloop_utils.ts +1 -27
  27. src/engine/engine_networking_instantiate.ts +2 -7
  28. src/engine/engine_networking_streams.ts +3 -3
  29. src/engine/engine_networking.ts +4 -7
  30. src/engine/engine_physics_rapier.ts +6 -12
  31. src/engine/engine_physics.ts +12 -14
  32. src/engine/engine_serialization_core.ts +2 -2
  33. src/engine/engine_three_utils.ts +2 -15
  34. src/engine/engine_types.ts +1 -29
  35. src/engine/engine_utils.ts +4 -68
  36. src/engine/engine_xr.ts +0 -2
  37. src/engine-components/ui/EventSystem.ts +126 -111
  38. src/engine-components/ui/Graphic.ts +2 -2
  39. src/engine-components/GroundProjection.ts +2 -7
  40. src/engine-components/webxr/index.ts +2 -1
  41. src/engine/xr/index.ts +0 -5
  42. src/engine-components/Interactable.ts +14 -6
  43. src/engine/xr/internal.ts +0 -34
  44. src/engine-components/Light.ts +7 -3
  45. src/engine/extensions/NEEDLE_techniques_webgl.ts +0 -2
  46. src/needle-engine.ts +3 -0
  47. src/engine/xr/NeedleXRController.ts +0 -615
  48. src/engine/xr/NeedleXRSession.ts +0 -1270
  49. src/engine/xr/NeedleXRSync.ts +0 -221
  50. src/engine-components/utils/OpenURL.ts +37 -5
  51. src/engine-components/OrbitControls.ts +3 -3
  52. src/engine-components/ParticleSystem.ts +0 -5
  53. src/engine-components/ParticleSystemModules.ts +2 -9
  54. src/engine-components/timeline/PlayableDirector.ts +1 -1
  55. src/engine-components/PlayerColor.ts +13 -17
  56. src/engine-components-experimental/networking/PlayerSync.ts +21 -108
  57. src/engine-components/ui/PointerEvents.ts +23 -48
  58. src/engine-components/ui/Raycaster.ts +7 -25
  59. src/engine/codegen/register_types.ts +25 -15
  60. src/engine-components/Renderer.ts +25 -19
  61. src/engine-components/RendererLightmap.ts +2 -2
  62. src/engine-components/SceneSwitcher.ts +9 -9
  63. src/engine-components/SpectatorCamera.ts +23 -12
  64. src/engine-components/SyncedCamera.ts +2 -1
  65. src/engine-components/SyncedTransform.ts +0 -17
  66. src/engine-components/webxr/TeleportTarget.ts +0 -9
  67. src/engine/xr/TempXRContext.ts +0 -182
  68. src/engine-components/ui/Text.ts +4 -4
  69. src/engine-components/timeline/TimelineTracks.ts +14 -51
  70. src/engine-components/webxr/types.ts +0 -4
  71. src/engine-components/export/usdz/USDZExporter.ts +78 -6
  72. src/engine-components/export/usdz/extensions/USDZUI.ts +1 -1
  73. src/engine-components/ui/Utils.ts +1 -2
  74. src/engine/xr/utils.ts +0 -39
  75. src/engine-schemes/vr-user-state-buffer.ts +30 -37
  76. src/engine-components/webxr/WebARCameraBackground.ts +45 -37
  77. src/engine-components/webxr/WebARSessionRoot.ts +27 -397
  78. src/engine-components/webxr/WebXR.ts +674 -208
  79. src/engine-components/webxr/WebXRAvatar.ts +299 -8
  80. src/engine-components/webxr/WebXRButtons.ts +0 -266
  81. src/engine-components/webxr/WebXRImageTracking.ts +71 -63
  82. src/engine-components/webxr/WebXRPlaneTracking.ts +45 -52
  83. src/engine-components/webxr/WebXRRig.ts +8 -44
  84. src/engine-components/webxr/controllers/XRControllerFollow.ts +0 -58
  85. src/engine-components/webxr/controllers/XRControllerModel.ts +0 -252
  86. src/engine-components/webxr/controllers/XRControllerMovement.ts +0 -316
  87. src/engine-components/webxr/XRFlag.ts +0 -143
  88. src/engine/xr/XRRig.ts +0 -9
  89. src/engine-components/webxr/WebXRController.ts +1168 -0
  90. src/engine-components/webxr/WebXRGrabRendering.ts +151 -0
  91. src/engine-components/webxr/WebXRSync.ts +463 -0
  92. src/engine-components/XRFlag.ts +139 -0
src/engine-schemes/vrUserStateBuffer.fbs CHANGED
File without changes
src/engine-schemes/README.md DELETED
@@ -1,2 +0,0 @@
1
- Using flatbuffer compiler 2.0
2
- https://github.com/google/flatbuffers/releases/tag/v2.0.0
src/engine-components/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 "./webxr/XRFlag.js"
7
+ export * from "./XRFlag.js"
8
8
 
9
9
  export * from "./export/index.js"
10
10
  export * from "./postprocessing/index.js"
src/engine/api.ts CHANGED
@@ -47,7 +47,6 @@
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";
51
50
 
52
51
  export { TypeStore, registerType } from "./engine_typestore.js";
53
52
 
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.dispatchEvent(new CustomEvent("ended", { detail: this }));
414
+ this.sound.dispatchEvent({ type: 'ended', target: this });
415
415
  }
416
416
 
417
417
  // this.gameObject.position.x = Math.sin(time.time) * 2;
src/engine-components/webxr/Avatar.ts DELETED
@@ -1,220 +0,0 @@
1
- import { AssetReference } from "../../engine/engine_addressables.js";
2
- import { serializable } from "../../engine/engine_serialization_decorator.js";
3
- import { PromiseAllWithErrors, getParam } from "../../engine/engine_utils.js";
4
- import { NeedleXREventArgs, NeedleXRSession, NeedleXRUtils } from "../../engine/xr/index.js";
5
- import { Behaviour, GameObject } from "../Component.js";
6
- import { Object3D, Quaternion, Vector3 } from "three";
7
- import { ObjectUtils, PrimitiveType } from "../../engine/engine_create_objects.js";
8
- import { SyncedTransform } from "../SyncedTransform.js";
9
- import { PlayerState } from "../../engine-components-experimental/networking/PlayerSync.js";
10
- import { IGameObject } from "../../engine/engine_types.js";
11
- import { XRFlag } from "./XRFlag.js";
12
- import { AvatarMarker } from "./WebXRAvatar.js";
13
-
14
- const debug = getParam("debugwebxr");
15
-
16
- const flipForwardQuaternion = new Quaternion().setFromAxisAngle(new Vector3(0, 1, 0), Math.PI);
17
-
18
- export class Avatar extends Behaviour {
19
-
20
- @serializable(AssetReference)
21
- head?: AssetReference;
22
-
23
- @serializable(AssetReference)
24
- leftHand?: AssetReference;
25
-
26
- @serializable(AssetReference)
27
- rightHand?: AssetReference;
28
-
29
- private _syncTransforms?: SyncedTransform[];
30
-
31
- async onEnterXR(_args: NeedleXREventArgs) {
32
- if (!this.activeAndEnabled) return;
33
- if (debug) console.warn("AVATAR ENTER XR", this.guid, this.sourceId, this, this.activeAndEnabled)
34
- if (this._syncTransforms)
35
- this._syncTransforms.length = 0;
36
- await this.prepareAvatar();
37
-
38
- const playerstate = PlayerState.getFor(this);
39
- if (playerstate?.owner) {
40
- const marker = this.gameObject.addNewComponent(AvatarMarker)!;
41
- marker.avatar = this.gameObject;
42
- marker.connectionId = playerstate.owner;
43
- }
44
- else if(this.context.connection.isConnected) console.error("No player state found for avatar", this);
45
- }
46
-
47
- onLeaveXR(_args: NeedleXREventArgs): void {
48
- const marker = this.gameObject.getComponent(AvatarMarker);
49
- if (marker) {
50
- marker.destroy();
51
- }
52
- }
53
-
54
- onUpdateXR(args: NeedleXREventArgs): void {
55
- if (!this.activeAndEnabled) return;
56
-
57
- const isLocalPlayer = PlayerState.isLocalPlayer(this);
58
- if (!isLocalPlayer) return;
59
-
60
- const xr = args.xr;
61
- // make sure the avatar is inside the active rig
62
- if (xr.rig && xr.rig.gameObject !== this.gameObject.parent) {
63
- this.gameObject.position.set(0, 0, 0);
64
- this.gameObject.rotation.set(0, 0, 0);
65
- this.gameObject.scale.set(1, 1, 1);
66
- xr.rig.gameObject.add(this.gameObject);
67
- }
68
- // this.gameObject.position.copy(xr.rig!.gameObject.position);
69
- // this.gameObject.quaternion.copy(xr.rig!.gameObject.quaternion);
70
- // this.gameObject.scale.set(1, 1, 1);
71
-
72
-
73
- if (this._syncTransforms && isLocalPlayer) {
74
- for (const sync of this._syncTransforms) {
75
- sync.fastMode = true;
76
- if (!sync.isOwned())
77
- sync.requestOwnership();
78
- }
79
- }
80
-
81
-
82
- // synchronize head
83
- if (this.head && this.context.mainCamera) {
84
- const headObj = this.head.asset as IGameObject;
85
- headObj.position.copy(this.context.mainCamera.position);
86
- headObj.quaternion.copy(this.context.mainCamera.quaternion);
87
- headObj.quaternion.x *= -1;
88
-
89
- // HACK: XRFlag limitation workaround to make sure first person user head is never rendered
90
- if (this.context.time.frameCount % 10 === 0) {
91
- const xrflags = GameObject.getComponentsInChildren(this.head.asset, XRFlag);
92
- for (const flag of xrflags) {
93
- flag.enabled = false;
94
- flag.gameObject.visible = false;
95
- }
96
- }
97
- }
98
-
99
- // synchronize hands
100
- const leftCtrl = args.xr.leftController;
101
- const leftObj = this.leftHand?.asset as Object3D;
102
- if (leftCtrl && leftObj) {
103
- leftObj.position.copy(leftCtrl.gripPosition);
104
- leftObj.quaternion.copy(leftCtrl.gripQuaternion);
105
- leftObj.quaternion.multiply(flipForwardQuaternion);
106
- leftObj.visible = leftCtrl.isTracking;
107
- }
108
-
109
- const right = args.xr.rightController;
110
- if (right && this.rightHand?.asset) {
111
- const rightObj = this.rightHand.asset as Object3D;
112
- rightObj.position.copy(right.gripPosition);
113
- rightObj.quaternion.copy(right.gripQuaternion);
114
- rightObj.quaternion.multiply(flipForwardQuaternion);
115
- rightObj.visible = right.isTracking;
116
- }
117
- }
118
-
119
- onBeforeRender(): void {
120
- if (this.context.time.frame % 10 === 0)
121
- this.updateRemoteAvatarVisibility();
122
- }
123
-
124
-
125
- private updateRemoteAvatarVisibility() {
126
- if (this.context.connection.isConnected) {
127
- const state = PlayerState.getFor(this);
128
- if (state && state.isLocalPlayer == false) {
129
-
130
- const sync = NeedleXRSession.getXRSync(this.context);
131
- if (sync) {
132
- if (sync.hasState(state.owner)) {
133
- this.tryFindAvatarObjectsIfMissing();
134
-
135
- const leftObj = this.leftHand?.asset as Object3D;
136
- if (leftObj) {
137
- leftObj.visible = sync?.isTracking(state.owner, "left") ?? false;
138
- }
139
- const rightObj = this.rightHand?.asset as Object3D;
140
- if (rightObj) {
141
- rightObj.visible = sync?.isTracking(state.owner, "right") ?? false;
142
- }
143
- }
144
- }
145
-
146
- // HACK: XRFlag limitation workaround to make sure first person user head of OTHER users is ALWAYS rendered
147
- if (this.head?.asset) {
148
- const xrflags = GameObject.getComponentsInChildren(this.head.asset, XRFlag);
149
- for (const flag of xrflags) {
150
- flag.enabled = false;
151
- flag.gameObject.visible = true;
152
- }
153
- }
154
- }
155
- }
156
- }
157
-
158
-
159
-
160
- private tryFindAvatarObjectsIfMissing() {
161
- // if no avatar objects are set, try to find them
162
- if (!this.head || !this.leftHand || !this.rightHand) {
163
- const res = { head: this.head, leftHand: this.leftHand, rightHand: this.rightHand };
164
- NeedleXRUtils.tryFindAvatarObjects(this.gameObject, this.sourceId || "", res);
165
- if (res.head) this.head = res.head;
166
- if (res.leftHand) this.leftHand = res.leftHand;
167
- if (res.rightHand) this.rightHand = res.rightHand;
168
- }
169
- }
170
-
171
- private async prepareAvatar() {
172
- // if no avatar objects are set, try to find them
173
- this.tryFindAvatarObjectsIfMissing();
174
-
175
- if (!this.head) {
176
- const head = new Object3D();
177
- head.name = "Head";
178
- const cube = ObjectUtils.createPrimitive(PrimitiveType.Cube);
179
- head.add(cube);
180
- this.gameObject.add(head);
181
- this.head = new AssetReference("", this.sourceId, head);
182
- if (debug) console.log("Create head", head);
183
- }
184
-
185
- if (!this.rightHand) {
186
- const rightHand = new Object3D();
187
- rightHand.name = "Right Hand";
188
- this.gameObject.add(rightHand);
189
- this.rightHand = new AssetReference("", this.sourceId, rightHand);
190
- if (debug) console.log("Create right hand", rightHand);
191
- }
192
-
193
- if (!this.leftHand) {
194
- const leftHand = new Object3D();
195
- leftHand.name = "Left Hand";
196
- this.gameObject.add(leftHand);
197
- this.leftHand = new AssetReference("", this.sourceId, leftHand);
198
- if (debug) console.log("Create left hand", leftHand);
199
- }
200
-
201
- await this.loadAvatarObjects(this.head, this.leftHand, this.rightHand);
202
-
203
- if (PlayerState.isLocalPlayer(this.gameObject)) {
204
- this._syncTransforms = GameObject.getComponentsInChildren(this.gameObject, SyncedTransform);
205
- }
206
- }
207
-
208
-
209
- private async loadAvatarObjects(head: AssetReference, left: AssetReference, right: AssetReference) {
210
- const pHead = head.loadAssetAsync();
211
- const pHandLeft = left.loadAssetAsync();
212
- const pHandRight = right.loadAssetAsync();
213
- const promises = new Array<Promise<any>>();
214
- if (pHead) promises.push(pHead);
215
- if (pHandLeft) promises.push(pHandLeft);
216
- if (pHandRight) promises.push(pHandRight);
217
- const res = await PromiseAllWithErrors(promises);
218
- if (debug) console.log("Avatar loaded results:", res);
219
- }
220
- }
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 "../webxr/XRFlag.js";
3
+ import { XRFlag, XRState } from "../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: Object3D | null = null;
41
+ shadowComponent: ThreeMeshUI.Block | 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: ThreeMeshUI.MeshUIBaseElement | Object3D | null | undefined) {
151
+ protected setShadowComponentOwner(current: 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) return;
123
+ if (!this.interactable || args.pointerId !== 0) return;
124
124
 
125
- if (args.button !== 0 && args.event.pointerType === PointerType.Mouse) return;
126
125
  // Button clicks should only run with left mouse button while using mouse
126
+ if(args.pointerId !== 0 && this.context.input.getIsMouse(args.pointerId)) return;
127
127
  if (debug) {
128
128
  console.warn("Button Click", this.onClick);
129
129
  showBalloonMessage("CLICKED button " + this.name + " at " + this.context.time.frameCount);
src/engine-components/Camera.ts CHANGED
@@ -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 } from "../engine/engine_setup.js";
5
+ import { Context, XRSessionMode } 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,6 +350,7 @@
350
350
  if (this._backgroundBlurriness !== undefined)
351
351
  this.context.scene.backgroundBlurriness = this._backgroundBlurriness;
352
352
  if (this._backgroundIntensity !== undefined)
353
+ //@ts-ignore
353
354
  this.context.scene.backgroundIntensity = this._backgroundIntensity;
354
355
 
355
356
  break;
@@ -391,7 +392,7 @@
391
392
  if (debug)
392
393
  showBalloonMessage("Environment blend mode: " + environmentBlendMode + " on " + navigator.userAgent);
393
394
  let transparent = environmentBlendMode === 'additive' || environmentBlendMode === 'alpha-blend';
394
- if (context.isInAR) {
395
+ if (context.xrSessionMode === XRSessionMode.ImmersiveAR) {
395
396
  if (environmentBlendMode === "opaque") {
396
397
  // workaround for Quest 2 returning opaque when it should be alpha-blend
397
398
  // check user agent if this is the Quest browser and return true if so
src/engine-components/ui/Canvas.ts CHANGED
@@ -12,7 +12,6 @@
12
12
  import { getParam } from "../../engine/engine_utils.js";
13
13
  import { LayoutGroup } from "./Layout.js";
14
14
  import { Mathf } from "../../engine/engine_math.js";
15
- import { NeedleXREventArgs } from "../../engine/xr/index.js";
16
15
 
17
16
  export enum RenderMode {
18
17
  ScreenSpaceOverlay = 0,
@@ -201,30 +200,19 @@
201
200
  }
202
201
  }
203
202
 
204
- onEnterXR(args: NeedleXREventArgs): void {
205
- if (this.screenspace) {
206
- if (args.xr.isVR || args.xr.isPassThrough) {
207
- this.gameObject.visible = false;
208
- }
209
- }
210
- }
211
- onLeaveXR(args: NeedleXREventArgs): void {
212
- if (this.screenspace) {
213
- if (args.xr.isVR || args.xr.isPassThrough) {
214
- this.gameObject.visible = true;
215
- }
216
- }
217
- }
218
-
219
203
  onBeforeRenderRoutine = () => {
220
- this.previousParent = this.gameObject.parent;
221
- if ((this.context.xr?.isVR || this.context.xr?.isPassThrough) && this.screenspace) {
222
- // see https://linear.app/needle/issue/NE-4114
223
- this.gameObject.visible = false;
224
- this.gameObject.removeFromParent();
204
+ if (this.context.isInVR) {
205
+ this.onUpdateRenderMode();
206
+ this.handleLayoutUpdates();
207
+ // TODO TMUI @swingingtom - For VR this is so we don't have text clipping
208
+ this.shadowComponent?.updateMatrixWorld(true);
209
+ this.shadowComponent?.updateWorldMatrix(true, true);
210
+ this.invokeBeforeRenderEvents();
211
+ EventSystem.ensureUpdateMeshUI(ThreeMeshUI, this.context, true);
225
212
  return;
226
213
  }
227
214
 
215
+ this.previousParent = this.gameObject.parent;
228
216
  // console.log(this.previousParent?.name + "/" + this.gameObject.name);
229
217
 
230
218
  if (this.renderOnTop || this.screenspace) {
@@ -243,12 +231,7 @@
243
231
  }
244
232
 
245
233
  onAfterRenderRoutine = () => {
246
- if ((this.context.xr?.isVR || this.context.xr?.isPassThrough) && this.screenspace) {
247
- this.previousParent?.add(this.gameObject);
248
- // this is currently causing an error during XR (https://linear.app/needle/issue/NE-4114)
249
- // this.gameObject.visible = true;
250
- return;
251
- }
234
+ if(this.context.isInVR) return;
252
235
  if ((this.screenspace || this.renderOnTop) && this.previousParent && this.context.mainCamera) {
253
236
  if (this.screenspace) {
254
237
  const camObj = this.context.mainCamera;
@@ -293,7 +276,7 @@
293
276
  for (const ch of this._rectTransforms) {
294
277
  if (matrixWorldChanged) ch.markDirty();
295
278
  let layout = this._layoutGroups.get(ch.gameObject);
296
- if (ch.isDirty && !layout) {
279
+ if(ch.isDirty && !layout){
297
280
  layout = ch.gameObject.getComponentInParent(LayoutGroup) as LayoutGroup;
298
281
  }
299
282
  if (ch.isDirty || layout?.isDirty) {
src/engine-components/Component.ts CHANGED
@@ -10,8 +10,6 @@
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";
15
13
 
16
14
  // export interface ISerializationCallbackReceiver {
17
15
  // onBeforeSerialize?(): object | void;
@@ -296,7 +294,7 @@
296
294
  abstract set worldQuaternion(val: Quaternion);
297
295
  abstract get worldQuaternion(): Quaternion;
298
296
  abstract set worldRotation(val: Vector3);
299
- abstract get worldRotation(): Vector3;
297
+ abstract get worldRotation(): Vector3;
300
298
  abstract set worldScale(val: Vector3);
301
299
  abstract get worldScale(): Vector3;
302
300
 
@@ -307,22 +305,17 @@
307
305
 
308
306
 
309
307
 
310
- export abstract class Component implements IComponent, EventTarget,
311
- Partial<INeedleXRSessionEventReceiver>,
312
- Partial<IPointerEventHandler>
313
- {
308
+ export class Component implements IComponent, EventTarget {
314
309
 
315
310
  get isComponent(): boolean { return true; }
316
311
 
317
312
  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 */
319
313
  get context(): Context {
320
314
  return this.__context ?? Context.Current;
321
315
  }
322
316
  set context(context: Context) {
323
317
  this.__context = context;
324
318
  }
325
- /** shorthand for `this.context.scene` */
326
319
  get scene(): Scene { return this.context.scene; }
327
320
 
328
321
  get layer(): number {
@@ -362,7 +355,7 @@
362
355
  return this.gameObject?.userData.hideFlags;
363
356
  }
364
357
 
365
- /** @returns true if the object is enabled and active in the hierarchy */
358
+
366
359
  get activeAndEnabled(): boolean {
367
360
  if (this.destroyed) return false;
368
361
  if (this.__isEnabled === false) return false;
@@ -392,27 +385,19 @@
392
385
  this.gameObject[activeInHierarchyFieldName] = val;
393
386
  }
394
387
 
395
- /** the object this component is attached to. Note that this is a threejs Object3D with some additional features */
396
388
  gameObject!: GameObject;
397
- /** the unique identifier for this component */
398
389
  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) */
400
390
  sourceId?: SourceIdentifier;
401
391
  // transform: Object3D = nullObject;
402
392
 
403
393
  /** called on a component with a map of old to new guids (e.g. when instantiate generated new guids and e.g. timeline track bindings needs to remape them) */
404
394
  resolveGuids?(guidsMap: GuidsMap): void;
405
395
 
406
- /** called once when the component becomes active for the first time (once per component)
407
- * This is the first callback to be called */
396
+ /** called once when the component becomes active for the first time */
408
397
  awake() { }
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
- */
398
+ /** called every time when the component gets enabled (this is invoked after awake and before start) */
412
399
  onEnable() { }
413
- /** called every time the component gets disabled or if a parent object (or this.gameObject) gets set to invisible */
414
400
  onDisable() { }
415
- /** Called when the component gets destroyed */
416
401
  onDestroy() {
417
402
  this.__destroyed = true;
418
403
  }
@@ -424,17 +409,11 @@
424
409
  /** Called for all scripts when the context gets paused or unpaused */
425
410
  onPausedChanged?(isPaused: boolean, wasPaused: boolean): void;
426
411
 
427
- /** called at the beginning of a frame (once per component) */
428
412
  start?(): void;
429
- /** first callback in a frame (called every frame when implemented) */
430
413
  earlyUpdate?(): void;
431
- /** regular callback in a frame (called every frame when implemented) */
432
414
  update?(): void;
433
- /** late callback in a frame (called every frame when implemented) */
434
415
  lateUpdate?(): void;
435
- /** called before the scene gets rendered in the main update loop */
436
416
  onBeforeRender?(frame: XRFrame | null): void;
437
- /** called after the scene was rendered */
438
417
  onAfterRender?(): void;
439
418
 
440
419
  onCollisionEnter?(col: Collision);
@@ -445,79 +424,18 @@
445
424
  onTriggerStay?(col: ICollider);
446
425
  onTriggerExit?(col: ICollider);
447
426
 
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
- */
501
427
  startCoroutine(routine: Generator, evt: FrameEvent = FrameEvent.Update): Generator {
502
428
  return this.context.registerCoroutineUpdate(this, routine, evt);
503
429
  }
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
- */
430
+
509
431
  stopCoroutine(routine: Generator, evt: FrameEvent = FrameEvent.Update): void {
510
432
  this.context.unregisterCoroutineUpdate(routine, evt);
511
433
  }
512
434
 
513
- /** @returns true if this component was destroyed (`this.destroy()`) or the whole object this component was part of */
514
435
  public get destroyed(): boolean {
515
436
  return this.__destroyed;
516
437
  }
517
438
 
518
- /**
519
- * Destroys this component (and removes it from the object)
520
- */
521
439
  public destroy() {
522
440
  if (this.__destroyed) return;
523
441
  this.__internalDestroy();
@@ -748,7 +666,5 @@
748
666
  }
749
667
  }
750
668
 
751
-
752
-
753
669
  export class Behaviour extends Component {
754
670
  }
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";
14
15
  export { AudioExtension } from "../export/usdz/extensions/behavior/AudioExtension.js";
15
16
  export { AudioListener } from "../AudioListener.js";
16
17
  export { AudioSource } from "../AudioSource.js";
17
18
  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,6 +51,7 @@
51
51
  export { ColorAdjustments } from "../postprocessing/Effects/ColorAdjustments.js";
52
52
  export { ColorBySpeedModule } from "../ParticleSystemModules.js";
53
53
  export { ColorOverLifetimeModule } from "../ParticleSystemModules.js";
54
+ export { Component } from "../Component.js";
54
55
  export { ContactShadows } from "../ContactShadows.js";
55
56
  export { ControlTrackHandler } from "../timeline/TimelineTracks.js";
56
57
  export { CustomBranding } from "../export/usdz/USDZExporter.js";
@@ -87,6 +88,7 @@
87
88
  export { Image } from "../ui/Image.js";
88
89
  export { InheritVelocityModule } from "../ParticleSystemModules.js";
89
90
  export { InputField } from "../ui/InputField.js";
91
+ export { Interactable } from "../Interactable.js";
90
92
  export { Light } from "../Light.js";
91
93
  export { LimitVelocityOverLifetimeModule } from "../ParticleSystemModules.js";
92
94
  export { LODGroup } from "../LODGroup.js";
@@ -100,7 +102,6 @@
100
102
  export { MeshRenderer } from "../Renderer.js";
101
103
  export { MinMaxCurve } from "../ParticleSystemModules.js";
102
104
  export { MinMaxGradient } from "../ParticleSystemModules.js";
103
- export { NeedleWebXRHtmlElement } from "../webxr/WebXRButtons.js";
104
105
  export { NestedGltf } from "../NestedGltf.js";
105
106
  export { Networking } from "../Networking.js";
106
107
  export { NoiseModule } from "../ParticleSystemModules.js";
@@ -124,6 +125,7 @@
124
125
  export { PreliminaryAction } from "../export/usdz/extensions/behavior/BehaviourComponents.js";
125
126
  export { PreliminaryTrigger } from "../export/usdz/extensions/behavior/BehaviourComponents.js";
126
127
  export { RawImage } from "../ui/Image.js";
128
+ export { Raycaster } from "../ui/Raycaster.js";
127
129
  export { Rect } from "../ui/RectTransform.js";
128
130
  export { RectTransform } from "../ui/RectTransform.js";
129
131
  export { ReflectionProbe } from "../ReflectionProbe.js";
@@ -151,7 +153,6 @@
151
153
  export { SizeOverLifetimeModule } from "../ParticleSystemModules.js";
152
154
  export { SkinnedMeshRenderer } from "../Renderer.js";
153
155
  export { SmoothFollow } from "../SmoothFollow.js";
154
- export { SpatialGrabRaycaster } from "../ui/Raycaster.js";
155
156
  export { SpatialHtml } from "../ui/SpatialHtml.js";
156
157
  export { SpatialTrigger } from "../SpatialTrigger.js";
157
158
  export { SpatialTriggerReceiver } from "../SpatialTrigger.js";
@@ -166,7 +167,7 @@
166
167
  export { SyncedRoom } from "../SyncedRoom.js";
167
168
  export { SyncedTransform } from "../SyncedTransform.js";
168
169
  export { TapGestureTrigger } from "../export/usdz/extensions/behavior/BehaviourComponents.js";
169
- export { TeleportTarget } from "../webxr/TeleportTarget.js";
170
+ export { TeleportTarget } from "../webxr/WebXRController.js";
170
171
  export { TestRunner } from "../TestRunner.js";
171
172
  export { TestSimulateUserData } from "../TestRunner.js";
172
173
  export { Text } from "../ui/Text.js";
@@ -196,16 +197,20 @@
196
197
  export { Volume } from "../postprocessing/Volume.js";
197
198
  export { VolumeParameter } from "../postprocessing/VolumeParameter.js";
198
199
  export { VolumeProfile } from "../postprocessing/VolumeProfile.js";
200
+ export { VRUserState } from "../webxr/WebXRSync.js";
201
+ export { WebAR } from "../webxr/WebXR.js";
199
202
  export { WebARCameraBackground } from "../webxr/WebARCameraBackground.js";
200
203
  export { WebARSessionRoot } from "../webxr/WebARSessionRoot.js";
201
204
  export { WebXR } from "../webxr/WebXR.js";
205
+ export { WebXRAvatar } from "../webxr/WebXRAvatar.js";
206
+ export { WebXRController } from "../webxr/WebXRController.js";
202
207
  export { WebXRImageTracking } from "../webxr/WebXRImageTracking.js";
203
208
  export { WebXRImageTrackingModel } from "../webxr/WebXRImageTracking.js";
204
209
  export { WebXRPlaneTracking } from "../webxr/WebXRPlaneTracking.js";
210
+ export { WebXRSync } from "../webxr/WebXRSync.js";
205
211
  export { WebXRTrackedImage } from "../webxr/WebXRImageTracking.js";
206
- export { XRControllerFollow } from "../webxr/controllers/XRControllerFollow.js";
207
- export { XRControllerModel } from "../webxr/controllers/XRControllerModel.js";
208
- export { XRControllerMovement } from "../webxr/controllers/XRControllerMovement.js";
209
- export { XRFlag } from "../webxr/XRFlag.js";
212
+ export { XRFlag } from "../XRFlag.js";
213
+ export { XRGrabModel } from "../webxr/WebXRGrabRendering.js";
214
+ export { XRGrabRendering } from "../webxr/WebXRGrabRendering.js";
210
215
  export { XRRig } from "../webxr/WebXRRig.js";
211
- export { XRState } from "../webxr/XRFlag.js";
216
+ export { XRState } from "../XRFlag.js";
src/engine/debug/debug_console.ts CHANGED
@@ -1,7 +1,6 @@
1
- import { getErrorCount, makeErrorsVisibleForDevelopment } from "./debug_overlay.js";
2
- import { getParam, isMobileDevice, isQuest } from "../engine_utils.js";
1
+ import { getErrorCount } from "./debug_overlay.js";
2
+ import { getParam, isMobileDevice } from "../engine_utils.js";
3
3
  import { isLocalNetwork } from "../engine_networking_utils.js";
4
- import { isDevEnvironment } from "./debug.js";
5
4
 
6
5
  let consoleInstance: any = null;
7
6
  let consoleHtmlElement: HTMLElement | null = null;
@@ -23,11 +22,8 @@
23
22
  currentUrl.searchParams.set("console", "1");
24
23
  console.log("🌵 Tip: You can add the \"?console\" query parameter to the url to show the debug console (on mobile it will automatically open in the bottom right corner when your get errors during development)", "\nOpen this page console: " + currentUrl.toString());
25
24
  }
26
- const isMobile = isMobileDevice() || (isQuest() && isDevEnvironment());
25
+ const isMobile = isMobileDevice();
27
26
  if (isMobile) {
28
- // we need to invoke this here - otherwise we will miss errors that happen after the console is loaded
29
- // and calling the method from the root needle-engine.ts import is evaluated later (if we import the method from the toplevel file and then invoke it)
30
- makeErrorsVisibleForDevelopment();
31
27
  beginWatchingLogs();
32
28
  createConsole(true);
33
29
  if (isMobile) {
@@ -195,7 +191,7 @@
195
191
  }
196
192
  `;
197
193
  consoleHtmlElement?.prepend(styles);
198
- if (startHidden === true && getErrorCount() <= 0)
194
+ if (startHidden === true)
199
195
  hideDebugConsole();
200
196
  console.log("🌵 Debug console has loaded");
201
197
  }
src/engine/debug/debug_overlay.ts CHANGED
@@ -15,7 +15,7 @@
15
15
  }
16
16
 
17
17
  export function getErrorCount() {
18
- return _errorCount;
18
+ return errorCount;
19
19
  }
20
20
 
21
21
  const originalConsoleError = console.error;
@@ -37,10 +37,9 @@
37
37
  if (hide) return;
38
38
  const isLocal = isLocalNetwork();
39
39
  if (debug) console.log("Is this a local network?", isLocal);
40
- if (isLocal)
41
- {
40
+ if (isLocal) {
42
41
  if (debug)
43
- console.warn("Patch console", window.location.hostname);
42
+ console.log(window.location.hostname);
44
43
  console.error = patchedConsoleError;
45
44
  window.addEventListener("error", (event) => {
46
45
  if (hide) return;
@@ -67,10 +66,10 @@
67
66
  }
68
67
 
69
68
 
70
- let _errorCount = 0;
69
+ let errorCount = 0;
71
70
 
72
71
  function onReceivedError() {
73
- _errorCount += 1;
72
+ errorCount += 1;
74
73
  }
75
74
 
76
75
  function onParseError(args: Array<any>) {
src/engine-components/DragControls.ts CHANGED
@@ -1,125 +1,104 @@
1
- import { Behaviour, GameObject } from "./Component.js";
1
+ import { GameObject } from "./Component.js";
2
2
  import { SyncedTransform } from "./SyncedTransform.js";
3
- import type { IPointerEventHandler, PointerEventData } from "./ui/PointerEvents.js";
3
+ import type { IPointerDownHandler, IPointerEnterHandler, IPointerEventHandler, IPointerExitHandler, IPointerUpHandler, PointerEventData } from "./ui/PointerEvents.js";
4
4
  import { Context } from "../engine/engine_setup.js";
5
- import { UsageMarker } from "./Interactable.js";
5
+ import { Interactable, UsageMarker } from "./Interactable.js";
6
6
  import { Rigidbody } from "./RigidBody.js";
7
+ import { WebXR } from "./webxr/WebXR.js";
7
8
  import { Avatar_POI } from "./avatar/Avatar_Brain_LookAt.js";
8
9
  import { RaycastOptions } from "../engine/engine_physics.js";
9
10
  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";
10
13
  import { InstancingUtil } from "../engine/engine_instancing.js";
11
14
  import { OrbitControls } from "./OrbitControls.js";
12
- import { AxesHelper, Box3, BufferGeometry, Camera, Color, Event, Line, LineBasicMaterial, Matrix3, Matrix4, Mesh, MeshBasicMaterial, Object3D, Plane, PlaneHelper, Quaternion, Ray, Raycaster, SphereGeometry, Vector3 } from "three";
15
+ import { BufferGeometry, Camera, Color, Line, LineBasicMaterial, Matrix4, Mesh, MeshBasicMaterial, Object3D, Plane, Ray, Raycaster, SphereGeometry, Vector2, Vector3 } from "three";
13
16
  import { ObjectRaycaster } from "./ui/Raycaster.js";
14
17
  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";
20
18
 
21
- const debug = getParam("debugdrag");
19
+ const debug = false;
22
20
 
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,
21
+ export enum DragEvents {
22
+ SelectStart = "selectstart",
23
+ SelectEnd = "selectend",
34
24
  }
35
25
 
36
- export class DragControls extends Behaviour implements IPointerEventHandler {
26
+ interface SelectArgs {
27
+ selected: Object3D;
28
+ attached: Object3D | GameObject | null;
29
+ }
37
30
 
38
- // dragPlane (floor, object, view)
39
- // snap to surface (snap orientation?)
40
- // two-handed drag (scale, rotate, move)
41
- // keep upright (no tilt)
42
31
 
43
- /** How and where the object is dragged along. */
44
- @serializable()
45
- public dragMode: DragMode = DragMode.DynamicViewAngle;
32
+ export interface IDragEventListener {
33
+ onDragStart?();
34
+ onDragEnd?();
35
+ }
46
36
 
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;
37
+ export class DragControls extends Interactable implements IPointerEventHandler {
58
38
 
59
- /** Keep the original rotation of the dragged object while dragging in XR. */
60
- @serializable()
61
- public xrKeepRotation: boolean = false;
39
+ private static _active: number = 0;
40
+ public static get HasAnySelected(): boolean { return this._active > 0; }
62
41
 
63
- /** Accelerate dragging objects closer / further away when in XR */
42
+ /** Show's drag gizmos when enabled */
64
43
  @serializable()
65
- public xrDistanceDragFactor: number = 1;
44
+ public showGizmo: boolean = true;
66
45
 
67
- /** When enabled, draws a line from the dragged object downwards to the next raycast hit. */
46
+ /** When enabled DragControls will drag vertically when the object is viewed from a low angle */
68
47
  @serializable()
69
- public showGizmo: boolean = false;
48
+ public useViewAngle: boolean = true;
70
49
 
71
- // future:
72
- // constraints?
50
+ public transformSelf: boolean = true;
51
+ // public transformGroup: boolean = true;
52
+ // public targets: Object3D[] | null = null;
73
53
 
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;
54
+ // private controls: Control | null = null;
79
55
  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;
90
56
 
91
- setTargetObject(obj: Object3D | null) {
92
- this.targetObject = obj as GameObject;
93
- for (const handler of this._dragHandlers.values()) {
94
- handler.setTargetObject(obj);
95
- }
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();
96
66
  }
97
67
 
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 = [];
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
+ }
108
79
  }
109
80
 
81
+
82
+
110
83
  start() {
111
84
  this.orbit = GameObject.findObjectOfType(OrbitControls, this.context);
112
- if (!this.gameObject.getComponentInParent(ObjectRaycaster))
85
+ if (!this.gameObject.getComponentInParent(ObjectRaycaster)) {
113
86
  this.gameObject.addNewComponent(ObjectRaycaster);
87
+ }
114
88
  }
115
89
 
90
+ private static lastHovered: Object3D;
91
+ private _draggingRigidbodies: Rigidbody[] = [];
92
+
116
93
  private allowEdit(_obj: Object3D | null = null) {
117
94
  return this.context.connection.allowEditing;
118
95
  }
119
96
 
120
97
  onPointerEnter(evt: PointerEventData) {
121
98
  if (!this.allowEdit(this.gameObject)) return;
122
- if (evt.mode !== "screen") return;
99
+ if (WebXR.IsInWebXR) return;
100
+ // const interactable = GameObject.getComponentInParent(evt.object, Interactable);
101
+ // if (!interactable) return;
123
102
  const dc = GameObject.getComponentInParent(evt.object, DragControls);
124
103
  if (!dc || dc !== this) return;
125
104
  DragControls.lastHovered = evt.object;
@@ -128,120 +107,83 @@
128
107
 
129
108
  onPointerExit(evt: PointerEventData) {
130
109
  if (!this.allowEdit(this.gameObject)) return;
131
- if (evt.mode !== "screen") return;
110
+ if (WebXR.IsInWebXR) return;
132
111
  if (DragControls.lastHovered !== evt.object) return;
112
+ // const interactable = GameObject.getComponentInParent(evt.object, Interactable);
113
+ // if (!interactable) return;
133
114
  this.context.domElement.style.cursor = 'auto';
134
115
  }
135
116
 
117
+ private _waitingForDragStart: PointerEventData | null = null;
118
+
136
119
  onPointerDown(args: PointerEventData) {
137
120
  if (!this.allowEdit(this.gameObject)) return;
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
- }
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;
170
130
  }
171
131
 
172
132
  onPointerMove(args: PointerEventData) {
173
- if (this._isDragging || this._potentialDragStartEvt !== null) args.use();
133
+ if(this._isDragging || this._waitingForDragStart !== null) args.use();
174
134
  }
175
135
 
176
136
  onPointerUp(args: PointerEventData) {
177
-
178
- if(debug) Gizmos.DrawLabel(args.point ?? this.gameObject.worldPosition, "POINTERUP:" + args.pointerId + ", " + args.button, .03, 3);
179
-
137
+ this._waitingForDragStart = null;
180
138
  if (!this.allowEdit(this.gameObject)) return;
181
- if (args.button !== 0) return;
182
- this._potentialDragStartEvt = null;
139
+ if (DragControls._active > 0)
140
+ DragControls._active -= 1;
141
+ if (WebXR.IsInWebXR) return;
142
+ this.onDragEnd(args);
143
+ args.stopPropagation();
144
+ if (this.orbit) this.orbit.enabled = true;
145
+ }
183
146
 
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
147
 
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
- }
208
- }
209
-
210
148
  update(): void {
149
+ if (WebXR.IsInWebXR) return;
211
150
 
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
-
219
151
  // drag start only after having dragged for some pixels
220
- if (this._potentialDragStartEvt) {
152
+ if (this._waitingForDragStart) {
221
153
  if (!this._didDrag) {
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)
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)
225
160
  this._didDrag = true;
226
161
  else return;
227
162
  }
228
- const args = this._potentialDragStartEvt;
229
- this._potentialDragStartEvt = null;
230
- this.onFirstDragStart(args);
163
+ const args = this._waitingForDragStart;
164
+ this._waitingForDragStart = null;
165
+ this.onDragStart(args);
231
166
  }
232
167
 
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();
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
+ }
238
175
  }
239
176
 
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) {
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) {
242
184
  if (!this._dragHelper) {
243
185
  if (this.context.mainCamera)
244
- this._dragHelper = new LegacyDragVisualsHelper(this.context.mainCamera);
186
+ this._dragHelper = new DragHelper(this.context.mainCamera);
245
187
  else
246
188
  return;
247
189
  }
@@ -250,17 +192,46 @@
250
192
  const dc = GameObject.getComponentInParent(evt.object, DragControls);
251
193
  if (!dc || dc !== this) return;
252
194
 
253
- const object = this.targetObject || this.gameObject;
254
195
 
255
- if (!object) return;
196
+ let object: Object3D = evt.object;
256
197
 
198
+ if (this.transformSelf) {
199
+ object = this.gameObject;
200
+ }
201
+
202
+ // raise event
203
+ const args: { selected: Object3D, attached: Object3D | null } = { selected: object, attached: object };
204
+ for (const listener of this.selectStartEventListener) {
205
+ listener(this, args);
206
+ }
207
+
208
+ this._activePointerId = evt.pointerId;
209
+
210
+ if (!args.attached) return;
211
+ if (args.attached !== object) {
212
+ // // if duplicatable changes the object being dragged
213
+ // // should it also change the active drag controls (e.g. if it has a own one)
214
+ // const drag = GameObject.getComponentInParent(args.attached, DragControls);
215
+ // if (drag && drag !== this) {
216
+ // // incredibly ugly code to pass the drag controls event to another drag controls instance
217
+ // // This is necessary since we dont call the onPointerUp events anymore for all objects
218
+ // // that have previously received the onPointerDown event.
219
+ // // NOTE: added the EventSystem.raisedPointerDownEvents array again because of this uglyness here. The code was originally removed in 757fc5e5bafd02aa13d6cd35dd5e8729c841465a and now we're adding it in 8ce886d8344d1abd5ebb89ae3e1fb8d6d47293da
220
+ // this.onDragEnd(null);
221
+ // drag.onPointerDown(evt);
222
+ // evt.object = args.attached;
223
+ // drag.onDragStart(evt);
224
+ // return;
225
+ // }
226
+ }
227
+ object = args.attached;
257
228
  this._isDragging = true;
258
229
  this._dragHelper.setSelected(object, this.context);
259
230
  if (this.orbit) this.orbit.enabled = false;
260
231
 
261
232
  const sync = GameObject.getComponentInChildren(object, SyncedTransform);
262
- if (debug) console.log("DRAG START", sync, object);
263
-
233
+ if (debug)
234
+ console.log("DRAG START", sync, object);
264
235
  if (sync) {
265
236
  sync.fastMode = true;
266
237
  sync?.requestOwnership();
@@ -268,31 +239,30 @@
268
239
 
269
240
  this._marker = GameObject.addNewComponent(object, UsageMarker);
270
241
 
242
+ // console.log(object, this._marker);
243
+
271
244
  this._draggingRigidbodies.length = 0;
272
245
  const rbs = GameObject.getComponentsInChildren(object, Rigidbody);
273
246
  if (rbs)
274
247
  this._draggingRigidbodies.push(...rbs);
248
+
249
+ const l = nameofFactory<IDragEventListener>();
250
+ GameObject.invokeOnChildren(this._dragHelper.selected, l("onDragStart"));
275
251
  }
276
252
 
277
- /** Called each frame as long as any pointer is dragging this object. */
278
- private onAnyDragUpdate() {
253
+ private onUpdateDrag() {
279
254
  if (!this._dragHelper) return;
280
255
  this._dragHelper.showGizmo = this.showGizmo;
256
+ this._dragHelper.useViewAngle = this.useViewAngle;
281
257
 
282
258
  this._dragHelper.onUpdate(this.context);
283
259
  for (const rb of this._draggingRigidbodies) {
284
260
  rb.wakeUp();
285
261
  rb.resetVelocities();
286
- rb.resetForcesAndTorques();
287
262
  }
288
-
289
- const object = this.targetObject || this.gameObject;
290
-
291
- InstancingUtil.markDirty(object);
292
263
  }
293
264
 
294
- /** Called when the last pointer has been removed from this object. */
295
- private onLastDragEnd(evt: PointerEventData | null) {
265
+ private onDragEnd(evt: PointerEventData | null) {
296
266
  if (!this || !this._isDragging) return;
297
267
  this._isDragging = false;
298
268
  if (!this._dragHelper) return;
@@ -301,7 +271,8 @@
301
271
  }
302
272
  this._draggingRigidbodies.length = 0;
303
273
  const selected = this._dragHelper.selected;
304
- if (debug) console.log("DRAG END", selected, selected?.visible)
274
+ if (debug)
275
+ console.log("DRAG END", selected, selected?.visible)
305
276
  this._dragHelper.setSelected(null, this.context);
306
277
  if (this.orbit) this.orbit.enabled = true;
307
278
  if (evt?.object) {
@@ -311,751 +282,23 @@
311
282
  // sync?.requestOwnership();
312
283
  }
313
284
  }
314
- if (this._marker)
285
+ if (this._marker) {
315
286
  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;
353
287
  }
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);
288
+ // raise event
289
+ for (const listener of this.selectEndEventListener) {
290
+ listener(this);
362
291
  }
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);
378
292
 
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
- }
293
+ const l = nameofFactory<IDragEventListener>();
294
+ GameObject.invokeOnChildren(selected, l("onDragEnd"));
400
295
  }
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
- }
547
296
  }
548
297
 
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
- }
563
298
 
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 {
568
299
 
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; }
300
+ class DragHelper {
574
301
 
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
-
1059
302
  showGizmo: boolean = true;
1060
303
  useViewAngle: boolean = true;
1061
304
 
@@ -1093,12 +336,13 @@
1093
336
  constructor(camera: Camera) {
1094
337
  this._camera = camera;
1095
338
 
1096
- const line = new Line(LegacyDragVisualsHelper.geometry);
339
+ const line = new Line(DragHelper.geometry);
1097
340
  const mat = line.material as LineBasicMaterial;
1098
341
  mat.color = new Color(.4, .4, .4);
1099
342
  line.layers.set(2);
1100
343
  line.name = 'line';
1101
344
  line.scale.y = 1;
345
+ // line.matrixAutoUpdate = false;
1102
346
  this._groundLine = line;
1103
347
 
1104
348
  const geometry = new SphereGeometry(.5, 22, 22);
@@ -1113,12 +357,13 @@
1113
357
  if (this._selected && context) {
1114
358
  for (const rb of this._rbs) {
1115
359
  rb.wakeUp();
360
+ // if (!rb.smoothedVelocity) continue;
1116
361
  rb.setVelocity(0, 0, 0);
1117
362
  }
1118
363
  }
1119
364
 
1120
365
  if (this._selected) {
1121
- // TODO move somewhere else
366
+
1122
367
  Avatar_POI.Remove(context, this._selected);
1123
368
  }
1124
369
 
@@ -1140,8 +385,6 @@
1140
385
  console.error("DragHelper: no context");
1141
386
  return;
1142
387
  }
1143
-
1144
- // TODO move somewhere else
1145
388
  Avatar_POI.Add(context, this._selected, null);
1146
389
 
1147
390
  this._groundOffsetFactor = 0;
@@ -1149,6 +392,7 @@
1149
392
  this._groundOffset.set(0, 0, 0);
1150
393
  this._requireUpdateGroundPlane = true;
1151
394
 
395
+ // this._rbs = GameObject.getComponentsInChildren(this._selected, Rigidbody);
1152
396
  this.onUpdateScreenSpacePlane();
1153
397
  }
1154
398
  }
@@ -1158,16 +402,6 @@
1158
402
  private _didDragOnGroundPlaneLastFrame: boolean = false;
1159
403
 
1160
404
  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
- /*
1171
405
  if (!this._context) return;
1172
406
 
1173
407
  const mainKey: KeyCode = "Space";
@@ -1254,7 +488,6 @@
1254
488
  this.onDidUpdate();
1255
489
  }
1256
490
  }
1257
- */
1258
491
  }
1259
492
 
1260
493
  private onUpdateWorldPosition(wp: Vector3, pointOnPlane: Vector3 | null, heightOnly: boolean) {
@@ -1316,6 +549,18 @@
1316
549
  this._groundOffset.copy(this._intersection).sub(wp);
1317
550
  }
1318
551
 
552
+ private onDidUpdate() {
553
+ // todo: when using instancing we need to mark the matrix to update
554
+ InstancingUtil.markDirty(this._selected);
555
+
556
+ for (const rb of this._rbs) {
557
+ rb.wakeUp();
558
+ rb.resetForcesAndTorques();
559
+ // rb.setBodyFromGameObject({ x: 0, y: 0, z: 0 });
560
+ rb.setAngularVelocity(0, 0, 0);
561
+ }
562
+ }
563
+
1319
564
  private contains(obj: Object3D, toSearch: Object3D): boolean {
1320
565
  if (obj === toSearch) return true;
1321
566
  if (obj.children) {
src/engine-components/Duplicatable.ts CHANGED
@@ -1,25 +1,22 @@
1
1
  import { Behaviour, GameObject } from "./Component.js";
2
- import { DragControls } from "./DragControls.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";
3
6
  import { Vector3, Quaternion, Object3D } from "three";
4
7
  import { serializable } from "../engine/engine_serialization_decorator.js";
5
8
  import { InstantiateOptions } from "../engine/engine_gameobject.js";
6
- import { IPointerEventHandler, PointerEventData } from "./ui/PointerEvents.js";
7
- import { ObjectRaycaster } from "./ui/Raycaster.js";
8
9
 
9
- export class Duplicatable extends Behaviour implements IPointerEventHandler {
10
+ export class Duplicatable extends Interactable {
10
11
 
11
- /** Duplicates will be parented into the set object. If not defined, this GameObject will be used as parent. */
12
12
  @serializable(Object3D)
13
13
  parent: GameObject | null = null;
14
-
15
- /** The object to be duplicated */
16
14
  @serializable(Object3D)
17
15
  object: GameObject | null = null;
18
16
 
19
17
  // limit max object spawn count per interval
20
18
  @serializable()
21
19
  limitCount = 10;
22
-
23
20
  @serializable()
24
21
  limitInterval = 60;
25
22
 
@@ -27,7 +24,17 @@
27
24
  private _startPosition: THREE.Vector3 | null = null;
28
25
  private _startQuaternion: THREE.Quaternion | null = null;
29
26
 
30
- start(): void {
27
+ awake(): void {
28
+ // TODO: add support to not having to assign a object to clone
29
+ // if(!this.object){
30
+ // const opts = new InstantiateOptions();
31
+ // opts.parent = this.gameObject;
32
+ // opts.idProvider = InstantiateIdProvider.createFromString(this.guid);
33
+ // const clone = GameObject.instantiate(this.gameObject, opts);
34
+ // const duplicatable =
35
+ // this.object = clone;
36
+ // }
37
+ // console.log(this, this.object);
31
38
  if (this.object) {
32
39
  if (this.object as any === this.gameObject) {
33
40
  console.error("Can not duplicate self");
@@ -41,43 +48,32 @@
41
48
  this._startQuaternion = this.object.quaternion?.clone() ?? new Quaternion(0, 0, 0, 1);
42
49
  }
43
50
 
44
- // legacy – DragControls was required for duplication and so often the component is still there; we work around that by disabling it here
45
- const dragControls = this.gameObject.getComponent(DragControls);
46
- if (dragControls) {
47
- console.warn("Please remove DragControls from object with Duplicatable component, it's not needed anymore.");
48
- dragControls.enabled = false;
51
+ const drag = GameObject.getComponentInParent(this.gameObject, DragControls);
52
+ if (drag) {
53
+ drag.addDragEventListener(DragEvents.SelectStart, (_ctrls, args) => {
54
+ if (this._currentCount >= this.limitCount) {
55
+ args.attached = null;
56
+ return;
57
+ }
58
+ const res = this.handleDuplication(args.selected);
59
+ if (res) {
60
+ console.assert(res !== args.selected, "Duplicated object is original");
61
+ args.attached = res;
62
+ }
63
+ });
49
64
  }
50
-
51
- if (!this.gameObject.getComponentInParent(ObjectRaycaster))
52
- this.gameObject.addNewComponent(ObjectRaycaster);
65
+ else console.warn("Could no find drag controls in parent", this.name);
53
66
 
54
- this.cloneLimitIntervalFn();
55
- }
56
-
57
- private _forwardPointerEvents: Map<Object3D, DragControls> = new Map();
58
-
59
- onPointerDown(args: PointerEventData) {
60
- if (!this.object) return;
61
- if (!this.context.connection.allowEditing) return;
62
- if (args.button !== 0) return;
63
-
64
- const res = this.handleDuplication();
65
- if (res) {
66
- const dragControls = GameObject.getComponent(res, DragControls);
67
- if (!dragControls) console.warn("Duplicated object does not have DragControls");
68
- else {
69
- dragControls.onPointerDown(args);
70
- this._forwardPointerEvents.set(args.event.space, dragControls);
67
+ WebXRController.addEventListener(ControllerEvents.SelectStart, (_controller: WebXRController, args: { selected: THREE.Object3D, grab: THREE.Object3D | GameObject | null }) => {
68
+ if (this._currentCount >= this.limitCount) {
69
+ args.grab = null;
70
+ return;
71
71
  }
72
- }
73
- }
72
+ const res = this.handleDuplication(args.selected);
73
+ if (res) args.grab = res;
74
+ });
74
75
 
75
- onPointerUp(args: PointerEventData) {
76
- const dragControls = this._forwardPointerEvents.get(args.event.space);
77
- if (dragControls) {
78
- dragControls.onPointerUp(args);
79
- this._forwardPointerEvents.delete(args.event.space);
80
- }
76
+ this.cloneLimitIntervalFn();
81
77
  }
82
78
 
83
79
  private cloneLimitIntervalFn() {
@@ -90,39 +86,62 @@
90
86
  }, (this.limitInterval / this.limitCount) * 1000);
91
87
  }
92
88
 
93
- private handleDuplication(): THREE.Object3D | null {
89
+ private handleDuplication(selected: THREE.Object3D): THREE.Object3D | null {
90
+ if (this._currentCount >= this.limitCount) return null;
94
91
  if (!this.object) return null;
95
- if (this._currentCount >= this.limitCount) return null;
96
- if (this.object as any === this.gameObject) return null;
92
+ if (selected === this.gameObject || this.handleMultiObject(selected)) {
97
93
 
98
- this.object.visible = true;
94
+ if (this.object as any === this.gameObject) return null;
95
+ this.object.visible = true;
99
96
 
100
- if (this._startPosition)
101
- this.object.position.copy(this._startPosition);
102
- if (this._startQuaternion)
103
- this.object.quaternion.copy(this._startQuaternion);
97
+ if (this._startPosition)
98
+ this.object.position.copy(this._startPosition);
99
+ if (this._startQuaternion)
100
+ this.object.quaternion.copy(this._startQuaternion);
104
101
 
105
- const opts = new InstantiateOptions();
106
- if (!this.parent) this.parent = this.gameObject.parent as GameObject;
107
- if (this.parent) {
108
- opts.parent = this.parent.guid ?? this.parent.userData?.guid;
109
- opts.keepWorldPosition = true;
102
+ const opts = new InstantiateOptions();
103
+ if (!this.parent) this.parent = this.gameObject.parent as GameObject;
104
+ if (this.parent) {
105
+ opts.parent = this.parent.guid ?? this.parent.userData?.guid;
106
+ opts.keepWorldPosition = true;
107
+ }
108
+ opts.position = this.worldPosition;
109
+ opts.rotation = this.worldQuaternion;
110
+ opts.context = this.context;
111
+ this._currentCount += 1;
112
+
113
+ const newInstance = GameObject.instantiateSynced(this.object as GameObject, opts) as GameObject;
114
+ console.assert(newInstance !== this.object, "Duplicated object is original");
115
+ this.object.visible = false;
116
+
117
+ // see if this fixes object being offset when duplicated and dragged - it looks like three clone has shared position/quaternion objects?
118
+ if (this._startPosition)
119
+ this.object.position.clone().copy(this._startPosition);
120
+ if (this._startQuaternion)
121
+ this.object.quaternion.clone().copy(this._startQuaternion);
122
+
123
+ return newInstance;
110
124
  }
111
- opts.position = this.worldPosition;
112
- opts.rotation = this.worldQuaternion;
113
- opts.context = this.context;
114
- this._currentCount += 1;
125
+ return null;
126
+ }
115
127
 
116
- const newInstance = GameObject.instantiateSynced(this.object as GameObject, opts) as GameObject;
117
- console.assert(newInstance !== this.object, "Duplicated object is original");
118
- this.object.visible = false;
128
+ private handleMultiObject(selected: THREE.Object3D): boolean {
129
+ const shouldSearchInChildren = this.gameObject.type === "Group" || this.gameObject.type === "Object3D";
130
+ if (!shouldSearchInChildren) return false;
131
+ return this.isInChildren(this.gameObject, selected);
132
+ }
119
133
 
120
- // see if this fixes object being offset when duplicated and dragged - it looks like three clone has shared position/quaternion objects?
121
- if (this._startPosition)
122
- this.object.position.clone().copy(this._startPosition);
123
- if (this._startQuaternion)
124
- this.object.quaternion.clone().copy(this._startQuaternion);
134
+ private isInChildren(current: THREE.Object3D, search: THREE.Object3D): boolean {
135
+ if (!current) return false;
136
+ if (current === search) return true;
137
+ if (current.children) {
138
+ for (const child of current.children) {
139
+ if (this.isInChildren(child, search)) {
140
+ return true;
141
+ }
142
+ }
143
+ }
144
+ return false;
145
+ }
125
146
 
126
- return newInstance;
127
- }
128
147
  }
src/engine/engine_context.ts CHANGED
@@ -26,7 +26,7 @@
26
26
  import { LightDataRegistry, type ILightDataRegistry } from './engine_lightdata.js';
27
27
  import { PlayerViewManager } from './engine_playerview.js';
28
28
 
29
- import { INeedleXRSession, type CoroutineData, type GLTF, type ICamera, type IComponent, type IContext, type ILight, type LoadedGLTF } from "./engine_types.js";
29
+ import { type CoroutineData, type GLTF, type ICamera, type IComponent, type IContext, type ILight, type LoadedGLTF } from "./engine_types.js";
30
30
  import { destroy, foreachComponent } from './engine_gameobject.js';
31
31
  import { ContextEvent, ContextRegistry } from './engine_context_registry.js';
32
32
  import { delay, getParam } from './engine_utils.js';
@@ -36,7 +36,6 @@
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';
40
39
 
41
40
 
42
41
  const debug = utils.getParam("debugcontext");
@@ -102,6 +101,11 @@
102
101
  Undefined = -1,
103
102
  }
104
103
 
104
+ export enum XRSessionMode {
105
+ ImmersiveVR = "immersive-vr",
106
+ ImmersiveAR = "immersive-ar",
107
+ }
108
+
105
109
  /** threejs callback event signature */
106
110
  export declare type OnRenderCallback = (renderer: WebGLRenderer, scene: Scene, camera: Camera, geometry: BufferGeometry, material: Material, group: Group) => void
107
111
 
@@ -209,7 +213,6 @@
209
213
  private _boundingClientRectFrame: number = -1;
210
214
  private _boundingClientRect: DOMRect | null = null;
211
215
  private _domX; private _domY;
212
- /** update bounding rects + domX, domY */
213
216
  private calculateBoundingClientRect() {
214
217
  // workaround for mozilla webXR viewer
215
218
  if (this.isInAR) {
@@ -224,44 +227,30 @@
224
227
  this._domY = this._boundingClientRect.y;
225
228
  }
226
229
 
227
- /** The width of the `<needle-engine>` element on the website */
228
230
  get domWidth(): number {
229
231
  // for mozilla XR
230
232
  if (this.isInAR) return window.innerWidth;
231
233
  return this.domElement.clientWidth;
232
234
  }
233
- /** The height of the `<needle-engine>` element on the website */
234
235
  get domHeight(): number {
235
236
  // for mozilla XR
236
237
  if (this.isInAR) return window.innerHeight;
237
238
  return this.domElement.clientHeight;
238
239
  }
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 */
245
244
  get domY(): number {
246
245
  this.calculateBoundingClientRect();
247
246
  return this._domY;
248
247
  }
249
248
  get isInXR() { return this.renderer?.xr?.isPresenting || false; }
250
- /** shorthand for `NeedleXRSession.active`
251
- * Automatically set by NeedleXRSession when a XR session is active */
252
- xr: INeedleXRSession | null = null;
253
- get xrSessionMode() { return this.xr?.mode; }
254
- get isInVR() { return this.xrSessionMode === "immersive-vr"; }
255
- get isInAR() { return this.xrSessionMode === "immersive-ar"; }
256
- /** If a XR session is active and in pass through mode (immersive-ar on e.g. Quest) */
257
- get isInPassThrough() { return this.xr ? this.xr.isPassThrough : false; }
258
- /** access the raw `XRSession` object (shorthand for `context.renderer.xr.getSession()`). For more control use `NeedleXRSession.active` */
249
+ xrSessionMode: XRSessionMode | undefined = undefined;
250
+ get isInVR() { return this.xrSessionMode === XRSessionMode.ImmersiveVR; }
251
+ get isInAR() { return this.xrSessionMode === XRSessionMode.ImmersiveAR; }
259
252
  get xrSession() { return this.renderer?.xr?.getSession(); }
260
- /** @returns the latest XRFrame (if a XRSession is currently active)
261
- * @link https://developer.mozilla.org/en-US/docs/Web/API/XRFrame
262
- */
263
253
  get xrFrame() { return this._xrFrame }
264
- /** @returns the current WebXR camera (shorthand for `context.renderer.xr.getCamera()`) */
265
254
  get xrCamera(): WebXRArrayCamera | undefined { return this.renderer?.xr?.getCamera(); }
266
255
  private _xrFrame: XRFrame | null = null;
267
256
  get arOverlayElement(): HTMLElement {
@@ -281,37 +270,17 @@
281
270
  composer: EffectComposer | null = null;
282
271
 
283
272
  // all scripts
284
- readonly scripts: IComponent[] = [];
285
- readonly scripts_pausedChanged: IComponent[] = [];
273
+ scripts: IComponent[] = [];
274
+ scripts_pausedChanged: IComponent[] = [];
286
275
  // scripts with update event
287
- readonly scripts_earlyUpdate: IComponent[] = [];
288
- readonly scripts_update: IComponent[] = [];
289
- readonly scripts_lateUpdate: IComponent[] = [];
290
- readonly scripts_onBeforeRender: IComponent[] = [];
291
- readonly scripts_onAfterRender: IComponent[] = [];
292
- readonly scripts_WithCorroutines: IComponent[] = [];
293
- readonly scripts_immersive_vr: INeedleXRSessionEventReceiver[] = [];
294
- readonly scripts_immersive_ar: INeedleXRSessionEventReceiver[] = [];
295
- readonly coroutines: { [FrameEvent: number]: Array<CoroutineData> } = {}
276
+ scripts_earlyUpdate: IComponent[] = [];
277
+ scripts_update: IComponent[] = [];
278
+ scripts_lateUpdate: IComponent[] = [];
279
+ scripts_onBeforeRender: IComponent[] = [];
280
+ scripts_onAfterRender: IComponent[] = [];
281
+ scripts_WithCorroutines: IComponent[] = [];
282
+ coroutines: { [FrameEvent: number]: Array<CoroutineData> } = {}
296
283
 
297
- /** callbacks called once after the context has been created */
298
- readonly post_setup_callbacks: Function[] = [];
299
- /** called every frame at the beginning of the frame (after component start events and before earlyUpdate) */
300
- readonly pre_update_callbacks: Function[] = [];
301
- /** called every frame before rendering (after all component events) */
302
- readonly pre_render_callbacks: Array<(frame: XRFrame | null) => void> = [];
303
- /** called every frame after rendering (after all component events) */
304
- readonly post_render_callbacks: Function[] = [];
305
-
306
- /** called every frame befroe update (this list is emptied every frame) */
307
- readonly pre_update_oneshot_callbacks: Function[] = [];
308
-
309
- readonly new_scripts: IComponent[] = [];
310
- readonly new_script_start: IComponent[] = [];
311
- readonly new_scripts_pre_setup_callbacks: Function[] = [];
312
- readonly new_scripts_post_setup_callbacks: Function[] = [];
313
- readonly new_scripts_xr: INeedleXRSessionEventReceiver[] = [];
314
-
315
284
  mainCameraComponent: ICamera | undefined;
316
285
 
317
286
  private _camera: Camera | null = null;
@@ -331,13 +300,20 @@
331
300
  this._camera = cam;
332
301
  }
333
302
 
303
+ post_setup_callbacks: Function[] = [];
304
+ pre_update_callbacks: Function[] = [];
305
+ pre_render_callbacks: Function[] = [];
306
+ post_render_callbacks: Function[] = [];
307
+
308
+ new_scripts: IComponent[] = [];
309
+ new_script_start: IComponent[] = [];
310
+ new_scripts_pre_setup_callbacks: Function[] = [];
311
+ new_scripts_post_setup_callbacks: Function[] = [];
312
+
334
313
  application: Application;
335
- /** access timings (current frame number, deltaTime, timeScale, ...) */
336
314
  time: Time;
337
315
  input: Input;
338
- /** access physics related methods (e.g. raycasting). To access the phyiscs engine use `context.physics.engine` */
339
316
  physics: Physics;
340
- /** access networking methods (use it to send or listen to messages or join a networking backend) */
341
317
  connection: NetworkConnection;
342
318
  /**
343
319
  * @deprecated AssetDataBase is deprecated
@@ -417,7 +393,7 @@
417
393
  }
418
394
  }
419
395
  }
420
- if (debug) console.log("Using Renderer Parameters:", params, this.domElement)
396
+ if(debug) console.log("Using Renderer Parameters:", params, this.domElement)
421
397
 
422
398
  this.renderer = new WebGLRenderer(params);
423
399
 
@@ -436,8 +412,6 @@
436
412
  this.renderer.outputColorSpace = SRGBColorSpace;
437
413
  // https://github.com/mrdoob/three.js/pull/25556
438
414
  this.renderer.useLegacyLights = false;
439
-
440
- this.input.bindEvents();
441
415
  }
442
416
 
443
417
 
@@ -449,13 +423,10 @@
449
423
 
450
424
  private _disposeCallbacks: Function[] = [];
451
425
 
426
+ // private _requestSizeUpdate : boolean = false;
452
427
 
453
- /** will request a renderer size update the next render call (will call updateSize the next update) */
454
- requestSizeUpdate() { this._sizeChanged = true; }
455
-
456
- /** update the renderer and canvas size */
457
- updateSize(force: boolean = false) {
458
- if (force || (!this.isManagedExternally && this.renderer.xr?.isPresenting === false)) {
428
+ updateSize() {
429
+ if (!this.isManagedExternally && this.renderer.xr?.isPresenting === false) {
459
430
  this._sizeChanged = false;
460
431
  const scaleFactor = this.resolutionScaleFactor;
461
432
  const width = this.domWidth * scaleFactor;
@@ -506,7 +477,7 @@
506
477
  async create(opts?: ContextCreateArgs) {
507
478
  try {
508
479
  this._isCreating = true;
509
- if (opts !== this._originalCreationArgs)
480
+ if(opts !== this._originalCreationArgs)
510
481
  this._originalCreationArgs = utils.deepClone(opts);
511
482
  window.addEventListener("unhandledrejection", this.onUnhandledRejection)
512
483
  const res = await this.internalOnCreate(opts);
@@ -559,11 +530,11 @@
559
530
  if (this.renderer) {
560
531
  this.renderer.setClearAlpha(0);
561
532
  this.renderer.clear();
562
- if (!this.isManagedExternally) {
563
- if (debug) console.log("Disposing renderer");
564
- this.renderer.dispose();
565
- }
566
533
  }
534
+ if (!this.isManagedExternally) {
535
+ if(debug) console.log("Disposing renderer");
536
+ this.renderer.dispose();
537
+ }
567
538
  this.scene = null!;
568
539
  this.renderer = null!;
569
540
  this.input.dispose();
@@ -581,10 +552,6 @@
581
552
  this._isCreated = false;
582
553
  ContextRegistry.dispatchCallback(ContextEvent.ContextDestroyed, this);
583
554
  ContextRegistry.unregister(this);
584
- if (Context.Current === this) {
585
- //@ts-ignore
586
- Context.Current = null;
587
- }
588
555
  }
589
556
 
590
557
  registerCoroutineUpdate(script: IComponent, coroutine: Generator, evt: FrameEvent): Generator {
@@ -736,7 +703,7 @@
736
703
  private async internalOnCreate(opts?: ContextCreateArgs) {
737
704
  const createId = ++this._createId;
738
705
 
739
- if (debug) console.log("Creating context", this.name, opts);
706
+ if(debug) console.log("Creating context", this.name, opts);
740
707
 
741
708
  this.clear();
742
709
  // stop the animation loop if its running during creation
@@ -843,8 +810,6 @@
843
810
  }
844
811
  }
845
812
 
846
- this.input.bindEvents();
847
-
848
813
  Context.Current = this;
849
814
  looputils.processNewScripts(this);
850
815
 
@@ -887,7 +852,7 @@
887
852
  this._dispatchReadyAfterFrame = true;
888
853
  const res = ContextRegistry.dispatchCallback(ContextEvent.ContextCreated, this, { files: loadedFiles });
889
854
  if (res) {
890
- if ("internalSetLoadingMessage" in this.domElement && typeof this.domElement.internalSetLoadingMessage === "function")
855
+ if("internalSetLoadingMessage" in this.domElement && typeof this.domElement.internalSetLoadingMessage === "function")
891
856
  this.domElement?.internalSetLoadingMessage("finish loading");
892
857
  await res;
893
858
  }
@@ -931,7 +896,7 @@
931
896
  }
932
897
 
933
898
  args?.onLoadingStart?.call(this, i, file);
934
- if (debug) console.log("Context Load " + file);
899
+ if(debug) console.log("Context Load " + file);
935
900
  const res = await loader.loadSync(this, file, file, loadingHash, prog => {
936
901
  progressArg.name = file;
937
902
  progressArg.progress = prog;
@@ -1007,7 +972,7 @@
1007
972
  catch (err) {
1008
973
  this._renderlooperrors += 1;
1009
974
  if ((isDevEnvironment() || debug) && (err instanceof Error || err instanceof TypeError))
1010
- showBalloonMessage(`Caught unhandled exception during render-loop - see console for details.`, LogType.Error);
975
+ showBalloonMessage("Caught unhandled exception during render-loop.<br/>Stopping renderloop...<br/>See console for details.", LogType.Error);
1011
976
  console.error(err);
1012
977
  if (this._renderlooperrors > 10) {
1013
978
  console.warn("Stopping render loop due to error")
@@ -1042,11 +1007,7 @@
1042
1007
 
1043
1008
  private internalOnBeforeRender(timestamp: DOMHighResTimeStamp, frame: XRFrame | null) {
1044
1009
 
1045
- const sessionStarted = frame !== null && this._xrFrame === null;
1046
1010
  this._xrFrame = frame;
1047
- if (sessionStarted) {
1048
- this.domElement.dispatchEvent(new CustomEvent("xr-session-started", { detail: { context: this, session: this.xrSession, frame: frame } }));
1049
- }
1050
1011
 
1051
1012
  this._currentFrameEvent = FrameEvent.Undefined;
1052
1013
 
@@ -1085,13 +1046,6 @@
1085
1046
  this.setCurrentCamera(last);
1086
1047
  }
1087
1048
 
1088
- if (this.pre_update_oneshot_callbacks) {
1089
- for (const i in this.pre_update_oneshot_callbacks) {
1090
- this.pre_update_oneshot_callbacks[i]();
1091
- }
1092
- this.pre_update_oneshot_callbacks.length = 0;
1093
- }
1094
-
1095
1049
  if (this.pre_update_callbacks) {
1096
1050
  for (const i in this.pre_update_callbacks) {
1097
1051
  this.pre_update_callbacks[i]();
@@ -1174,7 +1128,7 @@
1174
1128
 
1175
1129
  if (this.pre_render_callbacks) {
1176
1130
  for (const i in this.pre_render_callbacks) {
1177
- this.pre_render_callbacks[i](frame);
1131
+ this.pre_render_callbacks[i]();
1178
1132
  }
1179
1133
  }
1180
1134
 
@@ -1252,8 +1206,8 @@
1252
1206
  }
1253
1207
  this._isRendering = true;
1254
1208
  this.renderRequiredTextures();
1209
+
1255
1210
 
1256
-
1257
1211
  if (this.composer && !this.isInXR) {
1258
1212
  this.composer.render(this.time.deltaTime);
1259
1213
  }
src/engine/engine_create_objects.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import { PlaneGeometry, MeshBasicMaterial, DoubleSide, Mesh, Material, MeshStandardMaterial, BoxGeometry