@@ -6,7 +6,7 @@
|
|
6
6
|
import { RaycastOptions } from "../engine/engine_physics.js";
|
7
7
|
import { serializable } from "../engine/engine_serialization_decorator.js";
|
8
8
|
import { Context } from "../engine/engine_setup.js";
|
9
|
-
import { getBoundingBox, getWorldPosition, setWorldPosition } from "../engine/engine_three_utils.js";
|
9
|
+
import { getBoundingBox, getTempVector, getWorldPosition, setWorldPosition } from "../engine/engine_three_utils.js";
|
10
10
|
import { type IGameObject } from "../engine/engine_types.js";
|
11
11
|
import { getParam } from "../engine/engine_utils.js";
|
12
12
|
import { NeedleXRSession } from "../engine/engine_xr.js";
|
@@ -341,6 +341,21 @@
|
|
341
341
|
}
|
342
342
|
}
|
343
343
|
|
344
|
+
/** Common interface for pointer handlers (single touch and multi touch) */
|
345
|
+
interface IDragHandler {
|
346
|
+
/** Used to determine if a drag has happened for this handler */
|
347
|
+
getTotalMovement?(): Vector3;
|
348
|
+
/** Target object can change mid-flight (e.g. in Duplicatable), handlers should react properly to that */
|
349
|
+
setTargetObject(obj: Object3D | null): void;
|
350
|
+
|
351
|
+
/** Prewarms the drag – can already move internal points around here but should not move the object itself */
|
352
|
+
collectMovementInfo?(): void;
|
353
|
+
onDragStart?(args: PointerEventData): void;
|
354
|
+
onDragEnd?(args: PointerEventData): void;
|
355
|
+
/** The target object is moved around */
|
356
|
+
onDragUpdate?(numberOfPointers: number): void;
|
357
|
+
}
|
358
|
+
|
344
359
|
/** Handles two touch points affecting one object. Allows movement, scale and rotation of objects. */
|
345
360
|
class MultiTouchDragHandler implements IDragHandler {
|
346
361
|
|
@@ -581,20 +596,6 @@
|
|
581
596
|
}
|
582
597
|
}
|
583
598
|
|
584
|
-
/** Common interface for pointer handlers (single touch and multi touch) */
|
585
|
-
interface IDragHandler {
|
586
|
-
/** Used to determine if a drag has happened for this handler */
|
587
|
-
getTotalMovement?(): Vector3;
|
588
|
-
/** Target object can change mid-flight (e.g. in Duplicatable), handlers should react properly to that */
|
589
|
-
setTargetObject(obj: Object3D | null): void;
|
590
|
-
|
591
|
-
/** Prewarms the drag – can already move internal points around here but should not move the object itself */
|
592
|
-
collectMovementInfo?(): void;
|
593
|
-
onDragStart?(args: PointerEventData): void;
|
594
|
-
onDragEnd?(args: PointerEventData): void;
|
595
|
-
/** The target object is moved around */
|
596
|
-
onDragUpdate?(numberOfPointers: number): void;
|
597
|
-
}
|
598
599
|
|
599
600
|
/** Handles a single pointer on an object. DragPointerHandlers are created on pointer enter,
|
600
601
|
* help with determining if there's actually a drag, and then perform operations based on spatial pointer data.
|
@@ -908,7 +909,7 @@
|
|
908
909
|
|
909
910
|
// Acceleration for moving the object - move followObject along the ray distance by _totalMovementAlongRayDirection
|
910
911
|
let currentDist = 1.0;
|
911
|
-
let lerpFactor =
|
912
|
+
let lerpFactor = 2.0;
|
912
913
|
if (isSpatialInput && this._grabStartDistance > 0.5) // hands and controllers, but not touches
|
913
914
|
{
|
914
915
|
const factor = 1 + this._totalMovementAlongRayDirection * (2 * this.settings.xrDistanceDragFactor);
|
@@ -960,18 +961,23 @@
|
|
960
961
|
}
|
961
962
|
|
962
963
|
if (hit.face) {
|
964
|
+
const dragTimeThreshold = 0.15;
|
965
|
+
const dragTimeSatisfied = this._draggedOverObjectDuration >= dragTimeThreshold;
|
963
966
|
// Adjust drag plane if we're dragging over a different object (for a certain amount of time)
|
964
967
|
// or if the surface normal changed
|
965
|
-
if (
|
966
|
-
(this._draggedOverObjectLastSetUp !== this._draggedOverObject
|
967
|
-
|
968
|
+
if (dragTimeSatisfied &&
|
969
|
+
(this._draggedOverObjectLastSetUp !== this._draggedOverObject
|
970
|
+
|| this._draggedOverObjectLastNormal.dot(hit.face.normal) < 0.999999
|
971
|
+
// if we're dragging on a flat surface with different levels (like the sandbox floor)
|
972
|
+
|| this.context.time.frame % 60 === 0
|
973
|
+
)
|
968
974
|
) {
|
969
975
|
this._draggedOverObjectLastSetUp = this._draggedOverObject;
|
970
976
|
this._draggedOverObjectLastNormal.copy(hit.face.normal);
|
971
977
|
|
972
|
-
const center =
|
973
|
-
const size =
|
974
|
-
|
978
|
+
const center = getTempVector();
|
979
|
+
const size = getTempVector();
|
980
|
+
|
975
981
|
this._bounds.getCenter(center);
|
976
982
|
this._bounds.getSize(size);
|
977
983
|
center.sub(size.multiplyScalar(0.5).multiply(hit.face.normal));
|
@@ -986,7 +992,7 @@
|
|
986
992
|
this._bounds.getSize(size);
|
987
993
|
center.add(size.multiplyScalar(0.5).multiply(hit.face.normal));
|
988
994
|
|
989
|
-
const offset = this._hitPointInLocalSpace
|
995
|
+
const offset = getTempVector(this._hitPointInLocalSpace).add(center);
|
990
996
|
this._followObject.localToWorld(offset);
|
991
997
|
|
992
998
|
// See https://linear.app/needle/issue/NE-5004
|
@@ -994,6 +1000,13 @@
|
|
994
1000
|
const point = hit.point;//.sub(offsetWP);
|
995
1001
|
this._dragPlane.setFromNormalAndCoplanarPoint(hit.face.normal, point);
|
996
1002
|
}
|
1003
|
+
// If the drag has just started and we're not yet really starting to update the object's position
|
1004
|
+
// we want to return here and wait until the drag has been going on for a bit
|
1005
|
+
// Otherwise the object will either immediately change it's position (when the user starts dragging)
|
1006
|
+
// Or interpolate to a wrong position for a short moment
|
1007
|
+
else if (!dragTimeSatisfied) {
|
1008
|
+
return;
|
1009
|
+
}
|
997
1010
|
}
|
998
1011
|
}
|
999
1012
|
}
|
@@ -1345,7 +1358,7 @@
|
|
1345
1358
|
private onUpdateGroundPlane() {
|
1346
1359
|
if (!this._selected || !this._context) return;
|
1347
1360
|
const wp = getWorldPosition(this._selected);
|
1348
|
-
const ray = new Ray(
|
1361
|
+
const ray = new Ray(getTempVector(0, .1, 0).add(wp), getTempVector(0, -1, 0));
|
1349
1362
|
const opts = new RaycastOptions();
|
1350
1363
|
opts.testObject = o => o !== this._selected;
|
1351
1364
|
const hits = this._context.physics.raycastFromRay(ray, opts);
|
@@ -1354,7 +1367,7 @@
|
|
1354
1367
|
if (!hit.face || this.contains(this._selected, hit.object)) {
|
1355
1368
|
continue;
|
1356
1369
|
}
|
1357
|
-
const normal =
|
1370
|
+
const normal = getTempVector(0, 1, 0); // hit.face.normal
|
1358
1371
|
this._groundPlane.setFromNormalAndCoplanarPoint(normal, hit.point);
|
1359
1372
|
break;
|
1360
1373
|
}
|
@@ -44,6 +44,10 @@
|
|
44
44
|
/** if the instantiated object should be visible */
|
45
45
|
visible?: boolean;
|
46
46
|
context?: Context;
|
47
|
+
/** If true the components will be cloned as well
|
48
|
+
* @default true
|
49
|
+
*/
|
50
|
+
components?: boolean;
|
47
51
|
}
|
48
52
|
|
49
53
|
export class InstantiateOptions implements IInstantiateOptions {
|
@@ -55,8 +59,9 @@
|
|
55
59
|
scale?: Vector3 | undefined;
|
56
60
|
visible?: boolean | undefined;
|
57
61
|
context?: Context | undefined;
|
62
|
+
components?: boolean | undefined;
|
58
63
|
|
59
|
-
clone(){
|
64
|
+
clone() {
|
60
65
|
const clone = new InstantiateOptions();
|
61
66
|
clone.idProvider = this.idProvider;
|
62
67
|
clone.parent = this.parent;
|
@@ -64,17 +69,23 @@
|
|
64
69
|
clone.position = this.position?.clone();
|
65
70
|
clone.rotation = this.rotation?.clone();
|
66
71
|
clone.scale = this.scale?.clone();
|
72
|
+
clone.visible = this.visible;
|
73
|
+
clone.context = this.context;
|
74
|
+
clone.components = this.components;
|
67
75
|
return clone;
|
68
76
|
}
|
69
77
|
|
70
78
|
/** Copy fields from another object, clone field references */
|
71
|
-
cloneAssign(other: InstantiateOptions | IInstantiateOptions){
|
79
|
+
cloneAssign(other: InstantiateOptions | IInstantiateOptions) {
|
72
80
|
this.idProvider = other.idProvider;
|
73
81
|
this.parent = other.parent;
|
74
82
|
this.keepWorldPosition = other.keepWorldPosition;
|
75
83
|
this.position = other.position?.clone();
|
76
84
|
this.rotation = other.rotation?.clone();
|
77
85
|
this.scale = other.scale?.clone();
|
86
|
+
this.visible = other.visible;
|
87
|
+
this.context = other.context;
|
88
|
+
this.components = other.components;
|
78
89
|
}
|
79
90
|
}
|
80
91
|
|
@@ -326,29 +337,31 @@
|
|
326
337
|
}
|
327
338
|
|
328
339
|
const guidsMap: GuidsMap = {};
|
329
|
-
|
330
|
-
const
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
340
|
+
if (options?.components !== false) {
|
341
|
+
for (const i in components) {
|
342
|
+
const copy = components[i];
|
343
|
+
const oldGuid = copy.guid;
|
344
|
+
if (options && options.idProvider) {
|
345
|
+
copy.guid = options.idProvider.generateUUID();
|
346
|
+
guidsMap[oldGuid] = copy.guid;
|
347
|
+
if (debug)
|
348
|
+
console.log(copy.name, copy.guid)
|
349
|
+
}
|
350
|
+
registerComponent(copy, context);
|
351
|
+
if (copy.__internalNewInstanceCreated)
|
352
|
+
copy.__internalNewInstanceCreated();
|
337
353
|
}
|
338
|
-
|
339
|
-
|
340
|
-
copy.
|
354
|
+
for (const i in components) {
|
355
|
+
const copy = components[i];
|
356
|
+
if (copy.resolveGuids)
|
357
|
+
copy.resolveGuids(guidsMap);
|
358
|
+
if (copy.enabled === false) continue;
|
359
|
+
else copy.enabled = true;
|
360
|
+
}
|
361
|
+
|
362
|
+
processNewScripts(context);
|
341
363
|
}
|
342
|
-
for (const i in components) {
|
343
|
-
const copy = components[i];
|
344
|
-
if (copy.resolveGuids)
|
345
|
-
copy.resolveGuids(guidsMap);
|
346
|
-
if (copy.enabled === false) continue;
|
347
|
-
else copy.enabled = true;
|
348
|
-
}
|
349
364
|
|
350
|
-
processNewScripts(context);
|
351
|
-
|
352
365
|
return clone as GameObject;
|
353
366
|
}
|
354
367
|
|
@@ -1,5 +1,5 @@
|
|
1
1
|
|
2
|
-
import { setDracoDecoderLocation, setKTX2TranscoderLocation } from '@needle-tools/gltf-progressive';
|
2
|
+
import { createLoaders, setDracoDecoderLocation, setKTX2TranscoderLocation } from '@needle-tools/gltf-progressive';
|
3
3
|
import { MeshoptDecoder } from 'three/examples/jsm/libs/meshopt_decoder.module.js';
|
4
4
|
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js';
|
5
5
|
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
|
@@ -22,35 +22,40 @@
|
|
22
22
|
});
|
23
23
|
|
24
24
|
|
25
|
-
let dracoLoader: DRACOLoader;
|
26
25
|
let meshoptDecoder: typeof MeshoptDecoder;
|
27
|
-
let ktx2Loader: KTX2Loader;
|
28
26
|
|
27
|
+
let loaders: null | { dracoLoader: DRACOLoader, ktx2Loader: KTX2Loader, meshoptDecoder: typeof MeshoptDecoder } = null;
|
28
|
+
|
29
|
+
function ensureLoaders() {
|
30
|
+
if (!loaders) {
|
31
|
+
const res = createLoaders(null);
|
32
|
+
loaders = { dracoLoader: res.dracoLoader, ktx2Loader: res.ktx2Loader, meshoptDecoder: res.meshoptDecoder };
|
33
|
+
}
|
34
|
+
return loaders;
|
35
|
+
}
|
36
|
+
|
29
37
|
export function setDracoDecoderPath(path: string | undefined) {
|
30
38
|
if (path !== undefined && typeof path === "string") {
|
31
|
-
|
32
|
-
dracoLoader = new DRACOLoader();
|
39
|
+
const loaders = ensureLoaders();
|
33
40
|
if (debug) console.log("Setting draco decoder path to", path);
|
34
|
-
dracoLoader.setDecoderPath(path);
|
41
|
+
loaders.dracoLoader.setDecoderPath(path);
|
35
42
|
setDracoDecoderLocation(path);
|
36
43
|
}
|
37
44
|
}
|
38
45
|
|
39
46
|
export function setDracoDecoderType(type: string | undefined) {
|
40
47
|
if (type !== undefined && typeof type === "string") {
|
41
|
-
|
42
|
-
dracoLoader = new DRACOLoader();
|
48
|
+
const loaders = ensureLoaders();
|
43
49
|
if (debug) console.log("Setting draco decoder type to", type);
|
44
|
-
dracoLoader.setDecoderConfig({ type: type });
|
50
|
+
loaders.dracoLoader.setDecoderConfig({ type: type });
|
45
51
|
}
|
46
52
|
}
|
47
53
|
|
48
54
|
export function setKtx2TranscoderPath(path: string) {
|
49
55
|
if (path !== undefined && typeof path === "string") {
|
50
|
-
|
51
|
-
ktx2Loader = new KTX2Loader();
|
56
|
+
const loaders = ensureLoaders();
|
52
57
|
if (debug) console.log("Setting ktx2 transcoder path to", path);
|
53
|
-
ktx2Loader.setTranscoderPath(path);
|
58
|
+
loaders.ktx2Loader.setTranscoderPath(path);
|
54
59
|
setKTX2TranscoderLocation(path);
|
55
60
|
}
|
56
61
|
}
|
@@ -67,32 +72,25 @@
|
|
67
72
|
* @returns The GLTFLoader instance with the loaders added.
|
68
73
|
*/
|
69
74
|
export function addDracoAndKTX2Loaders(loader: GLTFLoader, context: Pick<Context, "renderer">) {
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
dracoLoader.setDecoderConfig({ type: 'js' });
|
74
|
-
if (debug) console.log("Setting draco decoder path to", DEFAULT_DRACO_DECODER_LOCATION);
|
75
|
-
}
|
76
|
-
if (!ktx2Loader) {
|
77
|
-
ktx2Loader = new KTX2Loader();
|
78
|
-
ktx2Loader.setTranscoderPath(DEFAULT_KTX2_TRANSCODER_LOCATION);
|
79
|
-
if (debug) console.log("Setting ktx2 transcoder path to", DEFAULT_KTX2_TRANSCODER_LOCATION);
|
80
|
-
}
|
75
|
+
|
76
|
+
const loaders = ensureLoaders();
|
77
|
+
|
81
78
|
if (!meshoptDecoder) {
|
82
|
-
meshoptDecoder =
|
79
|
+
meshoptDecoder = loaders.meshoptDecoder;
|
83
80
|
if (debug) console.log("Using the default meshopt decoder");
|
84
81
|
}
|
85
82
|
|
83
|
+
|
86
84
|
if (context.renderer) {
|
87
|
-
ktx2Loader.detectSupport(context.renderer);
|
85
|
+
loaders.ktx2Loader.detectSupport(context.renderer);
|
88
86
|
}
|
89
87
|
else
|
90
88
|
console.warn("No renderer provided to detect ktx2 support - loading KTX2 textures will probably fail");
|
91
89
|
|
92
90
|
if (!loader.dracoLoader)
|
93
|
-
loader.setDRACOLoader(dracoLoader);
|
91
|
+
loader.setDRACOLoader(loaders.dracoLoader);
|
94
92
|
if (!(loader as any).ktx2Loader)
|
95
|
-
loader.setKTX2Loader(ktx2Loader);
|
93
|
+
loader.setKTX2Loader(loaders.ktx2Loader);
|
96
94
|
if (!(loader as any).meshoptDecoder)
|
97
95
|
loader.setMeshoptDecoder(meshoptDecoder);
|
98
96
|
return loader;
|
@@ -76,10 +76,10 @@
|
|
76
76
|
// e.g. prefix = "/materials/";
|
77
77
|
const res = tryResolveDependency(paths, parser, val);
|
78
78
|
if (res) {
|
79
|
-
res.then(res => {
|
79
|
+
promises.push(res.then(res => {
|
80
80
|
obj[key] = res;
|
81
81
|
return res;
|
82
|
-
});
|
82
|
+
}));
|
83
83
|
continue;
|
84
84
|
}
|
85
85
|
}
|
@@ -271,6 +271,7 @@
|
|
271
271
|
|
272
272
|
/** @internal */
|
273
273
|
onEnable() {
|
274
|
+
this._didSetTarget = 0;
|
274
275
|
this._enableTime = this.context.time.time;
|
275
276
|
const cameraComponent = GameObject.getComponent(this.gameObject, Camera);
|
276
277
|
this._camera = cameraComponent;
|
@@ -330,6 +331,7 @@
|
|
330
331
|
// if (this.autoFit) this.fitCamera()
|
331
332
|
// }
|
332
333
|
this.context.input.addEventListener("pointerup", this._onPointerDown);
|
334
|
+
this.context.pre_render_callbacks.push(this.__onPreRender);
|
333
335
|
}
|
334
336
|
|
335
337
|
/** @internal */
|
@@ -406,8 +408,6 @@
|
|
406
408
|
}
|
407
409
|
this._controls.enabled = true;
|
408
410
|
|
409
|
-
this.__handleSetTargetWhenBecomingActiveTheFirstTime();
|
410
|
-
|
411
411
|
if (this.context.input.getPointerDown(1) || this.context.input.getPointerDown(2) || this.context.input.mouseWheelChanged || (this.context.input.getPointerPressed(0) && this.context.input.getPointerPositionDelta(0)?.length() || 0 > .1)) {
|
412
412
|
this._inputs += 1;
|
413
413
|
}
|
@@ -420,7 +420,36 @@
|
|
420
420
|
this._lookTargetLerpActive = false;
|
421
421
|
}
|
422
422
|
this._inputs = 0;
|
423
|
+
|
423
424
|
|
425
|
+
if (this.autoTarget) {
|
426
|
+
// we want to wait one frame so all matrixWorlds are updated
|
427
|
+
// otherwise raycasting will not work correctly
|
428
|
+
if (this._didSetTarget++ === 0) {
|
429
|
+
const camGo = GameObject.getComponent(this.gameObject, Camera);
|
430
|
+
if (camGo && !this.setLookTargetFromConstraint()) {
|
431
|
+
if (this.debugLog)
|
432
|
+
console.log("NO TARGET");
|
433
|
+
const worldPosition = getWorldPosition(camGo.cam);
|
434
|
+
const distanceToCenter = worldPosition.length();
|
435
|
+
const forward = new Vector3(0, 0, -distanceToCenter).applyMatrix4(camGo.cam.matrixWorld);
|
436
|
+
this.setLookTargetPosition(forward, true);
|
437
|
+
}
|
438
|
+
if (!this.setLookTargetFromConstraint()) {
|
439
|
+
const opts = new RaycastOptions();
|
440
|
+
// center of the screen:
|
441
|
+
opts.screenPoint = new Vector2(0, 0);
|
442
|
+
opts.lineThreshold = 0.1;
|
443
|
+
const hits = this.context.physics.raycast(opts);
|
444
|
+
if (hits.length > 0) {
|
445
|
+
this.setLookTargetPosition(hits[0].point, true);
|
446
|
+
}
|
447
|
+
if (debugCameraFit)
|
448
|
+
console.log("OrbitControls hits", ...hits);
|
449
|
+
}
|
450
|
+
}
|
451
|
+
}
|
452
|
+
|
424
453
|
let focusAtPointer = (this.middleClickToFocus && this.context.input.getPointerClicked(1));
|
425
454
|
focusAtPointer ||= (this.doubleClickToFocus && this.context.input.getPointerDoubleClicked(0) && this.context.time.time - this._enableTime > .3);
|
426
455
|
if (focusAtPointer) {
|
@@ -509,42 +538,20 @@
|
|
509
538
|
|
510
539
|
}
|
511
540
|
|
512
|
-
private
|
541
|
+
private __onPreRender = () => {
|
513
542
|
|
514
|
-
|
543
|
+
// We call this only once when the camera becomes active and use the engine pre_render_callbacks because they are run
|
544
|
+
// after all scripts have been executed
|
545
|
+
const index = this.context.pre_render_callbacks.indexOf(this.__onPreRender);
|
546
|
+
if (index >= 0) {
|
547
|
+
this.context.pre_render_callbacks.splice(index, 1);
|
548
|
+
}
|
515
549
|
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
}
|
522
|
-
|
523
|
-
// we want to wait one frame so all matrixWorlds are updated
|
524
|
-
// otherwise raycasting will not work correctly
|
525
|
-
if (this._didSetTarget++ === 0) {
|
526
|
-
const camGo = GameObject.getComponent(this.gameObject, Camera);
|
527
|
-
if (camGo && !this.setLookTargetFromConstraint()) {
|
528
|
-
if (this.debugLog)
|
529
|
-
console.log("NO TARGET");
|
530
|
-
const worldPosition = getWorldPosition(camGo.cam);
|
531
|
-
const distanceToCenter = worldPosition.length();
|
532
|
-
const forward = new Vector3(0, 0, -distanceToCenter).applyMatrix4(camGo.cam.matrixWorld);
|
533
|
-
this.setLookTargetPosition(forward, true);
|
534
|
-
}
|
535
|
-
if (!this.autoFit && !this.setLookTargetFromConstraint()) {
|
536
|
-
const opts = new RaycastOptions();
|
537
|
-
// center of the screen:
|
538
|
-
opts.screenPoint = new Vector2(0, 0);
|
539
|
-
opts.lineThreshold = 0.1;
|
540
|
-
const hits = this.context.physics.raycast(opts);
|
541
|
-
if (hits.length > 0) {
|
542
|
-
this.setLookTargetPosition(hits[0].point, true);
|
543
|
-
}
|
544
|
-
if (debugCameraFit)
|
545
|
-
console.log("OrbitControls hits", ...hits);
|
546
|
-
}
|
547
|
-
}
|
550
|
+
if (this.autoFit) {
|
551
|
+
this.fitCamera(this.scene.children, {
|
552
|
+
centerCamera: "y",
|
553
|
+
immediate: true,
|
554
|
+
})
|
548
555
|
}
|
549
556
|
}
|
550
557
|
|
@@ -53,6 +53,7 @@
|
|
53
53
|
}
|
54
54
|
|
55
55
|
unapply() {
|
56
|
+
if(debug) console.log("Unapplying postprocessing effects");
|
56
57
|
this._isActive = false;
|
57
58
|
if (this._lastVolumeComponents) {
|
58
59
|
for (const component of this._lastVolumeComponents) {
|
@@ -198,7 +198,6 @@
|
|
198
198
|
}
|
199
199
|
|
200
200
|
private unapply() {
|
201
|
-
if (debug) console.log("Unapply PostProcessing " + this.name);
|
202
201
|
this._postprocessing?.unapply();
|
203
202
|
}
|