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