@@ -88,7 +88,7 @@
|
|
88
88
|
/** This plugin logs all imports. This helps to find cases where incorrect folders are found/resolved. */
|
89
89
|
|
90
90
|
const debuggingPlugin = {
|
91
|
-
name: "needle
|
91
|
+
name: "needle:alias-debug",
|
92
92
|
// needs to run before regular resolver
|
93
93
|
enforce: 'pre',
|
94
94
|
resolveId(id, importer, options) {
|
@@ -17,8 +17,8 @@
|
|
17
17
|
return {
|
18
18
|
name: 'needle:asap',
|
19
19
|
transformIndexHtml: {
|
20
|
-
|
21
|
-
|
20
|
+
order: 'pre',
|
21
|
+
handler(html, _ctx) {
|
22
22
|
if (existsSync(process.cwd() + "/node_modules/@needle-tools/engine/src/asap/needle-asap.ts")) {
|
23
23
|
return {
|
24
24
|
html,
|
@@ -44,7 +44,7 @@
|
|
44
44
|
let taskFinished = false;
|
45
45
|
let taskSucceeded = false;
|
46
46
|
return {
|
47
|
-
name: 'needle
|
47
|
+
name: 'needle:buildpipeline',
|
48
48
|
enforce: "post",
|
49
49
|
apply: 'build',
|
50
50
|
buildEnd() {
|
@@ -13,7 +13,7 @@
|
|
13
13
|
if (userSettings?.noBuildInfo) return;
|
14
14
|
|
15
15
|
return {
|
16
|
-
name: 'needle
|
16
|
+
name: 'needle:buildinfo',
|
17
17
|
apply: "build",
|
18
18
|
enforce: "post",
|
19
19
|
closeBundle: async () => {
|
@@ -20,7 +20,7 @@
|
|
20
20
|
if (needleEngineConfig?.useRapier === false || userSettings?.useRapier === false) useRapier = false;
|
21
21
|
|
22
22
|
return {
|
23
|
-
name: 'needle
|
23
|
+
name: 'needle:defines',
|
24
24
|
enforce: 'pre',
|
25
25
|
config(viteConfig) {
|
26
26
|
// console.log("Update vite defines -------------------------------------------");
|
@@ -11,7 +11,7 @@
|
|
11
11
|
* @type {import('vite').Plugin}
|
12
12
|
*/
|
13
13
|
return {
|
14
|
-
name: 'needle
|
14
|
+
name: 'needle:dependencies',
|
15
15
|
enforce: 'pre',
|
16
16
|
config: (config) => {
|
17
17
|
if (config.optimizeDeps?.include?.includes("three-mesh-bvh")) {
|
@@ -161,8 +161,9 @@
|
|
161
161
|
expectedVersion = value;
|
162
162
|
}
|
163
163
|
if (expectedVersion?.length > 0) {
|
164
|
-
const isRange = expectedVersion.includes("^") || expectedVersion.includes(">") || expectedVersion.includes("<");
|
165
|
-
|
164
|
+
const isRange = expectedVersion.includes("^") || expectedVersion.includes(">") || expectedVersion.includes("<") || expectedVersion.includes("~");
|
165
|
+
const isLatest = expectedVersion === "latest";
|
166
|
+
if (!isRange && !isLatest) {
|
166
167
|
const packageJsonPath = path.join(depPath, "package.json");
|
167
168
|
/** @type {String} */
|
168
169
|
const installedVersion = JSON.parse(readFileSync(packageJsonPath, "utf8")).version;
|
@@ -11,7 +11,7 @@
|
|
11
11
|
if (command === "build") return;
|
12
12
|
|
13
13
|
return {
|
14
|
-
name: "needle
|
14
|
+
name: "needle:drop",
|
15
15
|
config(config) {
|
16
16
|
if(userSettings)
|
17
17
|
if (!config.server) config.server = {};
|
@@ -20,8 +20,8 @@
|
|
20
20
|
setTimeout(() => console.log("Update HMR port to " + config.server.hmr.port));
|
21
21
|
},
|
22
22
|
transformIndexHtml: {
|
23
|
-
|
24
|
-
|
23
|
+
order: 'pre',
|
24
|
+
handler(html, _) {
|
25
25
|
const file = path.join(__dirname, 'drop-client.js');
|
26
26
|
return [
|
27
27
|
{
|
@@ -65,10 +65,10 @@
|
|
65
65
|
const outputDir = getOutputDirectory();
|
66
66
|
|
67
67
|
return {
|
68
|
-
name: 'needle
|
68
|
+
name: 'needle:facebook-instant-games',
|
69
69
|
transformIndexHtml: {
|
70
|
-
|
71
|
-
|
70
|
+
order: 'post',
|
71
|
+
handler(html, _ctx) {
|
72
72
|
// post transform so we want to linebreak after the vite logs
|
73
73
|
console.log("\n");
|
74
74
|
|
@@ -19,7 +19,7 @@
|
|
19
19
|
const logToImportsLogFile = true;
|
20
20
|
|
21
21
|
return {
|
22
|
-
name: 'needle
|
22
|
+
name: 'needle:imports-logger',
|
23
23
|
enforce: 'pre',
|
24
24
|
resolveId(id, importer) {
|
25
25
|
|
@@ -11,7 +11,7 @@
|
|
11
11
|
let license = undefined;
|
12
12
|
|
13
13
|
return {
|
14
|
-
name: "needle
|
14
|
+
name: "needle:license",
|
15
15
|
enforce: 'pre',
|
16
16
|
async configResolved() {
|
17
17
|
if (userSettings.license) {
|
@@ -22,10 +22,10 @@
|
|
22
22
|
|
23
23
|
return {
|
24
24
|
// replace meta tags
|
25
|
-
name: 'needle
|
25
|
+
name: 'needle:meta',
|
26
26
|
transformIndexHtml: {
|
27
|
-
|
28
|
-
|
27
|
+
order: 'pre',
|
28
|
+
handler(html, _ctx) {
|
29
29
|
|
30
30
|
if (userSettings.allowMetaPlugin === false) return [];
|
31
31
|
|
@@ -11,10 +11,10 @@
|
|
11
11
|
if (userSettings.noPeer === true) return;
|
12
12
|
|
13
13
|
return {
|
14
|
-
name: 'needle
|
14
|
+
name: 'needle:peerjs',
|
15
15
|
transformIndexHtml: {
|
16
|
-
|
17
|
-
|
16
|
+
order: 'pre',
|
17
|
+
handler(html, _ctx) {
|
18
18
|
return {
|
19
19
|
html,
|
20
20
|
tags: [
|
@@ -19,7 +19,7 @@
|
|
19
19
|
if (userSettings.noPoster) return;
|
20
20
|
|
21
21
|
return {
|
22
|
-
name: 'needle
|
22
|
+
name: 'needle:poster',
|
23
23
|
configureServer(server) {
|
24
24
|
server.ws.on('needle:screenshot', async (data, client) => {
|
25
25
|
if (userSettings.noPoster) return;
|
@@ -55,8 +55,8 @@
|
|
55
55
|
});
|
56
56
|
},
|
57
57
|
transformIndexHtml: {
|
58
|
-
|
59
|
-
|
58
|
+
order: 'pre',
|
59
|
+
handler(html, ctx) {
|
60
60
|
const file = path.join(__dirname, 'poster-client.js');
|
61
61
|
let scriptContent = fs.readFileSync(file, 'utf8');
|
62
62
|
switch (userSettings.posterFormat) {
|
@@ -79,7 +79,7 @@
|
|
79
79
|
// log("PWA options", pwaOptions);
|
80
80
|
|
81
81
|
return {
|
82
|
-
name: 'needle
|
82
|
+
name: 'needle:pwa',
|
83
83
|
apply: 'build',
|
84
84
|
enforce: "post",
|
85
85
|
config(viteConfig) {
|
@@ -121,8 +121,8 @@
|
|
121
121
|
}
|
122
122
|
},
|
123
123
|
transformIndexHtml: {
|
124
|
-
|
125
|
-
|
124
|
+
order: 'pre',
|
125
|
+
handler(html, _ctx) {
|
126
126
|
// see https://vite-pwa-org.netlify.app/guide/auto-update.html
|
127
127
|
// post transform so we want to linebreak after the vite logs
|
128
128
|
console.log("\n");
|
@@ -43,7 +43,7 @@
|
|
43
43
|
if (projectConfig?.codegenDirectory?.length) ignorePatterns.push(`${projectConfig?.codegenDirectory}/**/*`);
|
44
44
|
|
45
45
|
return {
|
46
|
-
name: 'needle
|
46
|
+
name: 'needle:reload',
|
47
47
|
config(config) {
|
48
48
|
if (!config.server) config.server = { watch: { ignored: [] } };
|
49
49
|
else if (!config.server.watch) config.server.watch = { ignored: [] };
|
@@ -66,8 +66,8 @@
|
|
66
66
|
return insertScriptHotReloadCode(src, id);
|
67
67
|
},
|
68
68
|
transformIndexHtml: {
|
69
|
-
|
70
|
-
|
69
|
+
order: 'pre',
|
70
|
+
handler(html, _) {
|
71
71
|
if (config?.allowHotReload === false) return html;
|
72
72
|
if (userSettings?.allowHotReload === false) return html;
|
73
73
|
const file = path.join(__dirname, 'reload-client.js');
|
@@ -84,7 +84,6 @@
|
|
84
84
|
},
|
85
85
|
]
|
86
86
|
}
|
87
|
-
|
88
87
|
},
|
89
88
|
}
|
90
89
|
}
|
@@ -59,6 +59,7 @@
|
|
59
59
|
|
60
60
|
/**
|
61
61
|
* Animation component to play animations on a GameObject
|
62
|
+
* @category Animation and Sequencing
|
62
63
|
*/
|
63
64
|
export class Animation extends Behaviour implements IAnimationComponent {
|
64
65
|
|
@@ -189,6 +190,7 @@
|
|
189
190
|
|
190
191
|
/** @internal */
|
191
192
|
awake() {
|
193
|
+
this.mixer = undefined;
|
192
194
|
if (debug) console.log("Animation Awake", this.name, this);
|
193
195
|
if (this._tempAnimationsArray) {
|
194
196
|
this.animations = this._tempAnimationsArray;
|
@@ -1,15 +1,5 @@
|
|
1
|
-
import { Object3D
|
2
|
-
import type { GLTF } from "three/examples/jsm/loaders/GLTFLoader.js";
|
1
|
+
import { Object3D } from "three";
|
3
2
|
|
4
|
-
import { AnimationUtils } from "../engine/engine_animation.js";
|
5
|
-
import { addComponent, addNewComponent } from "../engine/engine_components.js";
|
6
|
-
import { ContextEvent, ContextRegistry } from "../engine/engine_context_registry.js";
|
7
|
-
import { Animation } from "./Animation.js";
|
8
|
-
import { Animator } from "./Animator.js";
|
9
|
-
import { GameObject } from "./Component.js";
|
10
|
-
import { PlayableDirector } from "./timeline/PlayableDirector.js";
|
11
|
-
|
12
|
-
|
13
3
|
const $objectAnimationKey = Symbol("objectIsAnimatedData");
|
14
4
|
|
15
5
|
/** Internal method - This marks an object as being animated. Make sure to always call isAnimated=false if you stop animating the object
|
@@ -36,40 +26,4 @@
|
|
36
26
|
if (!obj) return false;
|
37
27
|
const set = obj[$objectAnimationKey] as Set<object>;
|
38
28
|
return set !== undefined && set.size > 0;
|
39
|
-
}
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
ContextRegistry.registerCallback(ContextEvent.ContextCreated, args => {
|
44
|
-
const autoplay = args.context.domElement.getAttribute("autoplay");
|
45
|
-
if (autoplay !== undefined && (autoplay === "" || autoplay === "true" || autoplay === "1")) {
|
46
|
-
if (args.files) {
|
47
|
-
for (const file of args.files) {
|
48
|
-
const hasAnimation = GameObject.foreachComponent(file.file.scene, comp => {
|
49
|
-
if (comp.enabled === false) return undefined;
|
50
|
-
if (comp instanceof Animation && comp.playAutomatically || comp instanceof Animator || comp instanceof PlayableDirector && comp.playOnAwake === true) {
|
51
|
-
return true;
|
52
|
-
}
|
53
|
-
else if (comp instanceof Animation) {
|
54
|
-
comp.playAutomatically = true;
|
55
|
-
return true;
|
56
|
-
}
|
57
|
-
else if (comp instanceof PlayableDirector) {
|
58
|
-
comp.playOnAwake = true;
|
59
|
-
return true;
|
60
|
-
}
|
61
|
-
return undefined;
|
62
|
-
}, true);
|
63
|
-
if (hasAnimation !== true) {
|
64
|
-
AnimationUtils.assignAnimationsFromFile(file.file as GLTF, {
|
65
|
-
createAnimationComponent: (obj, _clip) => {
|
66
|
-
return addComponent(obj, Animation);
|
67
|
-
},
|
68
|
-
});
|
69
|
-
}
|
70
|
-
}
|
71
|
-
}
|
72
|
-
}
|
73
|
-
});
|
74
|
-
|
75
|
-
|
29
|
+
}
|
@@ -26,6 +26,7 @@
|
|
26
26
|
|
27
27
|
/** The Animator component is used to play animations on a GameObject. It is used in combination with an AnimatorController (which is a state machine for animations)
|
28
28
|
* A new AnimatorController can be created from code via `AnimatorController.createFromClips`
|
29
|
+
* @category Animation and Sequencing
|
29
30
|
*/
|
30
31
|
export class Animator extends Behaviour implements IAnimationComponent {
|
31
32
|
|
@@ -14,7 +14,9 @@
|
|
14
14
|
ULTRA = 3
|
15
15
|
}
|
16
16
|
|
17
|
-
|
17
|
+
/**
|
18
|
+
* @category Effects
|
19
|
+
*/
|
18
20
|
export class Antialiasing extends PostProcessingEffect {
|
19
21
|
get typeName(): string {
|
20
22
|
return "Antialiasing";
|
@@ -1,1 +1,5 @@
|
|
1
|
+
/**
|
2
|
+
* @module Experimental Components
|
3
|
+
*/
|
4
|
+
|
1
5
|
export * from "./networking/PlayerSync.js";
|
@@ -1,3 +1,39 @@
|
|
1
|
+
/**
|
2
|
+
* Contains Needle Engine Core Components.
|
3
|
+
*
|
4
|
+
* This includes
|
5
|
+
* - Interactivity components
|
6
|
+
* {@link DragControls}, {@link SmoothFollow}, {@link Duplicatable}, {@link SpatialTrigger}
|
7
|
+
*
|
8
|
+
* - Everywhere Actions
|
9
|
+
* {@link SetActiveOnClick}, {@link PlayAnimationOnClick}, {@link PlayAudioOnClick}, {@link ChangeMaterialOnClick}
|
10
|
+
* - Camera and user controls
|
11
|
+
* {@link OrbitControls}, {@link CharacterController}
|
12
|
+
* - Rendering components
|
13
|
+
* {@link Light}, {@link Renderer}, {@link ParticleSystem}, {@link Volume} (post processing), {@link ReflectionProbe}, {@link GroundProjectedEnv}, {@link ShadowCatcher}
|
14
|
+
* - Media components
|
15
|
+
* {@link AudioSource}, {@link VideoPlayer}
|
16
|
+
* - Helpers
|
17
|
+
* {@link AxesHelper}, {@link GridHelper}, {@link TransformGizmo}
|
18
|
+
* - Asset Management components
|
19
|
+
* {@link DropListener}, {@link SceneSwitcher}, {@link GltfExport}
|
20
|
+
* - XR components
|
21
|
+
* {@link WebXR}, {@link USDZExporter}, {@link XRRig}
|
22
|
+
* - Networking components
|
23
|
+
* {@link SyncedRoom}, {@link SyncedTransform}, {@link SyncedCamera}, {@link Voip}, {@link ScreenCapture}
|
24
|
+
* - Animation components
|
25
|
+
* {@link Animator}, {@link Animation}, {@link PlayableDirector}
|
26
|
+
* - Physics components
|
27
|
+
* {@link Rigidbody}, {@link BoxCollider}, {@link SphereCollider}, {@link MeshCollider}, {@link PhysicsMaterial}
|
28
|
+
* - Utilities
|
29
|
+
* {@link NeedleMenu}
|
30
|
+
* - and more.
|
31
|
+
*
|
32
|
+
* All these components are available wherever Needle Engine is used.
|
33
|
+
*
|
34
|
+
* @module Built-in Components
|
35
|
+
*/
|
36
|
+
|
1
37
|
export * from "./codegen/components.js";
|
2
38
|
export { Behaviour, Component, GameObject } from "./Component.js"
|
3
39
|
|
@@ -13,6 +49,11 @@
|
|
13
49
|
|
14
50
|
import "./CameraUtils.js"
|
15
51
|
import "./AnimationUtils.js"
|
52
|
+
import "./AnimationUtilsAutoplay.js"
|
16
53
|
|
17
54
|
export { DragMode } from "./DragControls.js"
|
18
55
|
export * from "./particlesystem/api.js"
|
56
|
+
|
57
|
+
// for correct type resolution in JSDoc
|
58
|
+
import type { PhysicsMaterial } from "../engine/engine_physics.types.js";
|
59
|
+
import type { Animation } from "./Animation.js";
|
@@ -1,1 +1,13 @@
|
|
1
|
+
/**
|
2
|
+
* This module contains the networking schemes used by Needle Engine.
|
3
|
+
* They are used to define the structure of the data that is sent over the network.
|
4
|
+
* Networking can use plain text or flatbuffers for serialization.
|
5
|
+
* Flatbuffers are more efficient and faster than plain text, but require more setup work.
|
6
|
+
*
|
7
|
+
* Some core components, like SyncedCamera or SyncedTransform, thus use Flatbuffers for networking to reduce latency and bandwidth.
|
8
|
+
*
|
9
|
+
* Schemes are compiled with [Flatbuffers 2.0](https://github.com/google/flatbuffers/releases/tag/v2.0.0).
|
10
|
+
* @module Networking Schemes
|
11
|
+
*/
|
12
|
+
|
1
13
|
export * from "./schemes.js";
|
@@ -1,3 +1,23 @@
|
|
1
|
+
/**
|
2
|
+
* Contains core functionality for Needle Engine.
|
3
|
+
* This includes
|
4
|
+
* - Context Management
|
5
|
+
* - Asset Loading
|
6
|
+
* - Component Lifecycle
|
7
|
+
* - Time Handling
|
8
|
+
* - XR support
|
9
|
+
* - Unified Input Handling
|
10
|
+
* - Needle Menu
|
11
|
+
* - Networking
|
12
|
+
* - Physics, Collisions, Raycasting
|
13
|
+
* - Math and Filtering Helpers
|
14
|
+
* - Rendering Utilities
|
15
|
+
* - Debugging Utilities, Gizmos
|
16
|
+
* - User agent detection
|
17
|
+
* - and more.
|
18
|
+
*
|
19
|
+
* @module Engine Core
|
20
|
+
*/
|
1
21
|
|
2
22
|
export * from "./debug/index.js";
|
3
23
|
export * from "./engine_addressables.js";
|
@@ -6,6 +6,7 @@
|
|
6
6
|
|
7
7
|
/**
|
8
8
|
* AudioListener represents a listener that can be attached to a GameObject to listen to audio sources in the scene.
|
9
|
+
* @category Multimedia
|
9
10
|
*/
|
10
11
|
export class AudioListener extends Behaviour {
|
11
12
|
|
@@ -34,7 +34,7 @@
|
|
34
34
|
|
35
35
|
/** The AudioSource can be used to play audio in the scene.
|
36
36
|
* Use `clip` to set the audio file to play.
|
37
|
-
* @category
|
37
|
+
* @category Multimedia
|
38
38
|
*/
|
39
39
|
export class AudioSource extends Behaviour {
|
40
40
|
|
@@ -17,6 +17,10 @@
|
|
17
17
|
|
18
18
|
const flipForwardQuaternion = new Quaternion().setFromAxisAngle(new Vector3(0, 1, 0), Math.PI);
|
19
19
|
|
20
|
+
/**
|
21
|
+
* @category XR
|
22
|
+
* @category Networking
|
23
|
+
*/
|
20
24
|
export class Avatar extends Behaviour {
|
21
25
|
|
22
26
|
@serializable(AssetReference)
|
@@ -22,8 +22,7 @@
|
|
22
22
|
this.brain = GameObject.getComponentInParent(this.gameObject, Avatar_Brain_LookAt);
|
23
23
|
}
|
24
24
|
if (!this.brain) {
|
25
|
-
|
26
|
-
this.brain = GameObject.addNewComponent(this.gameObject, Avatar_Brain_LookAt);
|
25
|
+
this.brain = GameObject.addComponent(this.gameObject, Avatar_Brain_LookAt);
|
27
26
|
}
|
28
27
|
if (this.brain && this.target) {
|
29
28
|
this.brain.controlledTarget = this.target;
|
@@ -6,6 +6,7 @@
|
|
6
6
|
|
7
7
|
/**
|
8
8
|
* AxesHelper is a component that displays the axes of the object in the scene.
|
9
|
+
* @category Helpers
|
9
10
|
*/
|
10
11
|
export class AxesHelper extends Behaviour {
|
11
12
|
@serializable()
|
@@ -26,8 +26,10 @@
|
|
26
26
|
|
27
27
|
export const $shadowDomOwner = Symbol("shadowDomOwner");
|
28
28
|
|
29
|
-
/**
|
30
|
-
*
|
29
|
+
/**
|
30
|
+
* Derive from this class if you want to implement your own UI components.
|
31
|
+
* It provides utility methods and simplifies managing the underlying three-mesh-ui hierarchy.
|
32
|
+
* @category User Interface
|
31
33
|
*/
|
32
34
|
export class BaseUIComponent extends Behaviour {
|
33
35
|
|
@@ -29,6 +29,9 @@
|
|
29
29
|
}
|
30
30
|
}
|
31
31
|
|
32
|
+
/**
|
33
|
+
* @category Everywhere Actions
|
34
|
+
*/
|
32
35
|
export class ChangeTransformOnClick extends Behaviour implements IPointerClickHandler, UsdzBehaviour {
|
33
36
|
|
34
37
|
@serializable(Object3D)
|
@@ -163,6 +166,9 @@
|
|
163
166
|
}
|
164
167
|
}
|
165
168
|
|
169
|
+
/**
|
170
|
+
* @category Everywhere Actions
|
171
|
+
*/
|
166
172
|
export class ChangeMaterialOnClick extends Behaviour implements IPointerClickHandler, UsdzBehaviour {
|
167
173
|
|
168
174
|
/**
|
@@ -346,6 +352,9 @@
|
|
346
352
|
}
|
347
353
|
}
|
348
354
|
|
355
|
+
/**
|
356
|
+
* @category Everywhere Actions
|
357
|
+
*/
|
349
358
|
export class SetActiveOnClick extends Behaviour implements IPointerClickHandler, UsdzBehaviour {
|
350
359
|
|
351
360
|
@serializable(Object3D)
|
@@ -576,6 +585,9 @@
|
|
576
585
|
}
|
577
586
|
}
|
578
587
|
|
588
|
+
/**
|
589
|
+
* @category Everywhere Actions
|
590
|
+
*/
|
579
591
|
export class HideOnStart extends Behaviour implements UsdzBehaviour {
|
580
592
|
|
581
593
|
start() {
|
@@ -601,6 +613,9 @@
|
|
601
613
|
}
|
602
614
|
}
|
603
615
|
|
616
|
+
/**
|
617
|
+
* @category Everywhere Actions
|
618
|
+
*/
|
604
619
|
export class EmphasizeOnClick extends Behaviour implements UsdzBehaviour {
|
605
620
|
|
606
621
|
@serializable()
|
@@ -629,6 +644,9 @@
|
|
629
644
|
afterCreateDocument(_ext, _context) { }
|
630
645
|
}
|
631
646
|
|
647
|
+
/**
|
648
|
+
* @category Everywhere Actions
|
649
|
+
*/
|
632
650
|
export class PlayAudioOnClick extends Behaviour implements IPointerClickHandler, UsdzBehaviour {
|
633
651
|
|
634
652
|
@serializable(AudioSource)
|
@@ -743,6 +761,9 @@
|
|
743
761
|
}
|
744
762
|
}
|
745
763
|
|
764
|
+
/**
|
765
|
+
* @category Everywhere Actions
|
766
|
+
*/
|
746
767
|
export class PlayAnimationOnClick extends Behaviour implements IPointerClickHandler, UsdzBehaviour, UsdzAnimation {
|
747
768
|
|
748
769
|
@serializable(Animator)
|
@@ -1056,6 +1077,9 @@
|
|
1056
1077
|
target?: PreliminaryAction;
|
1057
1078
|
}
|
1058
1079
|
|
1080
|
+
/**
|
1081
|
+
* @category Everywhere Actions
|
1082
|
+
*/
|
1059
1083
|
export class VisibilityAction extends PreliminaryAction {
|
1060
1084
|
|
1061
1085
|
//@type int
|
@@ -1077,6 +1101,9 @@
|
|
1077
1101
|
}
|
1078
1102
|
}
|
1079
1103
|
|
1104
|
+
/**
|
1105
|
+
* @category Everywhere Actions
|
1106
|
+
*/
|
1080
1107
|
export class TapGestureTrigger extends PreliminaryTrigger {
|
1081
1108
|
|
1082
1109
|
}
|
@@ -17,6 +17,8 @@
|
|
17
17
|
* bloom.scatter.value = 0.5;
|
18
18
|
* volume.add(bloom);
|
19
19
|
* ```
|
20
|
+
*
|
21
|
+
* @category Effects
|
20
22
|
*/
|
21
23
|
export class BloomEffect extends PostProcessingEffect {
|
22
24
|
|
@@ -1,13 +1,16 @@
|
|
1
1
|
import { Box3, Color, type ColorRepresentation, LineSegments, Object3D, Vector3 } from "three";
|
2
2
|
|
3
3
|
import { CreateWireCube, Gizmos } from "../engine/engine_gizmos.js";
|
4
|
-
import { getWorldPosition, getWorldScale } from "../engine/engine_three_utils.js";
|
4
|
+
import { getBoundingBox, getWorldPosition, getWorldScale } from "../engine/engine_three_utils.js";
|
5
5
|
import { getParam } from "../engine/engine_utils.js";
|
6
6
|
import { Behaviour } from "./Component.js";
|
7
7
|
|
8
8
|
const gizmos = getParam("gizmos");
|
9
9
|
const debug = getParam("debugboxhelper");
|
10
10
|
|
11
|
+
/**
|
12
|
+
* @category Helpers
|
13
|
+
*/
|
11
14
|
export class BoxHelperComponent extends Behaviour {
|
12
15
|
|
13
16
|
private box: Box3 | null = null;
|
@@ -15,44 +18,27 @@
|
|
15
18
|
private _lastMatrixUpdateFrame: number = -1;
|
16
19
|
private static _position: Vector3 = new Vector3();
|
17
20
|
private static _size: Vector3 = new Vector3(.01, .01, .01);
|
21
|
+
private static _emptyObjectSize: Vector3 = new Vector3(.01, .01, .01);
|
18
22
|
|
19
|
-
public isInBox(obj: Object3D
|
23
|
+
public isInBox(obj: Object3D): boolean | undefined {
|
20
24
|
if (!obj) return undefined;
|
21
25
|
|
22
|
-
// if (!obj.geometry.boundingBox) obj.geometry.computeBoundingBox();
|
23
|
-
// if (!obj.geometry.boundingBox) return undefined;
|
24
|
-
|
25
26
|
if (!this.box) {
|
26
27
|
this.box = new Box3();
|
27
28
|
}
|
28
29
|
|
30
|
+
getBoundingBox([obj], undefined, undefined, BoxHelperComponent.testBox);
|
29
31
|
|
30
|
-
if (
|
31
|
-
BoxHelperComponent.testBox.setFromObject(obj);
|
32
|
-
}
|
33
|
-
else if (obj.type === "Group") {
|
34
|
-
BoxHelperComponent.testBox.makeEmpty();
|
35
|
-
if (obj.children.length > 0) {
|
36
|
-
for (let i = 0; i < obj.children.length; i++) {
|
37
|
-
const ch = obj.children[i];
|
38
|
-
if (ch.type === "Mesh") {
|
39
|
-
BoxHelperComponent.testBox.expandByObject(obj);
|
40
|
-
}
|
41
|
-
}
|
42
|
-
}
|
43
|
-
}
|
44
|
-
else {
|
32
|
+
if (BoxHelperComponent.testBox.isEmpty()) {
|
45
33
|
const wp = getWorldPosition(obj, BoxHelperComponent._position);
|
46
|
-
|
47
|
-
if (scaleFactor !== undefined) size.multiplyScalar(scaleFactor);
|
48
|
-
BoxHelperComponent.testBox.setFromCenterAndSize(wp, size);
|
34
|
+
BoxHelperComponent.testBox.setFromCenterAndSize(wp, BoxHelperComponent._emptyObjectSize);
|
49
35
|
}
|
50
36
|
|
51
37
|
this.updateBox();
|
52
38
|
const intersects = this.box?.intersectsBox(BoxHelperComponent.testBox);
|
53
39
|
if (intersects) {
|
54
|
-
if (debug)
|
55
|
-
|
40
|
+
if (debug) Gizmos.DrawWireBox3(BoxHelperComponent.testBox, 0xff0000, 5);
|
41
|
+
|
56
42
|
}
|
57
43
|
return intersects;
|
58
44
|
}
|
@@ -60,6 +60,9 @@
|
|
60
60
|
selectedTrigger!: string;
|
61
61
|
}
|
62
62
|
|
63
|
+
/**
|
64
|
+
* @category User Interface
|
65
|
+
*/
|
63
66
|
export class Button extends Behaviour implements IPointerEventHandler {
|
64
67
|
|
65
68
|
/**
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import { EquirectangularReflectionMapping, Euler, Frustum,
|
1
|
+
import { EquirectangularReflectionMapping, Euler, Frustum, Matrix4, OrthographicCamera, PerspectiveCamera, Ray, Vector3 } from "three";
|
2
2
|
import { Texture } from "three";
|
3
3
|
|
4
4
|
import { isDevEnvironment, showBalloonMessage, showBalloonWarning } from "../engine/debug/index.js";
|
@@ -27,6 +27,9 @@
|
|
27
27
|
const debug = getParam("debugcam");
|
28
28
|
const debugscreenpointtoray = getParam("debugscreenpointtoray");
|
29
29
|
|
30
|
+
/**
|
31
|
+
* @category Camera Controls
|
32
|
+
*/
|
30
33
|
export class Camera extends Behaviour implements ICamera {
|
31
34
|
|
32
35
|
get isCamera() {
|
@@ -236,7 +239,19 @@
|
|
236
239
|
private _clearFlags: ClearFlags = ClearFlags.SolidColor;
|
237
240
|
private _skybox?: CameraSkybox;
|
238
241
|
|
242
|
+
/**
|
243
|
+
* Get the three.js camera object. This will create a camera if it does not exist yet.
|
244
|
+
* @returns {PerspectiveCamera | OrthographicCamera} the three camera
|
245
|
+
* @deprecated use {@link threeCamera} instead
|
246
|
+
*/
|
239
247
|
public get cam(): PerspectiveCamera | OrthographicCamera {
|
248
|
+
return this.threeCamera;
|
249
|
+
}
|
250
|
+
/**
|
251
|
+
* Get the three.js camera object. This will create a camera if it does not exist yet.
|
252
|
+
* @returns {PerspectiveCamera | OrthographicCamera} the three camera
|
253
|
+
*/
|
254
|
+
public get threeCamera(): PerspectiveCamera | OrthographicCamera {
|
240
255
|
if (this.activeAndEnabled)
|
241
256
|
this.buildCamera();
|
242
257
|
return this._cam!;
|
@@ -24,6 +24,9 @@
|
|
24
24
|
|
25
25
|
const debugLayout = getParam("debuguilayout");
|
26
26
|
|
27
|
+
/**
|
28
|
+
* @category User Interface
|
29
|
+
*/
|
27
30
|
export class Canvas extends UIRootComponent implements ICanvas {
|
28
31
|
|
29
32
|
get isCanvas() {
|
@@ -6,6 +6,9 @@
|
|
6
6
|
import { type ICanvasGroup, type IHasAlphaFactor } from "./Interfaces.js";
|
7
7
|
|
8
8
|
|
9
|
+
/**
|
10
|
+
* @category User Interface
|
11
|
+
*/
|
9
12
|
export class CanvasGroup extends Behaviour implements ICanvasGroup {
|
10
13
|
@serializable()
|
11
14
|
get alpha(): number {
|
@@ -13,6 +13,9 @@
|
|
13
13
|
|
14
14
|
const debug = getParam("debugcharactercontroller");
|
15
15
|
|
16
|
+
/**
|
17
|
+
* @category Camera Controls
|
18
|
+
*/
|
16
19
|
export class CharacterController extends Behaviour {
|
17
20
|
|
18
21
|
@serializable(Vector3)
|
@@ -102,6 +105,10 @@
|
|
102
105
|
}
|
103
106
|
}
|
104
107
|
|
108
|
+
/**
|
109
|
+
* @category Camera Controls
|
110
|
+
* @category Interactivity
|
111
|
+
*/
|
105
112
|
export class CharacterControllerInput extends Behaviour {
|
106
113
|
|
107
114
|
@serializable(CharacterController)
|
@@ -6,6 +6,9 @@
|
|
6
6
|
import { VolumeParameter } from "../VolumeParameter.js";
|
7
7
|
import { registerCustomEffectType, VolumeProfile } from "../VolumeProfile.js";
|
8
8
|
|
9
|
+
/**
|
10
|
+
* @category Effects
|
11
|
+
*/
|
9
12
|
export class ChromaticAberration extends PostProcessingEffect {
|
10
13
|
|
11
14
|
get typeName() {
|
@@ -17,7 +17,6 @@
|
|
17
17
|
* Colliders are used in combination with a Rigidbody to create physical interactions between objects.
|
18
18
|
* Colliders are registered with the physics engine when they are enabled and removed when they are disabled.
|
19
19
|
* @category Physics
|
20
|
-
* @inheritdoc
|
21
20
|
*/
|
22
21
|
export class Collider extends Behaviour implements ICollider {
|
23
22
|
|
@@ -100,6 +99,7 @@
|
|
100
99
|
|
101
100
|
/**
|
102
101
|
* SphereCollider is a collider that represents a sphere shape.
|
102
|
+
* @category Physics
|
103
103
|
*/
|
104
104
|
export class SphereCollider extends Collider implements ISphereCollider {
|
105
105
|
|
@@ -128,6 +128,7 @@
|
|
128
128
|
|
129
129
|
/**
|
130
130
|
* BoxCollider is a collider that represents a box shape.
|
131
|
+
* @category Physics
|
131
132
|
*/
|
132
133
|
export class BoxCollider extends Collider implements IBoxCollider {
|
133
134
|
|
@@ -172,6 +173,7 @@
|
|
172
173
|
/**
|
173
174
|
* MeshCollider is a collider that represents a mesh shape.
|
174
175
|
* The mesh collider can be used to create a collider from a mesh.
|
176
|
+
* @category Physics
|
175
177
|
*/
|
176
178
|
export class MeshCollider extends Collider {
|
177
179
|
|
@@ -239,6 +241,7 @@
|
|
239
241
|
|
240
242
|
/**
|
241
243
|
* CapsuleCollider is a collider that represents a capsule shape.
|
244
|
+
* @category Physics
|
242
245
|
*/
|
243
246
|
export class CapsuleCollider extends Collider {
|
244
247
|
@serializable(Vector3)
|
@@ -7,6 +7,9 @@
|
|
7
7
|
import { registerCustomEffectType } from "../VolumeProfile.js";
|
8
8
|
import { ToneMappingEffect } from "./Tonemapping.js";
|
9
9
|
|
10
|
+
/**
|
11
|
+
* @category Effects
|
12
|
+
*/
|
10
13
|
export class ColorAdjustments extends PostProcessingEffect {
|
11
14
|
|
12
15
|
get typeName() {
|
@@ -294,6 +294,7 @@
|
|
294
294
|
|
295
295
|
public static getAllComponents(go: IGameObject | Object3D): Component[] {
|
296
296
|
const componentsList = go.userData?.components;
|
297
|
+
if (!componentsList) return [];
|
297
298
|
const newList = [...componentsList];
|
298
299
|
return newList;
|
299
300
|
}
|
@@ -313,15 +314,17 @@
|
|
313
314
|
/**
|
314
315
|
* Needle Engine component base class. Component's are the main building blocks of the Needle Engine.
|
315
316
|
* Derive from {@link Behaviour} to implement your own using the provided lifecycle methods.
|
316
|
-
* Components can be added to
|
317
|
+
* Components can be added to any {@link Object3D} using {@link addComponent} or {@link GameObject.addComponent}.
|
317
318
|
*
|
318
|
-
* The most common lifecycle methods are
|
319
|
-
* XR specific callbacks include `onEnterXR`, `onLeaveXR`, `onUpdateXR`, `onControllerAdded` and `onControllerRemoved`.
|
320
|
-
* To receive pointer events implement `onPointerDown`, `onPointerUp`, `onPointerEnter`, `onPointerExit` and `onPointerMove`.
|
319
|
+
* The most common lifecycle methods are {@link update}, {@link awake}, {@link start}, {@link onEnable}, {@link onDisable} and {@link onDestroy}.
|
321
320
|
*
|
321
|
+
* XR specific callbacks include {@link onEnterXR}, {@link onLeaveXR}, {@link onUpdateXR}, {@link onXRControllerAdded} and {@link onXRControllerRemoved}.
|
322
|
+
*
|
323
|
+
* To receive pointer events implement {@link onPointerDown}, {@link onPointerUp}, {@link onPointerEnter}, {@link onPointerExit} and {@link onPointerMove}.
|
324
|
+
*
|
322
325
|
* @example
|
323
326
|
* ```typescript
|
324
|
-
* import { Behaviour } from "@
|
327
|
+
* import { Behaviour } from "@needle-tools/engine";
|
325
328
|
* export class MyComponent extends Behaviour {
|
326
329
|
* start() {
|
327
330
|
* console.log("Hello World");
|
@@ -331,6 +334,8 @@
|
|
331
334
|
* }
|
332
335
|
* }
|
333
336
|
* ```
|
337
|
+
*
|
338
|
+
* @group Components
|
334
339
|
*/
|
335
340
|
export abstract class Component implements IComponent, EventTarget,
|
336
341
|
Partial<INeedleXRSessionEventReceiver>,
|
@@ -32,6 +32,7 @@
|
|
32
32
|
|
33
33
|
/**
|
34
34
|
* ContactShadows is a component that allows to display contact shadows in the scene.
|
35
|
+
* @category Rendering
|
35
36
|
*/
|
36
37
|
export class ContactShadows extends Behaviour {
|
37
38
|
|
@@ -6,7 +6,6 @@
|
|
6
6
|
import { OneEuroFilterXYZ } from "../engine_math.js";
|
7
7
|
import { lookAtObject } from "../engine_three_utils.js";
|
8
8
|
import type { IContext, IGameObject } from "../engine_types.js";
|
9
|
-
import { getParam } from "../engine_utils.js";
|
10
9
|
import { isDevEnvironment } from "./debug.js";
|
11
10
|
import { onError } from "./debug_overlay.js";
|
12
11
|
|
@@ -1,6 +1,7 @@
|
|
1
1
|
|
2
2
|
import { Mesh } from "three";
|
3
3
|
|
4
|
+
import { Gizmos } from "../engine/engine_gizmos.js";
|
4
5
|
import { syncDestroy } from "../engine/engine_networking_instantiate.js";
|
5
6
|
import { getParam } from "../engine/engine_utils.js";
|
6
7
|
import { BoxHelperComponent } from "./BoxHelperComponent.js";
|
@@ -8,29 +9,52 @@
|
|
8
9
|
import { UsageMarker } from "./Interactable.js";
|
9
10
|
|
10
11
|
const debug = getParam("debugdeletable");
|
12
|
+
/**
|
13
|
+
* A box-shaped area that can be used to delete objects that get into it. Useful for sandbox-style builders or physics simulations.
|
14
|
+
* @category Interactivity
|
15
|
+
*/
|
16
|
+
export class DeleteBox extends BoxHelperComponent {
|
17
|
+
static _instances: DeleteBox[] = [];
|
18
|
+
|
19
|
+
onEnable(): void {
|
20
|
+
DeleteBox._instances.push(this);
|
21
|
+
}
|
11
22
|
|
12
|
-
|
23
|
+
onDisable(): void {
|
24
|
+
const idx = DeleteBox._instances.indexOf(this);
|
25
|
+
if (idx >= 0) DeleteBox._instances.splice(idx, 1);
|
26
|
+
}
|
27
|
+
}
|
13
28
|
|
14
|
-
|
29
|
+
/** Objects with this component can be destroyed by the {@link DeleteBox} component.
|
30
|
+
* @category Interactivity
|
31
|
+
*/
|
15
32
|
export class Deletable extends Behaviour {
|
16
33
|
|
17
|
-
private deleteBoxes: DeleteBox[] = [];
|
18
|
-
|
19
|
-
awake() {
|
20
|
-
this.deleteBoxes = GameObject.findObjectsOfType(DeleteBox, this.context);
|
21
|
-
}
|
22
|
-
|
23
34
|
update(): void {
|
24
|
-
for (const box of
|
35
|
+
for (const box of DeleteBox._instances) {
|
25
36
|
const obj = this.gameObject as unknown as Mesh;
|
26
37
|
const res = box.isInBox(obj);
|
27
38
|
if (res === true) {
|
28
39
|
const marker = GameObject.getComponentInParent(this.gameObject, UsageMarker);
|
29
40
|
if (!marker) {
|
30
|
-
if (debug)
|
41
|
+
if (debug) {
|
42
|
+
try {
|
43
|
+
if (box["box"]) {
|
44
|
+
const deleteBoxArea = box["box"];
|
45
|
+
const deletedObjectArea = BoxHelperComponent["testBox"];
|
46
|
+
Gizmos.DrawWireBox3(deleteBoxArea, 0xff0000, 5);
|
47
|
+
Gizmos.DrawWireBox3(deletedObjectArea, 0x0000ff, 5);
|
48
|
+
console.log("DeleteBox: Destroying", this.gameObject, { deleteBoxArea, deletedObjectArea });
|
49
|
+
}
|
50
|
+
else {
|
51
|
+
console.log("DeleteBox: Destroying", this.gameObject);
|
52
|
+
}
|
53
|
+
} catch (_e) {}
|
54
|
+
}
|
31
55
|
syncDestroy(this.gameObject, this.context.connection);
|
32
56
|
}
|
33
|
-
else if (debug) console.warn("
|
57
|
+
else if (debug) console.warn("DeleteBox: Not deleting object with usage marker", this.guid, marker)
|
34
58
|
}
|
35
59
|
}
|
36
60
|
}
|
@@ -15,6 +15,9 @@
|
|
15
15
|
|
16
16
|
const debug = getParam("debugpost");
|
17
17
|
|
18
|
+
/**
|
19
|
+
* @category Effects
|
20
|
+
*/
|
18
21
|
export class DepthOfField extends PostProcessingEffect {
|
19
22
|
|
20
23
|
get typeName() {
|
@@ -10,6 +10,9 @@
|
|
10
10
|
Mobile = 2 << 0,
|
11
11
|
}
|
12
12
|
|
13
|
+
/**
|
14
|
+
* @category Utilities
|
15
|
+
*/
|
13
16
|
export class DeviceFlag extends Behaviour {
|
14
17
|
|
15
18
|
@serializable()
|
@@ -42,6 +42,7 @@
|
|
42
42
|
|
43
43
|
/**
|
44
44
|
* DragControls allows you to drag objects around in the scene. It can be used to move objects in 2D (screen space) or 3D (world space).
|
45
|
+
* @category Interactivity
|
45
46
|
*/
|
46
47
|
export class DragControls extends Behaviour implements IPointerEventHandler {
|
47
48
|
|
@@ -370,14 +371,10 @@
|
|
370
371
|
private onLastDragEnd(evt: PointerEventData | null) {
|
371
372
|
if (!this || !this._isDragging) return;
|
372
373
|
this._isDragging = false;
|
373
|
-
if (!this._dragHelper) return;
|
374
374
|
for (const rb of this._draggingRigidbodies) {
|
375
375
|
rb.setVelocity(rb.smoothedVelocity);
|
376
376
|
}
|
377
377
|
this._draggingRigidbodies.length = 0;
|
378
|
-
const selected = this._dragHelper.selected;
|
379
|
-
if (debug) console.log("DRAG END", selected, selected?.visible)
|
380
|
-
this._dragHelper.setSelected(null, this.context);
|
381
378
|
this._targetObject = null;
|
382
379
|
if (evt?.object) {
|
383
380
|
const sync = GameObject.getComponentInChildren(evt.object, SyncedTransform);
|
@@ -388,6 +385,11 @@
|
|
388
385
|
}
|
389
386
|
if (this._marker)
|
390
387
|
this._marker.destroy();
|
388
|
+
|
389
|
+
if (!this._dragHelper) return;
|
390
|
+
const selected = this._dragHelper.selected;
|
391
|
+
if (debug) console.log("DRAG END", selected, selected?.visible)
|
392
|
+
this._dragHelper.setSelected(null, this.context);
|
391
393
|
}
|
392
394
|
}
|
393
395
|
|
@@ -939,9 +941,16 @@
|
|
939
941
|
// can only handle a single pointer
|
940
942
|
// if there's more, we defer to multi-touch drag handlers
|
941
943
|
if (numberOfPointers > 1) return;
|
942
|
-
|
943
|
-
|
944
|
-
|
944
|
+
const draggedObject = this.gameObject as IGameObject | null;
|
945
|
+
if (!draggedObject || !this._followObject) {
|
946
|
+
console.warn("Warning: DragPointerHandler doesn't have a dragged object. This is likely a bug.");
|
947
|
+
return;
|
948
|
+
}
|
949
|
+
const dragSource = this._followObject.parent as IGameObject | null;
|
950
|
+
if (!dragSource) {
|
951
|
+
console.warn("Warning: DragPointerHandler doesn't have a drag source. This is likely a bug.");
|
952
|
+
return;
|
953
|
+
}
|
945
954
|
this._followObject.updateMatrix();
|
946
955
|
const dragSourceWP = dragSource.worldPosition;
|
947
956
|
const rayDirection = dragSource.worldForward;
|
@@ -63,6 +63,8 @@
|
|
63
63
|
* const gltf = evt.detail as GLTF;
|
64
64
|
* });
|
65
65
|
* ```
|
66
|
+
*
|
67
|
+
* @category Asset Management
|
66
68
|
*/
|
67
69
|
export class DropListener extends Behaviour {
|
68
70
|
|
@@ -13,6 +13,7 @@
|
|
13
13
|
/**
|
14
14
|
* The Duplicatable component is used to duplicate a assigned {@link GameObject} when a pointer event occurs on the GameObject.
|
15
15
|
* It implements the {@link IPointerEventHandler} interface and can be used to expose duplication to the user in the editor without writing code.
|
16
|
+
* @category Interactivity
|
16
17
|
*/
|
17
18
|
export class Duplicatable extends Behaviour implements IPointerEventHandler {
|
18
19
|
|
@@ -122,12 +123,15 @@
|
|
122
123
|
const res = this.handleDuplication();
|
123
124
|
if (res) {
|
124
125
|
const dragControls = GameObject.getComponent(res, DragControls);
|
125
|
-
if (!dragControls) console.warn("Duplicated object does not have DragControls");
|
126
|
+
if (!dragControls) console.warn("Duplicated object does not have DragControls", res);
|
126
127
|
else {
|
127
128
|
dragControls.onPointerDown(args);
|
128
129
|
this._forwardPointerEvents.set(args.event.space, dragControls);
|
129
130
|
}
|
130
131
|
}
|
132
|
+
else {
|
133
|
+
console.warn("Could not duplicate object. Has the target object been destroyed?", this);
|
134
|
+
}
|
131
135
|
}
|
132
136
|
|
133
137
|
/** @internal */
|
@@ -154,6 +158,10 @@
|
|
154
158
|
if (!this.object) return null;
|
155
159
|
if (this._currentCount >= this.limitCount) return null;
|
156
160
|
if (this.object === this.gameObject) return null;
|
161
|
+
if (GameObject.isDestroyed(this.object)) {
|
162
|
+
this.object = null;
|
163
|
+
return null;
|
164
|
+
}
|
157
165
|
|
158
166
|
this.object.visible = true;
|
159
167
|
|
@@ -2,6 +2,9 @@
|
|
2
2
|
|
3
3
|
import { EffectProviderResult, PostProcessingEffect } from "../PostProcessingEffect.js";
|
4
4
|
|
5
|
+
/**
|
6
|
+
* @category Effects
|
7
|
+
*/
|
5
8
|
export class EffectWrapper extends PostProcessingEffect {
|
6
9
|
|
7
10
|
readonly effect: Effect;
|
@@ -14,7 +14,7 @@
|
|
14
14
|
|
15
15
|
/**
|
16
16
|
* The Addressables class is used to register and manage {@link AssetReference} types
|
17
|
-
* It can be accessed via {@link Context.Current} or
|
17
|
+
* It can be accessed from components via {@link Context.Current} or {@link Context.addressables} (e.g. `this.context.addressables`)
|
18
18
|
*/
|
19
19
|
export class Addressables {
|
20
20
|
|
@@ -90,8 +90,6 @@
|
|
90
90
|
export class AssetReference {
|
91
91
|
|
92
92
|
/**
|
93
|
-
* Experimental!
|
94
|
-
* @internal
|
95
93
|
* Get an AssetReference for a URL to be easily loaded.
|
96
94
|
* AssetReferences are cached so calling this method multiple times with the same arguments will always return the same AssetReference.
|
97
95
|
* @param url The URL of the asset to load. The url can be relative or absolute.
|
@@ -52,7 +52,6 @@
|
|
52
52
|
|
53
53
|
/**
|
54
54
|
* Utility class for working with animations.
|
55
|
-
* @category Animation
|
56
55
|
*/
|
57
56
|
export class AnimationUtils {
|
58
57
|
|
@@ -79,6 +79,7 @@
|
|
79
79
|
disposeObjectResources(obj.customDistanceMaterial);
|
80
80
|
obj.geometry = null;
|
81
81
|
obj.material = null;
|
82
|
+
obj.visible = false;
|
82
83
|
}
|
83
84
|
else if (obj instanceof Mesh) {
|
84
85
|
disposeObjectResources(obj.geometry);
|
@@ -87,6 +88,7 @@
|
|
87
88
|
disposeObjectResources(obj.customDistanceMaterial);
|
88
89
|
obj.geometry = null;
|
89
90
|
obj.material = null;
|
91
|
+
obj.visible = false;
|
90
92
|
}
|
91
93
|
else if (obj instanceof BufferGeometry) {
|
92
94
|
free(obj);
|
@@ -827,9 +827,12 @@
|
|
827
827
|
lastCharacterWasSpace = true;
|
828
828
|
}
|
829
829
|
}
|
830
|
-
|
830
|
+
|
831
|
+
if (isDevEnvironment() && name !== displayName)
|
832
|
+
console.debug("Generated display name: \"" + name + "\" → \"" + displayName + "\"");
|
831
833
|
return displayName.trim();
|
832
834
|
}
|
833
|
-
|
835
|
+
if (isDevEnvironment())
|
836
|
+
console.debug("Loading: use default name", name);
|
834
837
|
return name;
|
835
838
|
}
|
@@ -36,12 +36,14 @@
|
|
36
36
|
}
|
37
37
|
|
38
38
|
const _licenseCheckResultChangedCallbacks: ((result: boolean) => void)[] = [];
|
39
|
+
|
39
40
|
/** @internal */
|
40
41
|
export function onLicenseCheckResultChanged(cb: (result: boolean) => void) {
|
41
42
|
if (hasProLicense() || hasIndieLicense())
|
42
43
|
return cb(true);
|
43
44
|
_licenseCheckResultChangedCallbacks.push(cb);
|
44
45
|
}
|
46
|
+
|
45
47
|
function invokeLicenseCheckResultChanged(result: boolean) {
|
46
48
|
for (const cb of _licenseCheckResultChangedCallbacks) {
|
47
49
|
try {
|
@@ -1,6 +1,6 @@
|
|
1
|
-
import { LODsManager as _LODsManager, NEEDLE_progressive,
|
2
|
-
import { LOD_Results } from "@needle-tools/gltf-progressive/src/lods_manager.js";
|
3
|
-
import { Box3,
|
1
|
+
import { LODsManager as _LODsManager, NEEDLE_progressive, NEEDLE_progressive_plugin } from "@needle-tools/gltf-progressive";
|
2
|
+
import type { LOD_Results } from "@needle-tools/gltf-progressive/src/lods_manager.js";
|
3
|
+
import { Box3, Camera, Mesh, PerspectiveCamera, Scene, Sphere, WebGLRenderer } from "three";
|
4
4
|
|
5
5
|
import { findResourceUsers } from "./engine_assetdatabase.js";
|
6
6
|
import type { Context } from "./engine_context.js";
|
@@ -198,7 +198,7 @@
|
|
198
198
|
onPropertyChanged: Function,
|
199
199
|
};
|
200
200
|
|
201
|
-
export declare type FieldChangedCallbackFn = (newValue: any, previousValue: any) => void | boolean;
|
201
|
+
export declare type FieldChangedCallbackFn = (newValue: any, previousValue: any) => void | boolean | any;
|
202
202
|
|
203
203
|
/**
|
204
204
|
* **Decorate a field to be automatically networked synced**
|
@@ -207,7 +207,7 @@
|
|
207
207
|
*
|
208
208
|
* @param onFieldChanged name of a callback function that will be called when the field is changed.
|
209
209
|
* You can also pass in a function like so: syncField(myClass.prototype.myFunctionToBeCalled)
|
210
|
-
*
|
210
|
+
* Note: if you return `false` from this function you'll prevent the field from being synced with other clients
|
211
211
|
* (for example a networked color is sent as a number and may be converted to a color in the receiver again)
|
212
212
|
* Parameters: (newValue, previousValue)
|
213
213
|
*/
|
@@ -73,8 +73,9 @@
|
|
73
73
|
}
|
74
74
|
|
75
75
|
/** The Needle Engine networking server supports the concept of ownership that can be requested.
|
76
|
-
*
|
77
|
-
*
|
76
|
+
* This enum contains possible outgoing (Request*) and incoming (Response*) events for communicating ownership.
|
77
|
+
*
|
78
|
+
* Typically, using the {@link OwnershipModel} class instead of dealing with those events directly is preferred. */
|
78
79
|
export enum OwnershipEvent {
|
79
80
|
RequestHasOwner = 'request-has-owner',
|
80
81
|
ResponseHasOwner = "response-has-owner",
|
@@ -7,6 +7,9 @@
|
|
7
7
|
Maximum = 3,
|
8
8
|
}
|
9
9
|
|
10
|
+
/**
|
11
|
+
* Properties for physics simulation, like friction or bounciness.
|
12
|
+
*/
|
10
13
|
export type PhysicsMaterial = {
|
11
14
|
bounceCombine?: PhysicsMaterialCombine;
|
12
15
|
bounciness?: number;
|
@@ -39,6 +39,7 @@
|
|
39
39
|
|
40
40
|
const printGltf = utils.getParam("printGltf") || utils.getParam("printgltf");
|
41
41
|
const downloadGltf = utils.getParam("downloadgltf");
|
42
|
+
const debugFileTypes = utils.getParam("debugfileformat");
|
42
43
|
|
43
44
|
// const loader = new GLTFLoader();
|
44
45
|
// registerExtensions(loader);
|
@@ -100,7 +101,7 @@
|
|
100
101
|
export async function createLoader(url: string, context: Context): Promise<GLTFLoader | FBXLoader | USDZLoader | OBJLoader | null> {
|
101
102
|
|
102
103
|
const type = await tryDetermineFileTypeFromURL(url) || "unknown";
|
103
|
-
console.debug("Determined file type: " + type + " for url", url);
|
104
|
+
if (debugFileTypes) console.debug("Determined file type: " + type + " for url", url);
|
104
105
|
|
105
106
|
switch (type) {
|
106
107
|
case "unknown":
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import { AnimationAction, Box3, Box3Helper, Color, Euler, GridHelper, Material, Mesh, MeshStandardMaterial, Object3D, PerspectiveCamera, PlaneGeometry, Quaternion, Scene, ShadowMaterial, Texture, Uniform, Vector3 } from "three";
|
1
|
+
import { AnimationAction, Box3, Box3Helper, Color, Euler, GridHelper, Layers, Material, Mesh, MeshStandardMaterial, Object3D, PerspectiveCamera, PlaneGeometry, Quaternion, Scene, ShadowMaterial, Texture, Uniform, Vector3 } from "three";
|
2
2
|
import { ShaderMaterial, WebGLRenderer } from "three";
|
3
3
|
import { GroundedSkybox } from "three/examples/jsm/objects/GroundedSkybox.js";
|
4
4
|
|
@@ -548,13 +548,14 @@
|
|
548
548
|
}
|
549
549
|
|
550
550
|
/**
|
551
|
-
* Get the bounding box of a list of objects
|
552
|
-
* @param objects
|
553
|
-
* @param ignore
|
554
|
-
* @param
|
551
|
+
* Get the axis-aligned bounding box of a list of objects.
|
552
|
+
* @param objects The objects to get the bounding box from.
|
553
|
+
* @param ignore Objects to ignore when calculating the bounding box. Objects that are invisible (gizmos, helpers, etc.) are excluded by default.
|
554
|
+
* @param layers The layers to include. Typically the main camera's layers.
|
555
|
+
* @param result The result box to store the bounding box in. Returns a new box if not passed in.
|
555
556
|
*/
|
556
|
-
export function getBoundingBox(objects: Object3D[], ignore: ((obj: Object3D) => void | boolean) | Array<Object3D | null | undefined> | undefined = undefined): Box3 {
|
557
|
-
const box = new Box3();
|
557
|
+
export function getBoundingBox(objects: Object3D[], ignore: ((obj: Object3D) => void | boolean) | Array<Object3D | null | undefined> | undefined = undefined, layers: Layers | undefined | null = undefined, result: Box3 | undefined = undefined): Box3 {
|
558
|
+
const box = result || new Box3();
|
558
559
|
box.makeEmpty();
|
559
560
|
|
560
561
|
const emptyChildren = [];
|
@@ -573,9 +574,9 @@
|
|
573
574
|
// // Ignore shadow catcher geometry
|
574
575
|
if ((obj as Mesh).material instanceof ShadowMaterial) allowExpanding = false;
|
575
576
|
// ONLY fit meshes
|
576
|
-
if (!(isMesh(obj)))
|
577
|
-
|
578
|
-
|
577
|
+
if (!(isMesh(obj))) allowExpanding = false;
|
578
|
+
// Layer test, typically with the main camera
|
579
|
+
if (layers && obj.layers.test(layers) === false) allowExpanding = false;
|
579
580
|
if (allowExpanding) {
|
580
581
|
// Ignore things parented to the camera + ignore the camera
|
581
582
|
if (ignore && Array.isArray(ignore) && ignore?.includes(obj)) return;
|
@@ -38,7 +38,7 @@
|
|
38
38
|
if (!ext?.length) {
|
39
39
|
ext = urlobj.pathname.split(".").pop()?.toUpperCase();
|
40
40
|
}
|
41
|
-
console.debug("Use file extension to determine type: " + ext);
|
41
|
+
if (debug) console.debug("Use file extension to determine type: " + ext);
|
42
42
|
switch (ext) {
|
43
43
|
case "GLTF":
|
44
44
|
return "gltf";
|
@@ -554,9 +554,6 @@
|
|
554
554
|
};
|
555
555
|
|
556
556
|
|
557
|
-
|
558
|
-
|
559
|
-
|
560
557
|
declare global {
|
561
558
|
interface NavigatorUAData {
|
562
559
|
platform: string;
|
@@ -566,66 +563,145 @@
|
|
566
563
|
}
|
567
564
|
}
|
568
565
|
|
569
|
-
|
566
|
+
/**
|
567
|
+
* Utility function to detect certain device types (mobile, desktop) or browsers
|
568
|
+
*/
|
569
|
+
export namespace DeviceUtilities {
|
570
|
+
let _isDesktop: boolean | undefined;
|
571
|
+
/** Is MacOS or Windows (and not hololens) */
|
572
|
+
export function isDesktop() {
|
573
|
+
if (_isDesktop !== undefined) return _isDesktop;
|
574
|
+
const ua = window.navigator.userAgent;
|
575
|
+
const standalone = /Windows|MacOS|Mac OS/.test(ua);
|
576
|
+
const isHololens = /Windows NT/.test(ua) && /Edg/.test(ua) && !/Win64/.test(ua);
|
577
|
+
return _isDesktop = standalone && !isHololens && !isiOS();
|
578
|
+
}
|
579
|
+
let _ismobile: boolean | undefined;
|
580
|
+
/** @returns `true` if it's a phone or tablet */
|
581
|
+
export function isMobileDevice() {
|
582
|
+
if (_ismobile !== undefined) return _ismobile;
|
583
|
+
if ((typeof window.orientation !== "undefined") || (navigator.userAgent.indexOf('IEMobile') !== -1)) {
|
584
|
+
return _ismobile = true;
|
585
|
+
}
|
586
|
+
return _ismobile = /iPhone|iPad|iPod|Android|IEMobile/i.test(navigator.userAgent);
|
587
|
+
}
|
588
|
+
/**
|
589
|
+
* @deprecated use {@link isiPad} instead
|
590
|
+
*/
|
591
|
+
export function isIPad() {
|
592
|
+
return /iPad/.test(navigator.userAgent);
|
593
|
+
}
|
594
|
+
/**
|
595
|
+
* @returns `true` if it's an iPad by checking the navigator.userAgent
|
596
|
+
*/
|
597
|
+
export function isiPad() {
|
598
|
+
return /iPad/.test(navigator.userAgent);
|
599
|
+
}
|
600
|
+
export function isAndroidDevice() {
|
601
|
+
return /Android/.test(navigator.userAgent);
|
602
|
+
}
|
603
|
+
/** @returns `true` if we're currently using the mozilla XR browser */
|
604
|
+
export function isMozillaXR() {
|
605
|
+
return /WebXRViewer\//i.test(navigator.userAgent);
|
606
|
+
}
|
607
|
+
let __isMacOs: boolean | undefined;
|
608
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/Navigator/userAgentData
|
609
|
+
export function isMacOS() {
|
610
|
+
if (__isMacOs !== undefined) return __isMacOs;
|
611
|
+
if (navigator.userAgentData) {
|
612
|
+
// Use modern UA Client Hints API if available
|
613
|
+
return __isMacOs = navigator.userAgentData.platform === 'macOS';
|
614
|
+
} else {
|
615
|
+
// Fallback to user agent string parsing
|
616
|
+
const userAgent = navigator.userAgent.toLowerCase();
|
617
|
+
return __isMacOs = userAgent.includes('mac os x') || userAgent.includes('macintosh');
|
618
|
+
}
|
619
|
+
}
|
620
|
+
let __isiOS: boolean | undefined;
|
621
|
+
const iosDevices = ['iPad Simulator', 'iPhone Simulator', 'iPod Simulator', 'iPad', 'iPhone', 'iPod'];
|
622
|
+
/** @returns `true` for iOS devices like iPad, iPhone, iPod... */
|
623
|
+
export function isiOS() {
|
624
|
+
if (__isiOS !== undefined) return __isiOS;
|
625
|
+
return __isiOS = iosDevices.includes(navigator.platform)
|
626
|
+
// iPad on iOS 13 detection
|
627
|
+
|| (navigator.userAgent.includes("Mac") && "ontouchend" in document)
|
628
|
+
}
|
629
|
+
|
630
|
+
/** @returns `true` if we're currently on safari */
|
631
|
+
export function isSafari() {
|
632
|
+
return /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
633
|
+
}
|
634
|
+
|
635
|
+
/** @returns true if we're currently running on quest */
|
636
|
+
export function isQuest() {
|
637
|
+
return navigator.userAgent.includes("OculusBrowser");
|
638
|
+
}
|
639
|
+
|
640
|
+
/** @returns `true` if the user allowed to use the microphone */
|
641
|
+
export async function microphonePermissionsGranted() {
|
642
|
+
try {
|
643
|
+
//@ts-ignore
|
644
|
+
const res = await navigator.permissions.query({ name: 'microphone' });
|
645
|
+
if (res.state === "denied") {
|
646
|
+
return false;
|
647
|
+
}
|
648
|
+
return true;
|
649
|
+
}
|
650
|
+
catch (err) {
|
651
|
+
console.error("Error querying `microphone` permissions.", err);
|
652
|
+
return false;
|
653
|
+
}
|
654
|
+
}
|
655
|
+
|
656
|
+
}
|
657
|
+
|
658
|
+
|
659
|
+
|
570
660
|
/** Is MacOS or Windows (and not hololens) */
|
571
661
|
export function isDesktop() {
|
572
|
-
|
573
|
-
const ua = window.navigator.userAgent;
|
574
|
-
const standalone = /Windows|MacOS|Mac OS/.test(ua);
|
575
|
-
const isHololens = /Windows NT/.test(ua) && /Edg/.test(ua) && !/Win64/.test(ua);
|
576
|
-
return _isDesktop = standalone && !isHololens && !isiOS();
|
662
|
+
return DeviceUtilities.isDesktop();
|
577
663
|
}
|
578
664
|
|
579
|
-
let _ismobile: boolean | undefined;
|
580
665
|
/** @returns `true` if it's a phone or tablet */
|
581
666
|
export function isMobileDevice() {
|
582
|
-
|
583
|
-
if ((typeof window.orientation !== "undefined") || (navigator.userAgent.indexOf('IEMobile') !== -1)) {
|
584
|
-
return _ismobile = true;
|
585
|
-
}
|
586
|
-
return _ismobile = /iPhone|iPad|iPod|Android|IEMobile/i.test(navigator.userAgent);
|
667
|
+
return DeviceUtilities.isMobileDevice();
|
587
668
|
}
|
588
669
|
|
670
|
+
/** @deprecated use {@link isiPad} instead */
|
671
|
+
export function isIPad() {
|
672
|
+
return DeviceUtilities.isiPad();
|
673
|
+
}
|
674
|
+
|
675
|
+
export function isiPad() {
|
676
|
+
return DeviceUtilities.isiPad();
|
677
|
+
}
|
678
|
+
|
589
679
|
export function isAndroidDevice() {
|
590
|
-
return
|
680
|
+
return DeviceUtilities.isAndroidDevice();
|
591
681
|
}
|
592
682
|
|
593
683
|
/** @returns `true` if we're currently using the mozilla XR browser */
|
594
684
|
export function isMozillaXR() {
|
595
|
-
return
|
685
|
+
return DeviceUtilities.isMozillaXR();
|
596
686
|
}
|
597
687
|
|
598
|
-
let __isMacOs: boolean | undefined;
|
599
688
|
// https://developer.mozilla.org/en-US/docs/Web/API/Navigator/userAgentData
|
600
689
|
export function isMacOS() {
|
601
|
-
|
602
|
-
if (navigator.userAgentData) {
|
603
|
-
// Use modern UA Client Hints API if available
|
604
|
-
return __isMacOs = navigator.userAgentData.platform === 'macOS';
|
605
|
-
} else {
|
606
|
-
// Fallback to user agent string parsing
|
607
|
-
const userAgent = navigator.userAgent.toLowerCase();
|
608
|
-
return __isMacOs = userAgent.includes('mac os x') || userAgent.includes('macintosh');
|
609
|
-
}
|
690
|
+
return DeviceUtilities.isMacOS();
|
610
691
|
}
|
611
692
|
|
612
|
-
let __isiOS: boolean | undefined;
|
613
|
-
const iosDevices = ['iPad Simulator', 'iPhone Simulator', 'iPod Simulator', 'iPad', 'iPhone', 'iPod'];
|
614
693
|
/** @returns `true` for iOS devices like iPad, iPhone, iPod... */
|
615
694
|
export function isiOS() {
|
616
|
-
|
617
|
-
return __isiOS = iosDevices.includes(navigator.platform)
|
618
|
-
// iPad on iOS 13 detection
|
619
|
-
|| (navigator.userAgent.includes("Mac") && "ontouchend" in document)
|
695
|
+
return DeviceUtilities.isiOS();
|
620
696
|
}
|
621
697
|
|
622
698
|
/** @returns `true` if we're currently on safari */
|
623
699
|
export function isSafari() {
|
624
|
-
return
|
700
|
+
return DeviceUtilities.isSafari();
|
625
701
|
}
|
626
702
|
|
627
703
|
export function isQuest() {
|
628
|
-
return
|
704
|
+
return DeviceUtilities.isQuest();
|
629
705
|
}
|
630
706
|
|
631
707
|
/** @returns `true` if the user allowed to use the microphone */
|
@@ -1,9 +1,9 @@
|
|
1
1
|
import "./engine_hot_reload.js";
|
2
2
|
import "./tests/test_utils.js";
|
3
3
|
|
4
|
-
import { RGBAColor } from "../engine-components/js-extensions/RGBAColor.js";
|
5
4
|
import * as engine_scenetools from "./engine_scenetools.js";
|
6
5
|
import * as engine_setup from "./engine_setup.js";
|
6
|
+
import { RGBAColor } from "./js-extensions/RGBAColor.js";
|
7
7
|
|
8
8
|
const engine : any = {
|
9
9
|
...engine_setup,
|
@@ -30,6 +30,9 @@
|
|
30
30
|
|
31
31
|
declare type IComponentCanMaybeReceiveEvents = IPointerEventHandler & IComponent & { interactable?: boolean };
|
32
32
|
|
33
|
+
/**
|
34
|
+
* @category User Interface
|
35
|
+
*/
|
33
36
|
export class EventSystem extends Behaviour {
|
34
37
|
private static _eventSystemMap = new Map<Context, EventSystem[]>();
|
35
38
|
|
@@ -8,12 +8,13 @@
|
|
8
8
|
@serializable()
|
9
9
|
eventID!: EventType;
|
10
10
|
@serializable(EventList)
|
11
|
-
callback
|
11
|
+
callback?: EventList;
|
12
12
|
}
|
13
13
|
|
14
14
|
/**
|
15
15
|
* The EventTrigger component is used to trigger events when certain pointer events occur on the GameObject.
|
16
16
|
* It implements the {@link IPointerEventHandler} interface and can be used to expose events to the user in the editor without writing code.
|
17
|
+
* @category Interactivity
|
17
18
|
*/
|
18
19
|
export class EventTrigger extends Behaviour implements IPointerEventHandler {
|
19
20
|
|
@@ -26,7 +27,7 @@
|
|
26
27
|
if (!this.triggers) return;
|
27
28
|
for (const trigger of this.triggers) {
|
28
29
|
if (trigger.eventID === type) {
|
29
|
-
trigger.callback
|
30
|
+
trigger.callback?.invoke();
|
30
31
|
}
|
31
32
|
}
|
32
33
|
}
|
@@ -6,7 +6,10 @@
|
|
6
6
|
import { FrameEvent } from "../engine/engine_setup.js";
|
7
7
|
import { Behaviour } from "./Component.js";
|
8
8
|
|
9
|
-
|
9
|
+
/**
|
10
|
+
* BoxGizmo is a component that displays a box around the object in the scene. It can optionally expand to the object's bounds.
|
11
|
+
* @category Helpers
|
12
|
+
*/
|
10
13
|
export class BoxGizmo extends Behaviour {
|
11
14
|
@serializable()
|
12
15
|
objectBounds: boolean = false;
|
@@ -26,6 +26,9 @@
|
|
26
26
|
sceneRoot?: Object3D;
|
27
27
|
}
|
28
28
|
|
29
|
+
/**
|
30
|
+
* @category Asset Management
|
31
|
+
*/
|
29
32
|
export class GltfExport extends Behaviour {
|
30
33
|
|
31
34
|
@serializable()
|
@@ -21,6 +21,9 @@
|
|
21
21
|
borderOpacity: 1,
|
22
22
|
};
|
23
23
|
|
24
|
+
/**
|
25
|
+
* @category User Interface
|
26
|
+
*/
|
24
27
|
export class Graphic extends BaseUIComponent implements IGraphic, IRectTransformChangedReceiver {
|
25
28
|
|
26
29
|
get isGraphic() { return true; }
|
@@ -254,6 +257,9 @@
|
|
254
257
|
}
|
255
258
|
}
|
256
259
|
|
260
|
+
/**
|
261
|
+
* @category User Interface
|
262
|
+
*/
|
257
263
|
export class MaskableGraphic extends Graphic {
|
258
264
|
|
259
265
|
private _flippedObject = false;
|
@@ -6,6 +6,7 @@
|
|
6
6
|
|
7
7
|
/**
|
8
8
|
* GridHelper is a component that allows to display a grid in the scene.
|
9
|
+
* @category Helpers
|
9
10
|
*/
|
10
11
|
export class GridHelper extends Behaviour {
|
11
12
|
|
@@ -1,8 +1,9 @@
|
|
1
1
|
import { ShaderMaterial, Texture } from "three";
|
2
2
|
import { GroundedSkybox as GroundProjection } from 'three/examples/jsm/objects/GroundedSkybox.js';
|
3
3
|
|
4
|
+
import { Gizmos } from "../engine/engine_gizmos.js";
|
4
5
|
import { serializable } from "../engine/engine_serialization_decorator.js";
|
5
|
-
import { getBoundingBox,
|
6
|
+
import { getBoundingBox, getTempVector, getWorldScale, Graphics, setVisibleInCustomShadowRendering,setWorldPosition } from "../engine/engine_three_utils.js";
|
6
7
|
import { delayForFrames, getParam, Watch as Watch } from "../engine/engine_utils.js";
|
7
8
|
import { Behaviour } from "./Component.js";
|
8
9
|
|
@@ -10,6 +11,7 @@
|
|
10
11
|
|
11
12
|
/**
|
12
13
|
* GroundProjectedEnv creates a ground projection of the current environment map.
|
14
|
+
* @category Rendering
|
13
15
|
*/
|
14
16
|
export class GroundProjectedEnv extends Behaviour {
|
15
17
|
|
@@ -41,7 +43,7 @@
|
|
41
43
|
|
42
44
|
/**
|
43
45
|
* How far the camera that took the photo was above the ground. A larger value will magnify the downward part of the image.
|
44
|
-
* @
|
46
|
+
* @default 3
|
45
47
|
*/
|
46
48
|
@serializable()
|
47
49
|
set height(val: number) {
|
@@ -149,36 +151,52 @@
|
|
149
151
|
if (!this.gameObject || this.destroyed) {
|
150
152
|
return;
|
151
153
|
}
|
154
|
+
|
155
|
+
let needsNewAutoFit = true;
|
152
156
|
// offset here must be zero (and not .01) because the plane occlusion (when mesh tracking is active) is otherwise not correct
|
153
157
|
const offset = 0;
|
154
158
|
if (!this._projection || this.context.scene.environment !== this._lastEnvironment || this._height !== this._lastHeight || this._radius !== this._lastRadius) {
|
155
159
|
if (debug)
|
156
160
|
console.log("Create/Update Ground Projection", this.context.scene.environment.name);
|
157
161
|
this._projection?.removeFromParent();
|
158
|
-
this._projection
|
162
|
+
if (!this._projection || (this.context.scene.environment !== this._lastEnvironment || this._lastHeight !== this._height || this._lastRadius !== this._radius)) {
|
163
|
+
try {
|
164
|
+
this._projection = new GroundProjection(this.context.scene.environment, this._height, this.radius, 64);
|
165
|
+
}
|
166
|
+
catch (e) {
|
167
|
+
console.error("Failed to enable GroundProjection for environment", e);
|
168
|
+
return;
|
169
|
+
}
|
170
|
+
}
|
171
|
+
else
|
172
|
+
needsNewAutoFit = false;
|
159
173
|
this._projection.position.y = this._height - offset;
|
160
174
|
this._projection.name = "GroundProjection";
|
161
175
|
setVisibleInCustomShadowRendering(this._projection, false);
|
162
176
|
}
|
177
|
+
else {
|
178
|
+
needsNewAutoFit = false;
|
179
|
+
}
|
163
180
|
|
164
|
-
this._lastEnvironment = this.context.scene.environment;
|
165
|
-
this._lastHeight = this._height;
|
166
|
-
this._lastRadius = this._radius;
|
167
181
|
if (!this._projection.parent)
|
168
182
|
this.gameObject.add(this._projection);
|
169
183
|
|
170
|
-
if (this.autoFit) {
|
184
|
+
if (this.autoFit && needsNewAutoFit) {
|
171
185
|
// TODO: should also update the radius (?)
|
172
186
|
this._projection.updateWorldMatrix(true, true);
|
173
|
-
const
|
174
|
-
|
187
|
+
const box = getBoundingBox(this.context.scene.children, [this._projection]);
|
188
|
+
|
189
|
+
const floor_y = box.min.y;
|
175
190
|
if (floor_y < Infinity) {
|
176
|
-
const wp =
|
177
|
-
wp.x =
|
178
|
-
|
179
|
-
wp.
|
191
|
+
const wp = getTempVector();
|
192
|
+
wp.x = box.min.x + (box.max.x - box.min.x) * .5;
|
193
|
+
const scale = getWorldScale(this.gameObject).x;
|
194
|
+
wp.y = floor_y + (this._height * scale) - offset;
|
195
|
+
wp.z = box.min.z + (box.max.z - box.min.z) * .5;
|
180
196
|
setWorldPosition(this._projection, wp);
|
181
197
|
}
|
198
|
+
|
199
|
+
if (debug) Gizmos.DrawWireBox3(box, 0x00ff00, 5);
|
182
200
|
}
|
183
201
|
|
184
202
|
/* TODO realtime adjustments aren't possible anymore with GroundedSkybox (mesh generation)
|
@@ -187,10 +205,14 @@
|
|
187
205
|
this.env.height = this._height;
|
188
206
|
*/
|
189
207
|
|
190
|
-
if (this.context.scene.backgroundBlurriness > 0.001
|
208
|
+
if (this.context.scene.backgroundBlurriness > 0.001 && this._needsTextureUpdate) {
|
191
209
|
this.updateBlurriness();
|
192
210
|
}
|
193
211
|
|
212
|
+
this._lastEnvironment = this.context.scene.environment;
|
213
|
+
this._lastHeight = this._height;
|
214
|
+
this._lastRadius = this._radius;
|
215
|
+
|
194
216
|
this._needsTextureUpdate = false;
|
195
217
|
}
|
196
218
|
|
@@ -11,6 +11,9 @@
|
|
11
11
|
rect?: { width: number, height: number };
|
12
12
|
}
|
13
13
|
|
14
|
+
/**
|
15
|
+
* @category User Interface
|
16
|
+
*/
|
14
17
|
export class Image extends MaskableGraphic {
|
15
18
|
|
16
19
|
set image(img: Texture | null) {
|
@@ -78,6 +81,9 @@
|
|
78
81
|
}
|
79
82
|
}
|
80
83
|
|
84
|
+
/**
|
85
|
+
* @category User Interface
|
86
|
+
*/
|
81
87
|
export class RawImage extends MaskableGraphic {
|
82
88
|
@serializable(Texture)
|
83
89
|
get mainTexture(): Texture | undefined {
|
@@ -1,2 +1,2 @@
|
|
1
1
|
|
2
|
-
export { NeedleMenu } from "./needle-menu.js";
|
2
|
+
export { NeedleMenu } from "./needle menu/needle-menu.js";
|
@@ -9,6 +9,9 @@
|
|
9
9
|
|
10
10
|
const debug = getParam("debuginputfield");
|
11
11
|
|
12
|
+
/**
|
13
|
+
* @category User Interface
|
14
|
+
*/
|
12
15
|
export class InputField extends Behaviour implements IPointerEventHandler {
|
13
16
|
|
14
17
|
get text(): string {
|
@@ -300,6 +300,9 @@
|
|
300
300
|
|
301
301
|
}
|
302
302
|
|
303
|
+
/**
|
304
|
+
* @category User Interface
|
305
|
+
*/
|
303
306
|
export class VerticalLayoutGroup extends HorizontalOrVerticalLayoutGroup {
|
304
307
|
|
305
308
|
protected get primaryAxis() {
|
@@ -308,6 +311,9 @@
|
|
308
311
|
|
309
312
|
}
|
310
313
|
|
314
|
+
/**
|
315
|
+
* @category User Interface
|
316
|
+
*/
|
311
317
|
export class HorizontalLayoutGroup extends HorizontalOrVerticalLayoutGroup {
|
312
318
|
|
313
319
|
protected get primaryAxis() {
|
@@ -316,6 +322,9 @@
|
|
316
322
|
|
317
323
|
}
|
318
324
|
|
325
|
+
/**
|
326
|
+
* @category User Interface
|
327
|
+
*/
|
319
328
|
export class GridLayoutGroup extends LayoutGroup {
|
320
329
|
protected onCalculateLayout() {
|
321
330
|
}
|
@@ -88,6 +88,7 @@
|
|
88
88
|
* The light can be set to cast shadows and the shadow type can be set to hard or soft shadows.
|
89
89
|
* The light can be set to be baked or realtime.
|
90
90
|
* The light can be set to be a main light which will be used for the main directional light in the scene.
|
91
|
+
* @category Rendering
|
91
92
|
*/
|
92
93
|
export class Light extends Behaviour implements ILight {
|
93
94
|
|
@@ -40,6 +40,7 @@
|
|
40
40
|
|
41
41
|
/**
|
42
42
|
* LODGroup allows to create a group of LOD levels for an object.
|
43
|
+
* @category Rendering
|
43
44
|
*/
|
44
45
|
export class LODGroup extends Behaviour {
|
45
46
|
|
@@ -20,7 +20,6 @@
|
|
20
20
|
min-width: fit-content;
|
21
21
|
/* height: 100%; can not have height 100% because of align-items: stretch; in the parent */
|
22
22
|
display: flex;
|
23
|
-
margin: 0 0.3rem;
|
24
23
|
}
|
25
24
|
|
26
25
|
.wrapper {
|
@@ -36,6 +35,7 @@
|
|
36
35
|
width: 95px;
|
37
36
|
height: 100%;
|
38
37
|
align-self: end;
|
38
|
+
margin-left: 0.6rem;
|
39
39
|
}
|
40
40
|
span {
|
41
41
|
font-size: 1rem;
|
@@ -30,6 +30,7 @@
|
|
30
30
|
}
|
31
31
|
}
|
32
32
|
|
33
|
+
return;
|
33
34
|
|
34
35
|
// if (needleEngineHasLoaded()) {
|
35
36
|
// if (debug) console.log("Skip asap, needle engine has already loaded.");
|
@@ -244,7 +244,7 @@
|
|
244
244
|
lineHeight: 1,
|
245
245
|
backgroundColor: 0xffffff,
|
246
246
|
backgroundOpacity: .55,
|
247
|
-
borderRadius: .
|
247
|
+
borderRadius: 1.0,
|
248
248
|
whiteSpace: 'pre-wrap',
|
249
249
|
flexDirection: 'row',
|
250
250
|
alignItems: 'center',
|
@@ -1,5 +1,5 @@
|
|
1
1
|
import type { Context } from "../../engine_context.js";
|
2
|
-
import { hasCommercialLicense,
|
2
|
+
import { hasCommercialLicense, onLicenseCheckResultChanged } from "../../engine_license.js";
|
3
3
|
import { isLocalNetwork } from "../../engine_networking_utils.js";
|
4
4
|
import { getParam, isMobileDevice } from "../../engine_utils.js";
|
5
5
|
import { onXRSessionStart, XRSessionEventArgs } from "../../xr/events.js";
|
@@ -314,10 +314,10 @@
|
|
314
314
|
justify-content: center;
|
315
315
|
align-items: stretch;
|
316
316
|
gap: 0px;
|
317
|
-
padding: 0
|
317
|
+
padding: 0 0rem;
|
318
318
|
}
|
319
319
|
|
320
|
-
.wrapper > *, .options > button, ::slotted(*) {
|
320
|
+
.wrapper > *, .options > button, .options > select, ::slotted(*) {
|
321
321
|
position: relative;
|
322
322
|
border: none;
|
323
323
|
border-radius: 0;
|
@@ -326,6 +326,7 @@
|
|
326
326
|
justify-content: center;
|
327
327
|
align-items: center;
|
328
328
|
max-height: 2.3rem;
|
329
|
+
max-width: 100%;
|
329
330
|
|
330
331
|
/** basic font settings for all entries **/
|
331
332
|
font-size: 1rem;
|
@@ -342,7 +343,7 @@
|
|
342
343
|
outline: rgb(0 0 0 / 5%) 1px solid;
|
343
344
|
border: 1px solid rgba(255, 255, 255, .1);
|
344
345
|
box-shadow: 0px 7px 0.5rem 0px rgb(0 0 0 / 6%), inset 0px 0px 1.3rem rgba(0,0,0,.05);
|
345
|
-
border-radius: 1.
|
346
|
+
border-radius: 1.5rem;
|
346
347
|
/**
|
347
348
|
* to make nested background filter work
|
348
349
|
* https://stackoverflow.com/questions/60997948/backdrop-filter-not-working-for-nested-elements-in-chrome
|
@@ -355,7 +356,7 @@
|
|
355
356
|
top: 0;
|
356
357
|
left: 0;
|
357
358
|
z-index: -1;
|
358
|
-
border-radius: 1.
|
359
|
+
border-radius: 1.5rem;
|
359
360
|
-webkit-backdrop-filter: blur(8px);
|
360
361
|
backdrop-filter: blur(8px);
|
361
362
|
}
|
@@ -369,6 +370,7 @@
|
|
369
370
|
.options {
|
370
371
|
display: flex;
|
371
372
|
flex-direction: row;
|
373
|
+
align-items: center;
|
372
374
|
}
|
373
375
|
|
374
376
|
.options > *, ::slotted(*) {
|
@@ -376,12 +378,12 @@
|
|
376
378
|
padding: .4rem .5rem;
|
377
379
|
}
|
378
380
|
|
379
|
-
:host .options >
|
381
|
+
:host .options > *, ::slotted(*) {
|
380
382
|
background: transparent;
|
381
383
|
border: none;
|
382
384
|
white-space: nowrap;
|
383
385
|
transition: all 0.1s linear .02s;
|
384
|
-
border-radius:
|
386
|
+
border-radius: 1.5rem;
|
385
387
|
user-select: none;
|
386
388
|
}
|
387
389
|
:host .options > *:hover, ::slotted(*:hover) {
|
@@ -441,6 +443,7 @@
|
|
441
443
|
:host .has-options .logo {
|
442
444
|
border-left: 1px solid rgba(40,40,40,.4);
|
443
445
|
margin-left: 0.3rem;
|
446
|
+
margin-right: 0.5rem;
|
444
447
|
}
|
445
448
|
|
446
449
|
.logo > span {
|
@@ -501,6 +504,7 @@
|
|
501
504
|
}
|
502
505
|
.open .options, .open .foldout {
|
503
506
|
display: flex;
|
507
|
+
justify-content: center;
|
504
508
|
}
|
505
509
|
.compact .wrapper {
|
506
510
|
padding: 0;
|
@@ -564,7 +568,7 @@
|
|
564
568
|
background: rgb(150,150,150);
|
565
569
|
}
|
566
570
|
|
567
|
-
.compact .options > * {
|
571
|
+
.compact .options > *, .compact .options > ::slotted(*) {
|
568
572
|
font-size: 1.2rem;
|
569
573
|
padding: .6rem .5rem;
|
570
574
|
width: 100%;
|
@@ -575,17 +579,22 @@
|
|
575
579
|
margin-left: 1rem;
|
576
580
|
margin-bottom: .02rem;
|
577
581
|
}
|
578
|
-
.compact .options
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
+
.compact .options {
|
583
|
+
/** e.g. if we have a very wide menu item like a select with long option names we don't want to overflow **/
|
584
|
+
max-width: 100%;
|
585
|
+
|
586
|
+
& > button, & > select {
|
587
|
+
display: flex;
|
588
|
+
flex-basis: 100%;
|
589
|
+
min-height: 3rem;
|
590
|
+
}
|
591
|
+
& > button.row2 {
|
592
|
+
//border: 1px solid red !important;
|
593
|
+
display: flex;
|
594
|
+
flex: 1;
|
595
|
+
flex-basis: 30%;
|
596
|
+
}
|
582
597
|
}
|
583
|
-
.compact .options > button.row2 {
|
584
|
-
//border: 1px solid red !important;
|
585
|
-
display: flex;
|
586
|
-
flex: 1;
|
587
|
-
flex-basis: 30%;
|
588
|
-
}
|
589
598
|
|
590
599
|
/** If there's really not enough space then just hide all options **/
|
591
600
|
@media (max-width: 100px) or (max-height: 100px){
|
@@ -664,6 +673,7 @@
|
|
664
673
|
globalThis.open("https://needle.tools", "_blank");
|
665
674
|
});
|
666
675
|
|
676
|
+
try {
|
667
677
|
// if the user has a license then we CAN hide the needle logo
|
668
678
|
onLicenseCheckResultChanged(res => {
|
669
679
|
if (res == true && hasCommercialLicense() && !debugNonCommercial) {
|
@@ -672,6 +682,9 @@
|
|
672
682
|
this.#onSetLogoVisible(visible);
|
673
683
|
}
|
674
684
|
});
|
685
|
+
} catch (e) {
|
686
|
+
console.error("[Needle Menu] License check failed.", e);
|
687
|
+
}
|
675
688
|
|
676
689
|
this.compactMenuButton.addEventListener("click", evt => {
|
677
690
|
evt.preventDefault();
|
@@ -1,10 +1,12 @@
|
|
1
|
+
import type { Context } from '../engine/engine_context.js';
|
1
2
|
import { serializable } from '../engine/engine_serialization.js';
|
2
3
|
import { isMobileDevice } from '../engine/engine_utils.js';
|
3
4
|
import { Behaviour } from './Component.js';
|
4
5
|
|
5
6
|
/**
|
6
|
-
* Exposes options to
|
7
|
-
* From code you can access the menu via
|
7
|
+
* Exposes options to customize the built-in Needle Menu.
|
8
|
+
* From code, you can access the menu via {@link Context.menu}.
|
9
|
+
* @category User Interface
|
8
10
|
**/
|
9
11
|
export class NeedleMenu extends Behaviour {
|
10
12
|
|
@@ -161,6 +161,7 @@
|
|
161
161
|
private _hasSelectEvent = false;
|
162
162
|
get hasSelectEvent() { return this._hasSelectEvent; }
|
163
163
|
private _isMxInk = false;
|
164
|
+
private _isMxInkFallback = false;
|
164
165
|
private _isMetaQuestTouchController = false;
|
165
166
|
|
166
167
|
/** Perform a hit test against the XR planes or meshes. shorthand for `xr.getHitTest(controller)`
|
@@ -439,7 +440,7 @@
|
|
439
440
|
|
440
441
|
// HACK: offset for MX Ink on QuestOS v69 and less. This will hopefully not be required with OS v70+ anymore,
|
441
442
|
// when the pen should have its own proper profile and correct ray space.
|
442
|
-
if (this._isMxInk) {
|
443
|
+
if (this._isMxInk && !this._isMxInkFallback) {
|
443
444
|
const offset = getTempVector(0.013, 0.000, -0.028).applyQuaternion(rayQuaternionRaw);
|
444
445
|
rayPositionRaw.add(offset);
|
445
446
|
this._rayPosition.add(offset);
|
@@ -761,8 +762,11 @@
|
|
761
762
|
this.getMotionController = fetchProfileCall.then(res => {
|
762
763
|
|
763
764
|
if (!this.connected) return null;
|
764
|
-
if (this._isMxInk)
|
765
|
+
if (this._isMxInk && !res.assetPath) {
|
766
|
+
if (debug) console.log("Falling back to custom MX Ink model", res.profile, res.assetPath);
|
765
767
|
res.assetPath = "https://cdn.needle.tools/static/models/controllers/logitech_vr_stylus_v1.3.1_grip_questos68.glb";
|
768
|
+
this._isMxInkFallback = true;
|
769
|
+
}
|
766
770
|
|
767
771
|
this._motioncontroller = new MotionController(
|
768
772
|
this.inputSource,
|
@@ -8,6 +8,7 @@
|
|
8
8
|
|
9
9
|
/**
|
10
10
|
* The networking component is used to provide a websocket url to the networking system. It implements the {@link INetworkingWebsocketUrlProvider} interface.
|
11
|
+
* @category Networking
|
11
12
|
*/
|
12
13
|
export class Networking extends Behaviour implements INetworkingWebsocketUrlProvider {
|
13
14
|
|
@@ -17,6 +17,7 @@
|
|
17
17
|
|
18
18
|
/**
|
19
19
|
* OpenURL behaviour opens a URL in a new tab or window.
|
20
|
+
* @category Interactivity
|
20
21
|
*/
|
21
22
|
export class OpenURL extends Behaviour implements IPointerClickHandler {
|
22
23
|
|
@@ -46,6 +46,7 @@
|
|
46
46
|
/** The OrbitControls component is used to control a camera using the [OrbitControls from three.js](https://threejs.org/docs/#examples/en/controls/OrbitControls) library.
|
47
47
|
* The three OrbitControls object can be accessed via the `controls` property.
|
48
48
|
* The object being controlled by the OrbitControls (usually the camera) can be accessed via the `controllerObject` property.
|
49
|
+
* @category Camera Controls
|
49
50
|
*/
|
50
51
|
export class OrbitControls extends Behaviour implements ICameraController {
|
51
52
|
|
@@ -235,6 +236,12 @@
|
|
235
236
|
private _cameraLerp01: number = 0;
|
236
237
|
private _cameraLerpDuration: number = 0;
|
237
238
|
|
239
|
+
private _fovLerpActive: boolean = false;
|
240
|
+
private _fovLerpStartValue: number = 0;
|
241
|
+
private _fovLerpEndValue: number = 0;
|
242
|
+
private _fovLerp01: number = 0;
|
243
|
+
private _fovLerpDuration: number = 0;
|
244
|
+
|
238
245
|
private _inputs: number = 0;
|
239
246
|
private _enableTime: number = 0; // use to disable double click when double clicking on UI
|
240
247
|
private _startedListeningToKeyEvents: boolean = false;
|
@@ -460,8 +467,7 @@
|
|
460
467
|
this.setTargetFromRaycast();
|
461
468
|
}
|
462
469
|
|
463
|
-
if (this._lookTargetLerpActive || this._cameraLerpActive) {
|
464
|
-
|
470
|
+
if (this._lookTargetLerpActive || this._cameraLerpActive || this._fovLerpActive) {
|
465
471
|
// lerp the camera
|
466
472
|
if (this._cameraLerpActive && this._cameraObject) {
|
467
473
|
this._cameraLerp01 += this.context.time.deltaTime / this._cameraLerpDuration;
|
@@ -488,6 +494,20 @@
|
|
488
494
|
this._controls.target.lerpVectors(this._lookTargetStartPosition, this._lookTargetEndPosition, t);
|
489
495
|
}
|
490
496
|
}
|
497
|
+
|
498
|
+
// lerp the fov
|
499
|
+
if (this._fovLerpActive && this._cameraObject) {
|
500
|
+
const cam = this._cameraObject as PerspectiveCamera;
|
501
|
+
this._fovLerp01 += this.context.time.deltaTime / this._fovLerpDuration;
|
502
|
+
if (this._fovLerp01 >= 1) {
|
503
|
+
cam.fov = this._fovLerpEndValue;
|
504
|
+
this._fovLerpActive = false;
|
505
|
+
} else {
|
506
|
+
const t = Mathf.easeInOutCubic(this._fovLerp01);
|
507
|
+
cam.fov = Mathf.lerp(this._fovLerpStartValue, this._fovLerpEndValue, t);
|
508
|
+
}
|
509
|
+
cam.updateProjectionMatrix();
|
510
|
+
}
|
491
511
|
}
|
492
512
|
|
493
513
|
|
@@ -511,7 +531,7 @@
|
|
511
531
|
this._controls.maxPolarAngle = this.maxPolarAngle;
|
512
532
|
// set the min/max zoom if it's not a free cam
|
513
533
|
if (!freeCam) {
|
514
|
-
if (this._camera?.
|
534
|
+
if (this._camera?.threeCamera?.type === "PerspectiveCamera") {
|
515
535
|
this._controls.minDistance = this.minZoom;
|
516
536
|
this._controls.maxDistance = this.maxZoom;
|
517
537
|
this._controls.minZoom = 0;
|
@@ -602,7 +622,9 @@
|
|
602
622
|
this._cameraEndPosition.copy(position);
|
603
623
|
if (immediateOrDuration === true) {
|
604
624
|
this._cameraLerpActive = false;
|
605
|
-
|
625
|
+
if (this._cameraObject) {
|
626
|
+
this._cameraObject.position.copy(this._cameraEndPosition);
|
627
|
+
}
|
606
628
|
}
|
607
629
|
else if (this._cameraObject) {
|
608
630
|
this._cameraLerpActive = true;
|
@@ -621,6 +643,26 @@
|
|
621
643
|
this._cameraLerpActive = false;
|
622
644
|
}
|
623
645
|
|
646
|
+
public setFieldOfView(fov: number | undefined, immediateOrDuration: boolean | number = false) {
|
647
|
+
if (!this._controls) return;
|
648
|
+
if (typeof fov !== "number") return;
|
649
|
+
const cam = this._camera?.threeCamera as PerspectiveCamera;
|
650
|
+
if (!cam) return;
|
651
|
+
if (immediateOrDuration === true) {
|
652
|
+
cam.fov = fov;
|
653
|
+
}
|
654
|
+
else {
|
655
|
+
this._fovLerpActive = true;
|
656
|
+
this._fovLerp01 = 0;
|
657
|
+
this._fovLerpStartValue = cam.fov;
|
658
|
+
this._fovLerpEndValue = fov;
|
659
|
+
if (typeof immediateOrDuration === "number") {
|
660
|
+
this._fovLerpDuration = immediateOrDuration;
|
661
|
+
}
|
662
|
+
else this._fovLerpDuration = this.targetLerpDuration;
|
663
|
+
}
|
664
|
+
}
|
665
|
+
|
624
666
|
/** Moves the camera look-at target to a position smoothly.
|
625
667
|
* @param position The position in world space to move the camera target to. If null the camera will stop lerping to the target.
|
626
668
|
* @param immediateOrDuration If true the camera target will move immediately to the new position, otherwise it will lerp. If a number is passed in it will be used as the duration of the lerp.
|
@@ -760,9 +802,6 @@
|
|
760
802
|
return;
|
761
803
|
}
|
762
804
|
|
763
|
-
if (!options) options = {}
|
764
|
-
const { immediate = false, centerCamera = "y", cameraNearFar = "auto", fitOffset = 1.1 } = options;
|
765
|
-
|
766
805
|
const camera = this._cameraObject as PerspectiveCamera;
|
767
806
|
const controls = this._controls as ThreeOrbitControls | null;
|
768
807
|
|
@@ -771,13 +810,17 @@
|
|
771
810
|
return;
|
772
811
|
}
|
773
812
|
|
813
|
+
if (!options) options = {}
|
814
|
+
const { immediate = false, centerCamera = "y", cameraNearFar = "auto", fitOffset = 1.1, fov = camera?.fov } = options;
|
815
|
+
|
774
816
|
const size = new Vector3();
|
775
817
|
const center = new Vector3();
|
776
818
|
// TODO would be much better to calculate the bounds in camera space instead of world space -
|
777
819
|
// we would get proper view-dependant fit.
|
778
820
|
// Right now it's independent from where the camera is actually looking from,
|
779
821
|
// and thus we're just getting some maximum that will work for sure.
|
780
|
-
const box = getBoundingBox(objects, undefined);
|
822
|
+
const box = getBoundingBox(objects, undefined, this._camera?.threeCamera?.layers);
|
823
|
+
const boxCopy = box.clone();
|
781
824
|
|
782
825
|
camera.updateMatrixWorld();
|
783
826
|
camera.updateProjectionMatrix();
|
@@ -800,7 +843,7 @@
|
|
800
843
|
return;
|
801
844
|
}
|
802
845
|
|
803
|
-
const verticalFov = camera.fov;
|
846
|
+
const verticalFov = options.fov || camera.fov;
|
804
847
|
const horizontalFov = 2 * Math.atan(Math.tan(verticalFov * Math.PI / 360 / 2) * camera.aspect) / Math.PI * 360;
|
805
848
|
const fitHeightDistance = size.y / (2 * Math.atan(Math.PI * verticalFov / 360));
|
806
849
|
const fitWidthDistance = size.x / (2 * Math.atan(Math.PI * horizontalFov / 360));
|
@@ -808,17 +851,18 @@
|
|
808
851
|
const distance = fitOffset * Math.max(fitHeightDistance, fitWidthDistance) + size.z / 2;
|
809
852
|
|
810
853
|
if (debugCameraFit) {
|
811
|
-
console.log("Fit camera to objects", fitHeightDistance, fitWidthDistance,
|
854
|
+
console.log("Fit camera to objects", {fitHeightDistance, fitWidthDistance, distance, verticalFov, horizontalFov});
|
812
855
|
}
|
813
856
|
|
814
|
-
|
815
|
-
|
857
|
+
this.maxZoom = distance * 10;
|
858
|
+
this.minZoom = distance * 0.01;
|
816
859
|
|
817
860
|
const verticalOffset = 0.05;
|
818
861
|
|
819
862
|
const lookAt = center.clone();
|
820
863
|
lookAt.y -= size.y * verticalOffset;
|
821
864
|
this.setLookTargetPosition(lookAt, immediate);
|
865
|
+
this.setFieldOfView(options.fov, immediate);
|
822
866
|
this.autoTarget = false;
|
823
867
|
|
824
868
|
if (cameraNearFar == undefined || cameraNearFar == "auto") {
|
@@ -829,8 +873,18 @@
|
|
829
873
|
// TODO: this doesnt take the Camera component nearClipPlane into account
|
830
874
|
camera.near = (distance / 100);
|
831
875
|
camera.far = boundsMax + distance * 10;
|
876
|
+
|
877
|
+
// adjust maxZoom so that the ground projection radius is always inside
|
878
|
+
if (groundprojection) {
|
879
|
+
this.maxZoom = Math.max(Math.min(this.maxZoom, groundProjectionRadius * 0.5), distance);
|
880
|
+
}
|
832
881
|
}
|
833
882
|
|
883
|
+
// ensure we're not clipping out of the current zoom level just because we're fitting
|
884
|
+
const currentZoom = controls.getDistance();
|
885
|
+
if (currentZoom < this.minZoom) this.minZoom = currentZoom * 0.9;
|
886
|
+
if (currentZoom > this.maxZoom) this.maxZoom = currentZoom * 1.1;
|
887
|
+
|
834
888
|
camera.updateMatrixWorld();
|
835
889
|
camera.updateProjectionMatrix();
|
836
890
|
|
@@ -842,30 +896,35 @@
|
|
842
896
|
direction.normalize();
|
843
897
|
direction.multiplyScalar(distance);
|
844
898
|
if (centerCamera === "y")
|
845
|
-
direction.y += -verticalOffset * 4 *
|
899
|
+
direction.y += -verticalOffset * 4 * distance;
|
846
900
|
|
901
|
+
let cameraLocalPosition = center.clone().sub(direction);
|
847
902
|
if (camera.parent) {
|
848
|
-
|
849
|
-
this.setCameraTargetPosition(cameraLocalPosition, immediate);
|
903
|
+
cameraLocalPosition = camera.parent.worldToLocal(cameraLocalPosition);
|
850
904
|
}
|
851
|
-
|
852
|
-
|
853
|
-
// setWorldPosition(camera, controls.target.clone().sub(direction));
|
854
|
-
|
905
|
+
this.setCameraTargetPosition(cameraLocalPosition, immediate);
|
906
|
+
|
855
907
|
if (debugCameraFit) {
|
856
908
|
const helper = new Box3Helper(box);
|
857
909
|
this.context.scene.add(helper);
|
858
910
|
setWorldRotation(helper, getWorldRotation(camera));
|
859
911
|
setTimeout(() => {
|
860
912
|
this.context.scene.remove(helper);
|
861
|
-
},
|
913
|
+
}, 10_000);
|
914
|
+
Gizmos.DrawWireBox3(boxCopy, 0x00ff00, 10);
|
862
915
|
|
863
916
|
if (!this._haveAttachedKeyboardEvents) {
|
864
917
|
this._haveAttachedKeyboardEvents = true;
|
865
918
|
document.body.addEventListener("keydown", (e) => {
|
866
919
|
if (e.code === "KeyF") {
|
867
|
-
|
920
|
+
// random fov for easier debugging of fov-based fitting
|
921
|
+
let fov: number | undefined = undefined;
|
922
|
+
if (this._cameraObject instanceof PerspectiveCamera) fov = (Math.random() * Math.random()) * 170 + 10;
|
923
|
+
this.fitCamera({ objects, fitOffset, immediate: false, fov });
|
868
924
|
}
|
925
|
+
if (e.code === "KeyV") {
|
926
|
+
if (this._cameraObject instanceof PerspectiveCamera) this._cameraObject.fov = 60;
|
927
|
+
}
|
869
928
|
});
|
870
929
|
}
|
871
930
|
}
|
@@ -874,10 +933,6 @@
|
|
874
933
|
}
|
875
934
|
|
876
935
|
private _haveAttachedKeyboardEvents: boolean = false;
|
877
|
-
|
878
|
-
// private onPositionDrag(){
|
879
|
-
|
880
|
-
// }
|
881
936
|
}
|
882
937
|
|
883
938
|
|
@@ -900,4 +955,5 @@
|
|
900
955
|
/** If set to "y" the camera will be centered in the y axis */
|
901
956
|
centerCamera?: "none" | "y",
|
902
957
|
cameraNearFar?: "keep" | "auto",
|
958
|
+
fov?: number,
|
903
959
|
}
|
@@ -641,9 +641,13 @@
|
|
641
641
|
}
|
642
642
|
|
643
643
|
/**
|
644
|
-
* The ParticleSystem component efficiently handles the rendering of particles.
|
644
|
+
* The ParticleSystem component efficiently handles the motion and rendering of many individual particles.
|
645
645
|
*
|
646
646
|
* You can add custom behaviours to the particle system to fully customize the behaviour of the particles. See {@link ParticleSystemBaseBehaviour} and {@link ParticleSystem.addBehaviour} for more information.
|
647
|
+
*
|
648
|
+
* Needle Engine uses [three.quarks](https://github.com/Alchemist0823/three.quarks) under the hood to handle particles.
|
649
|
+
*
|
650
|
+
* @category Rendering
|
647
651
|
*/
|
648
652
|
export class ParticleSystem extends Behaviour implements IParticleSystem {
|
649
653
|
|
@@ -893,7 +897,7 @@
|
|
893
897
|
this._particleSystem.behaviors.length = 0;
|
894
898
|
return true;
|
895
899
|
}
|
896
|
-
/** Get the
|
900
|
+
/** Get the underlying three.quarks particle system behaviours. This can be used to fully customize the behaviour of the particles. */
|
897
901
|
get behaviours(): Behavior[] | null {
|
898
902
|
if (!this._particleSystem) return null;
|
899
903
|
return this._particleSystem.behaviors;
|
@@ -5,6 +5,9 @@
|
|
5
5
|
import { VolumeParameter } from "../VolumeParameter.js";
|
6
6
|
import { registerCustomEffectType } from "../VolumeProfile.js";
|
7
7
|
|
8
|
+
/**
|
9
|
+
* @category Effects
|
10
|
+
*/
|
8
11
|
export class PixelationEffect extends PostProcessingEffect {
|
9
12
|
get typeName(): string {
|
10
13
|
return "PixelationEffect";
|
@@ -56,6 +56,7 @@
|
|
56
56
|
* The PlayableDirector component is the main component to control timelines in needle engine.
|
57
57
|
* It is used to play, pause, stop and evaluate timelines.
|
58
58
|
* Assign a TimelineAsset to the `playableAsset` property to start playing a timeline.
|
59
|
+
* @category Animation and Sequencing
|
59
60
|
*/
|
60
61
|
export class PlayableDirector extends Behaviour {
|
61
62
|
|
@@ -278,6 +279,13 @@
|
|
278
279
|
}
|
279
280
|
|
280
281
|
/**
|
282
|
+
* @returns all animation tracks of the timeline
|
283
|
+
*/
|
284
|
+
get animationTracks() {
|
285
|
+
return this._animationTracks;
|
286
|
+
}
|
287
|
+
|
288
|
+
/**
|
281
289
|
* @returns all audio tracks of the timeline
|
282
290
|
*/
|
283
291
|
get audioTracks(): Tracks.AudioTrackHandler[] {
|
@@ -9,6 +9,7 @@
|
|
9
9
|
/**
|
10
10
|
* PlayerColor assigns a unique color for each user in the room to the object it is attached to.
|
11
11
|
* The color is generated based on the user's ID.
|
12
|
+
* @category Networking
|
12
13
|
*/
|
13
14
|
export class PlayerColor extends Behaviour {
|
14
15
|
|
@@ -47,6 +47,8 @@
|
|
47
47
|
* }
|
48
48
|
* registerCustomEffectType("Antialiasing", Antialiasing)
|
49
49
|
* ```
|
50
|
+
*
|
51
|
+
* @category Effects
|
50
52
|
*/
|
51
53
|
export abstract class PostProcessingEffect extends Component implements IEffectProvider, IEditorModification {
|
52
54
|
|
@@ -13,6 +13,9 @@
|
|
13
13
|
const $reflectionProbeKey = Symbol("reflectionProbeKey");
|
14
14
|
const $originalMaterial = Symbol("original material");
|
15
15
|
|
16
|
+
/**
|
17
|
+
* @category Rendering
|
18
|
+
*/
|
16
19
|
export class ReflectionProbe extends Behaviour {
|
17
20
|
|
18
21
|
private static _probes: Map<Context, ReflectionProbe[]> = new Map();
|
@@ -31,10 +34,13 @@
|
|
31
34
|
return probe;
|
32
35
|
}
|
33
36
|
}
|
34
|
-
|
37
|
+
/*
|
38
|
+
// TODO not supported right now, as we'd have to pass the ReflectionProbe scale through as well.
|
39
|
+
else if (probe.isInBox(object)) {
|
35
40
|
if (debug) console.log("Found reflection probe", object.name, probe.name);
|
36
41
|
return probe;
|
37
42
|
}
|
43
|
+
*/
|
38
44
|
}
|
39
45
|
}
|
40
46
|
}
|
@@ -69,8 +75,8 @@
|
|
69
75
|
|
70
76
|
private _boxHelper?: BoxHelperComponent;
|
71
77
|
|
72
|
-
private isInBox(obj: Object3D
|
73
|
-
return this._boxHelper?.isInBox(obj
|
78
|
+
private isInBox(obj: Object3D) {
|
79
|
+
return this._boxHelper?.isInBox(obj);
|
74
80
|
}
|
75
81
|
|
76
82
|
constructor() {
|
@@ -82,7 +88,7 @@
|
|
82
88
|
}
|
83
89
|
|
84
90
|
awake() {
|
85
|
-
this._boxHelper = this.gameObject.
|
91
|
+
this._boxHelper = this.gameObject.addComponent(BoxHelperComponent) as BoxHelperComponent;
|
86
92
|
this._boxHelper.updateBox(true);
|
87
93
|
if (debug)
|
88
94
|
this._boxHelper.showHelper(0x555500, true);
|
@@ -1,6 +1,6 @@
|
|
1
1
|
/* eslint-disable */
|
2
2
|
import { TypeStore } from "./../engine_typestore.js"
|
3
|
-
|
3
|
+
|
4
4
|
// Import types
|
5
5
|
import { __Ignore } from "../../engine-components/codegen/components.js";
|
6
6
|
import { ActionBuilder } from "../../engine-components/export/usdz/extensions/behavior/BehavioursBuilder.js";
|
@@ -220,7 +220,7 @@
|
|
220
220
|
import { XRFlag } from "../../engine-components/webxr/XRFlag.js";
|
221
221
|
import { XRRig } from "../../engine-components/webxr/WebXRRig.js";
|
222
222
|
import { XRState } from "../../engine-components/webxr/XRFlag.js";
|
223
|
-
|
223
|
+
|
224
224
|
// Register types
|
225
225
|
TypeStore.add("__Ignore", __Ignore);
|
226
226
|
TypeStore.add("ActionBuilder", ActionBuilder);
|
@@ -197,6 +197,9 @@
|
|
197
197
|
|
198
198
|
}
|
199
199
|
|
200
|
+
/**
|
201
|
+
* @category Rendering
|
202
|
+
*/
|
200
203
|
export class Renderer extends Behaviour implements IRenderer {
|
201
204
|
|
202
205
|
/** Enable or disable instancing for an object. This will create a Renderer component if it does not exist yet.
|
@@ -314,7 +314,7 @@
|
|
314
314
|
|
315
315
|
// console.log(geometry.name, geometry.uuid);
|
316
316
|
|
317
|
-
|
317
|
+
if (!this.validateGeometry(geometry)) return false;
|
318
318
|
|
319
319
|
// const validationMethod = this.inst["_validateGeometry"];
|
320
320
|
// if (!validationMethod) throw new Error("InstancedMesh does not have a _validateGeometry method");
|
@@ -411,12 +411,6 @@
|
|
411
411
|
add(handle: InstanceHandle) {
|
412
412
|
const geo = handle.object.geometry as BufferGeometry;
|
413
413
|
|
414
|
-
if (!this.validateGeometry(geo)) {
|
415
|
-
if (debugInstancing) console.error("Cannot add instance, invalid geometry", this.name, geo);
|
416
|
-
else console.warn("Cannot add instance, invalid geometry: " + handle.name);
|
417
|
-
return false;
|
418
|
-
}
|
419
|
-
|
420
414
|
if (this.mustGrow(geo)) {
|
421
415
|
if (this.allowResize) {
|
422
416
|
this.grow(geo);
|
@@ -501,7 +495,8 @@
|
|
501
495
|
continue;
|
502
496
|
}
|
503
497
|
if (!geometry.hasAttribute(attributeName)) {
|
504
|
-
|
498
|
+
if (isDevEnvironment())
|
499
|
+
console.warn(`BatchedMesh: Added geometry missing "${attributeName}". All geometries must have consistent attributes.`);
|
505
500
|
// geometry.setAttribute(attributeName, batchGeometry.getAttribute(attributeName).clone());
|
506
501
|
return false;
|
507
502
|
// throw new Error(`BatchedMesh: Added geometry missing "${attributeName}". All geometries must have consistent attributes.`);
|
@@ -517,6 +512,7 @@
|
|
517
512
|
}
|
518
513
|
|
519
514
|
private markNeedsUpdate() {
|
515
|
+
if (debugInstancing) console.warn("Marking instanced mesh dirty", this.name);
|
520
516
|
this._needUpdateBounds = true;
|
521
517
|
// this.inst.instanceMatrix.needsUpdate = true;
|
522
518
|
}
|
@@ -133,7 +133,8 @@
|
|
133
133
|
}
|
134
134
|
|
135
135
|
/**
|
136
|
-
* A Rigidbody is used together with a Collider to create physical interactions between objects in the scene.
|
136
|
+
* A Rigidbody is used together with a Collider to create physical interactions between objects in the scene.
|
137
|
+
* @category Physics
|
137
138
|
*/
|
138
139
|
export class Rigidbody extends Behaviour implements IRigidbody {
|
139
140
|
|
@@ -104,6 +104,7 @@
|
|
104
104
|
* });
|
105
105
|
* ```
|
106
106
|
*
|
107
|
+
* @category Asset Management
|
107
108
|
*/
|
108
109
|
export class SceneSwitcher extends Behaviour {
|
109
110
|
|
@@ -70,6 +70,7 @@
|
|
70
70
|
* By default the component will start sharing the screen when the user clicks on the object this component is attached to. You can set {@link device} This behaviour can be disabled by setting `allowStartOnClick` to false.
|
71
71
|
* It is also possible to start the stream manually from your code by calling the {@link share} method.
|
72
72
|
*
|
73
|
+
* @category Networking
|
73
74
|
*/
|
74
75
|
export class ScreenCapture extends Behaviour implements IPointerClickHandler {
|
75
76
|
|
@@ -8,7 +8,7 @@
|
|
8
8
|
|
9
9
|
/** Screenspace Ambient Occlusion post-processing effect.
|
10
10
|
* We recommend using ScreenSpaceAmbientOcclusionN8 instead.
|
11
|
-
* @category
|
11
|
+
* @category Effects
|
12
12
|
*/
|
13
13
|
export class ScreenSpaceAmbientOcclusion extends PostProcessingEffect {
|
14
14
|
|
@@ -19,7 +19,7 @@
|
|
19
19
|
}
|
20
20
|
|
21
21
|
/** Screen Space Ambient Occlusion (SSAO) effect.
|
22
|
-
* @category
|
22
|
+
* @category Effects
|
23
23
|
* @link [N8AO documentation](https://github.com/N8python/n8ao)
|
24
24
|
*/
|
25
25
|
export class ScreenSpaceAmbientOcclusionN8 extends PostProcessingEffect {
|
@@ -20,6 +20,7 @@
|
|
20
20
|
/**
|
21
21
|
* ShadowCatcher can be added to an Object3D to make it render shadows (or light) in the scene. It can also be used to create a shadow mask, or to occlude light.
|
22
22
|
* If the GameObject is a Mesh, it will apply a shadow-catching material to it - otherwise it will create a quad with the shadow-catching material.
|
23
|
+
* @category Rendering
|
23
24
|
*/
|
24
25
|
export class ShadowCatcher extends Behaviour {
|
25
26
|
|
@@ -4,6 +4,9 @@
|
|
4
4
|
import { serializable } from "../../../engine/engine_serialization.js";
|
5
5
|
import { PostProcessingEffect } from "../PostProcessingEffect.js";
|
6
6
|
|
7
|
+
/**
|
8
|
+
* @category Effects
|
9
|
+
*/
|
7
10
|
export class SharpeningEffect extends PostProcessingEffect {
|
8
11
|
|
9
12
|
get typeName() {
|
@@ -9,6 +9,7 @@
|
|
9
9
|
/**
|
10
10
|
* SmoothFollow makes the {@link Object3D} (`GameObject`) smoothly follow another target {@link Object3D}.
|
11
11
|
* It can follow the target's position, rotation, or both.
|
12
|
+
* @category Interactivity
|
12
13
|
*/
|
13
14
|
export class SmoothFollow extends Behaviour {
|
14
15
|
|
@@ -16,6 +16,9 @@
|
|
16
16
|
return layer1.test(layer2);
|
17
17
|
}
|
18
18
|
|
19
|
+
/**
|
20
|
+
* @category Interactivity
|
21
|
+
*/
|
19
22
|
export class SpatialTriggerReceiver extends Behaviour {
|
20
23
|
|
21
24
|
// currently Layers in unity but maybe this should be a string or plane number? Or should it be a bitmask to allow receivers use multiple triggers?
|
@@ -78,6 +81,10 @@
|
|
78
81
|
}
|
79
82
|
}
|
80
83
|
|
84
|
+
/**
|
85
|
+
* A trigger that can be used to detect if an object is inside a box.
|
86
|
+
* @category Interactivity
|
87
|
+
*/
|
81
88
|
export class SpatialTrigger extends Behaviour {
|
82
89
|
|
83
90
|
static triggers: SpatialTrigger[] = [];
|
@@ -24,6 +24,9 @@
|
|
24
24
|
|
25
25
|
const debug = getParam("debugspectator");
|
26
26
|
|
27
|
+
/**
|
28
|
+
* @category Networking
|
29
|
+
*/
|
27
30
|
export class SpectatorCamera extends Behaviour {
|
28
31
|
|
29
32
|
cam: Camera | null = null;
|
@@ -441,7 +444,7 @@
|
|
441
444
|
constructor(context: Context, spectator: SpectatorCamera) {
|
442
445
|
this.context = context;
|
443
446
|
this.spectator = spectator;
|
444
|
-
console.log("Click other avatars or cameras to follow them. Press ESC to exit spectator mode.");
|
447
|
+
console.log("[Spectator Camera] Click other avatars or cameras to follow them. Press ESC to exit spectator mode.");
|
445
448
|
this.context.domElement.addEventListener("keydown", (evt) => {
|
446
449
|
if(!this.spectator.useKeys) return;
|
447
450
|
const key = evt.key;
|
@@ -83,6 +83,7 @@
|
|
83
83
|
|
84
84
|
/**
|
85
85
|
* A sprite is a mesh that represents a 2D image
|
86
|
+
* @category Rendering
|
86
87
|
*/
|
87
88
|
export class Sprite {
|
88
89
|
|
@@ -63,6 +63,7 @@
|
|
63
63
|
/**
|
64
64
|
* SyncedCamera is a component that syncs the camera position and rotation of all users in the room.
|
65
65
|
* A prefab can be set to represent the remote cameras visually in the scene.
|
66
|
+
* @category Networking
|
66
67
|
*/
|
67
68
|
export class SyncedCamera extends Behaviour {
|
68
69
|
|
@@ -32,6 +32,8 @@
|
|
32
32
|
* const myObject = new Object3D();
|
33
33
|
* myObject.addComponent(SyncedRoom, { joinRandomRoom: true, roomPrefix: "myApp_" });
|
34
34
|
* ```
|
35
|
+
*
|
36
|
+
* @category Networking
|
35
37
|
*/
|
36
38
|
export class SyncedRoom extends Behaviour {
|
37
39
|
|
@@ -176,7 +178,7 @@
|
|
176
178
|
|
177
179
|
if (this.requireRoomParameter && !hasRoomParameter) {
|
178
180
|
if (debug || isDevEnvironment())
|
179
|
-
console.warn("SyncedRoom
|
181
|
+
console.warn("[SyncedRoom] Missing required room parameter \"" + this.urlParameterName + "\" in url - will not connect.\nTo allow joining a room without a query parameter you can set \"requireRoomParameter\" to false.");
|
180
182
|
return false;
|
181
183
|
}
|
182
184
|
|
@@ -189,7 +191,7 @@
|
|
189
191
|
this.roomName = this._roomPrefix + this.roomName;
|
190
192
|
|
191
193
|
if (this.roomName.length <= 0) {
|
192
|
-
console.warn("SyncedRoom
|
194
|
+
console.warn("[SyncedRoom] Room name is not set so we can not join a networked room.\nPlease choose one of the following options to fix this:\nA) Set a room name in the SyncedRoom component\nB) Set a room name in the URL parameter \"?" + this.urlParameterName + "=my_room\"\nC) Set \"joinRandomRoom\" to true");
|
193
195
|
return false;
|
194
196
|
}
|
195
197
|
|
@@ -51,6 +51,7 @@
|
|
51
51
|
|
52
52
|
/**
|
53
53
|
* SyncedTransform is a behaviour that syncs the transform of a game object over the network.
|
54
|
+
* @category Networking
|
54
55
|
*/
|
55
56
|
export class SyncedTransform extends Behaviour {
|
56
57
|
|
@@ -39,6 +39,9 @@
|
|
39
39
|
BoldAndItalic = 3,
|
40
40
|
}
|
41
41
|
|
42
|
+
/**
|
43
|
+
* @category User Interface
|
44
|
+
*/
|
42
45
|
export class Text extends Graphic implements IHasAlphaFactor, ICanvasEventReceiver {
|
43
46
|
|
44
47
|
@serializable()
|
@@ -664,7 +664,8 @@
|
|
664
664
|
const options = {
|
665
665
|
allBehaviorTargets,
|
666
666
|
debug: false,
|
667
|
-
boneReparentings: allReparentingObjects
|
667
|
+
boneReparentings: allReparentingObjects,
|
668
|
+
quickLookCompatible: context.quickLookCompatible,
|
668
669
|
};
|
669
670
|
if (this.debug) logUsdHierarchy(context.document, "Hierarchy BEFORE pruning", options);
|
670
671
|
prune( context.document, options );
|
@@ -927,7 +928,8 @@
|
|
927
928
|
function prune ( object: USDObject, options : {
|
928
929
|
allBehaviorTargets: Set<string>,
|
929
930
|
debug: boolean,
|
930
|
-
boneReparentings: Set<string
|
931
|
+
boneReparentings: Set<string>,
|
932
|
+
quickLookCompatible: boolean,
|
931
933
|
} ) {
|
932
934
|
|
933
935
|
let allChildsWerePruned = true;
|
@@ -956,7 +958,7 @@
|
|
956
958
|
const isBehaviorSourceOrTarget = options.allBehaviorTargets.has(object.uuid);
|
957
959
|
|
958
960
|
// check if this object has any material or geometry
|
959
|
-
const isVisible = object.geometry || object.material || object.camera || object.skinnedMesh || false;
|
961
|
+
const isVisible = object.geometry || object.material || (object.camera && !options.quickLookCompatible) || object.skinnedMesh || false;
|
960
962
|
|
961
963
|
// check if this object is part of any reparenting
|
962
964
|
const isBoneReparenting = options.boneReparentings.has(object.uuid);
|
@@ -1450,7 +1452,7 @@
|
|
1450
1452
|
if (isSkinnedMesh)
|
1451
1453
|
_apiSchemas.push("SkelBindingAPI");
|
1452
1454
|
}
|
1453
|
-
else if ( camera )
|
1455
|
+
else if ( camera && !context.quickLookCompatible)
|
1454
1456
|
writer.beginBlock( `def Camera "${name}"`, "(", false );
|
1455
1457
|
else if ( model.type !== undefined)
|
1456
1458
|
writer.beginBlock( `def ${model.type} "${name}"` );
|
@@ -1514,7 +1516,7 @@
|
|
1514
1516
|
if (model.visibility !== undefined)
|
1515
1517
|
writer.appendLine(`token visibility = "${model.visibility}"`);
|
1516
1518
|
|
1517
|
-
if ( camera ) {
|
1519
|
+
if ( camera && !context.quickLookCompatible) {
|
1518
1520
|
|
1519
1521
|
if ( 'isOrthographicCamera' in camera && camera.isOrthographicCamera ) {
|
1520
1522
|
|
@@ -5,7 +5,9 @@
|
|
5
5
|
import { VolumeParameter } from "../VolumeParameter.js";
|
6
6
|
import { registerCustomEffectType } from "../VolumeProfile.js";
|
7
7
|
|
8
|
-
|
8
|
+
/**
|
9
|
+
* @category Effects
|
10
|
+
*/
|
9
11
|
export class TiltShiftEffect extends PostProcessingEffect {
|
10
12
|
get typeName(): string {
|
11
13
|
return "TiltShiftEffect";
|
@@ -136,15 +136,24 @@
|
|
136
136
|
|
137
137
|
// TODO: add support for clip clamp modes (loop, pingpong, clamp)
|
138
138
|
export class AnimationTrackHandler extends TrackHandler {
|
139
|
+
/** @internal */
|
139
140
|
models: Array<Models.ClipModel> = [];
|
141
|
+
/** @internal */
|
140
142
|
trackOffset?: Models.TrackOffset;
|
141
143
|
|
144
|
+
/** The object that is being animated. */
|
142
145
|
target?: Object3D;
|
143
146
|
/** The AnimationMixer, should be shared with the animator if an animator is bound */
|
144
147
|
mixer?: AnimationMixer;
|
145
148
|
clips: Array<AnimationClip> = [];
|
146
149
|
actions: Array<AnimationAction> = [];
|
147
150
|
|
151
|
+
/**
|
152
|
+
* You can use the weight to blend the timeline animation tracks with multiple animation tracks on the same object.
|
153
|
+
* @default 1
|
154
|
+
*/
|
155
|
+
weight: number = 1;
|
156
|
+
|
148
157
|
/** holds data/info about clips differences */
|
149
158
|
private _actionOffsets: Array<AnimationClipOffsetData> = [];
|
150
159
|
private _didBind: boolean = false;
|
@@ -351,7 +360,7 @@
|
|
351
360
|
|
352
361
|
if (isActive) {
|
353
362
|
// const clip = this.clips[i];
|
354
|
-
let weight =
|
363
|
+
let weight = this.weight;
|
355
364
|
weight *= this.evaluateWeight(time, i, this.models, isActive);
|
356
365
|
weight *= this.director.weight;
|
357
366
|
|
@@ -63,7 +63,9 @@
|
|
63
63
|
}
|
64
64
|
}
|
65
65
|
|
66
|
-
|
66
|
+
/**
|
67
|
+
* @category Effects
|
68
|
+
*/
|
67
69
|
export class ToneMappingEffect extends PostProcessingEffect {
|
68
70
|
|
69
71
|
get typeName() {
|
@@ -9,6 +9,7 @@
|
|
9
9
|
|
10
10
|
/**
|
11
11
|
* TransformGizmo is a component that displays a gizmo for transforming the object in the scene.
|
12
|
+
* @category Helpers
|
12
13
|
*/
|
13
14
|
export class TransformGizmo extends Behaviour {
|
14
15
|
|
@@ -9,6 +9,8 @@
|
|
9
9
|
import { WebXRButtonFactory } from "../../../engine/webcomponents/WebXRButtons.js";
|
10
10
|
import { InternalUSDZRegistry } from "../../../engine/xr/usdz.js"
|
11
11
|
import { Behaviour, GameObject } from "../../Component.js";
|
12
|
+
import { ContactShadows } from "../../ContactShadows.js";
|
13
|
+
import { GroundProjectedEnv } from "../../GroundProjection.js";
|
12
14
|
import { Renderer } from "../../Renderer.js"
|
13
15
|
import { SpriteRenderer } from "../../SpriteRenderer.js";
|
14
16
|
import { WebARSessionRoot } from "../../webxr/WebARSessionRoot.js";
|
@@ -61,6 +63,7 @@
|
|
61
63
|
* usdz.autoExportAudioSources = true;
|
62
64
|
* usdz.exportAsync();
|
63
65
|
* ```
|
66
|
+
* @category XR
|
64
67
|
*/
|
65
68
|
export class USDZExporter extends Behaviour {
|
66
69
|
|
@@ -387,12 +390,16 @@
|
|
387
390
|
exporter.debug = debug;
|
388
391
|
exporter.pruneUnusedNodes = !debugUsdzPruning;
|
389
392
|
exporter.keepObject = (object) => {
|
393
|
+
let keep = true;
|
390
394
|
// This explicitly removes geometry and material data from disabled renderers.
|
391
395
|
// Note that this is different to the object itself being active –
|
392
396
|
// here, we have an active object with a disabled renderer.
|
393
|
-
const renderer = GameObject.getComponent(object, Renderer)
|
394
|
-
if (renderer && !renderer.enabled)
|
395
|
-
|
397
|
+
const renderer = GameObject.getComponent(object, Renderer);
|
398
|
+
if (renderer && !renderer.enabled) keep = false;
|
399
|
+
if (keep && GameObject.getComponentInParent(object, ContactShadows)) keep = false;
|
400
|
+
if (keep && GameObject.getComponentInParent(object, GroundProjectedEnv)) keep = false;
|
401
|
+
if (debug && !keep) console.log("USDZExporter: Discarding object", object);
|
402
|
+
return keep;
|
396
403
|
}
|
397
404
|
|
398
405
|
// Collect invisible objects so that we can disable them if
|
@@ -54,6 +54,7 @@
|
|
54
54
|
* playOnAwake: true,
|
55
55
|
* });
|
56
56
|
* ```
|
57
|
+
* @category Multimedia
|
57
58
|
*/
|
58
59
|
export class VideoPlayer extends Behaviour {
|
59
60
|
|
@@ -5,7 +5,9 @@
|
|
5
5
|
import { VolumeParameter } from "../VolumeParameter.js";
|
6
6
|
import { registerCustomEffectType } from "../VolumeProfile.js";
|
7
7
|
|
8
|
-
|
8
|
+
/**
|
9
|
+
* @category Effects
|
10
|
+
*/
|
9
11
|
export class Vignette extends PostProcessingEffect {
|
10
12
|
get typeName(): string {
|
11
13
|
return "Vignette";
|
@@ -14,6 +14,7 @@
|
|
14
14
|
/**
|
15
15
|
* The Voice over IP component (VoIP) allows you to send and receive audio streams to other users in the same networked room.
|
16
16
|
* It requires a networking connection to be working (e.g. by having an active SyncedRoom component in the scene or by connecting to a room manually).
|
17
|
+
* @category Networking
|
17
18
|
*/
|
18
19
|
export class Voip extends Behaviour {
|
19
20
|
|
@@ -39,6 +39,9 @@
|
|
39
39
|
* pixelation.granularity.value = 10;
|
40
40
|
* volume.addEffect(pixelation);
|
41
41
|
* ```
|
42
|
+
*
|
43
|
+
* @category Rendering
|
44
|
+
* @category Effects
|
42
45
|
*/
|
43
46
|
export class Volume extends Behaviour implements IEditorModificationReceiver, IPostProcessingManager {
|
44
47
|
|
@@ -145,7 +145,7 @@
|
|
145
145
|
// discussion on exactly this:
|
146
146
|
// https://discourse.threejs.org/t/using-a-webgltexture-as-texture-for-three-js/46245/8
|
147
147
|
// HACK from https://stackoverflow.com/a/55084367 to inject a custom texture into three.js
|
148
|
-
const texProps = this.context.renderer.properties.get(this.threeTexture);
|
148
|
+
const texProps = this.context.renderer.properties.get(this.threeTexture) as { __webglTexture: WebGLTexture | null };
|
149
149
|
texProps.__webglTexture = glImage;
|
150
150
|
|
151
151
|
if (this.backgroundPlane) {
|
@@ -29,6 +29,8 @@
|
|
29
29
|
* console.log("Scene has been placed in AR");
|
30
30
|
* });
|
31
31
|
* ```
|
32
|
+
*
|
33
|
+
* @category XR
|
32
34
|
*/
|
33
35
|
export class WebARSessionRoot extends Behaviour {
|
34
36
|
|
@@ -26,6 +26,7 @@
|
|
26
26
|
/**
|
27
27
|
* WebXR component to enable VR, AR and Quicklook on iOS in your scene.
|
28
28
|
* It provides a simple wrapper around the {@link NeedleXRSession} API and adds some additional features like creating buttons or enabling default movement behaviour.
|
29
|
+
* @category XR
|
29
30
|
*/
|
30
31
|
export class WebXR extends Behaviour {
|
31
32
|
|
@@ -226,6 +226,9 @@
|
|
226
226
|
}
|
227
227
|
}
|
228
228
|
|
229
|
+
/**
|
230
|
+
* @category XR
|
231
|
+
*/
|
229
232
|
export class WebXRImageTracking extends Behaviour {
|
230
233
|
|
231
234
|
@serializable(WebXRImageTrackingModel)
|
@@ -49,6 +49,7 @@
|
|
49
49
|
|
50
50
|
/**
|
51
51
|
* Use this component to track planes and meshes in the real world when in immersive-ar (e.g. on Oculus Quest).
|
52
|
+
* @category XR
|
52
53
|
*/
|
53
54
|
export class WebXRPlaneTracking extends Behaviour {
|
54
55
|
|
@@ -13,6 +13,7 @@
|
|
13
13
|
/**
|
14
14
|
* A user in XR (VR or AR) is parented to an XR rig during the session.
|
15
15
|
* When moving through the scene the rig is moved instead of the user.
|
16
|
+
* @category XR
|
16
17
|
*/
|
17
18
|
export class XRRig extends Behaviour implements IXRRig {
|
18
19
|
|
@@ -7,7 +7,10 @@
|
|
7
7
|
import { Behaviour } from "../../Component.js";
|
8
8
|
|
9
9
|
|
10
|
-
/**
|
10
|
+
/**
|
11
|
+
* Add this script to an object and set `side` to make the object follow a specific controller.
|
12
|
+
* @category XR
|
13
|
+
* */
|
11
14
|
export class XRControllerFollow extends Behaviour {
|
12
15
|
|
13
16
|
// override active and enabled here so that we always receive xr update events
|
@@ -271,7 +271,7 @@
|
|
271
271
|
const expectedHandModelName = controller.side === "left" ? "left." : "right.";
|
272
272
|
const customHand = controller.side === "left" ? this.customLeftHand : this.customRightHand;
|
273
273
|
if (customHand) {
|
274
|
-
if (!customHand.
|
274
|
+
if (!customHand.url.includes(expectedHandModelName)) {
|
275
275
|
console.warn("XRControllerModel: custom hand model must be named " + expectedHandModelName);
|
276
276
|
showBalloonWarning("Custom Hand: unexpected name, please see the console for details");
|
277
277
|
}
|
@@ -290,7 +290,7 @@
|
|
290
290
|
// The hand mesh should not receive raycasts
|
291
291
|
object.traverse(child => {
|
292
292
|
child.layers.set(2);
|
293
|
-
if (NeedleXRSession.active?.isPassThrough)
|
293
|
+
if (NeedleXRSession.active?.isPassThrough && !customHand)
|
294
294
|
this.makeOccluder(child);
|
295
295
|
if (child instanceof Mesh) {
|
296
296
|
NEEDLE_progressive.assignMeshLOD(child, 0);
|
@@ -22,6 +22,7 @@
|
|
22
22
|
declare type HitPointObject = Object3D & { material: Material & { opacity: number } }
|
23
23
|
/**
|
24
24
|
* XRControllerMovement is a component that allows to move the XR rig using the XR controller input.
|
25
|
+
* @category XR
|
25
26
|
*/
|
26
27
|
export class XRControllerMovement extends Behaviour implements XRMovementBehaviour {
|
27
28
|
|
@@ -60,6 +60,10 @@
|
|
60
60
|
}
|
61
61
|
}
|
62
62
|
|
63
|
+
/**
|
64
|
+
* @category XR
|
65
|
+
* @category Utilities
|
66
|
+
*/
|
63
67
|
export class XRFlag extends Behaviour {
|
64
68
|
|
65
69
|
private static registry: XRFlag[] = [];
|
@@ -0,0 +1,43 @@
|
|
1
|
+
import type { GLTF } from "three/examples/jsm/loaders/GLTFLoader.js";
|
2
|
+
|
3
|
+
import { AnimationUtils } from "../engine/engine_animation.js";
|
4
|
+
import { addComponent } from "../engine/engine_components.js";
|
5
|
+
import { ContextEvent, ContextRegistry } from "../engine/engine_context_registry.js";
|
6
|
+
import { Animation } from "./Animation.js";
|
7
|
+
import { Animator } from "./Animator.js";
|
8
|
+
import { GameObject } from "./Component.js";
|
9
|
+
import { PlayableDirector } from "./timeline/PlayableDirector.js";
|
10
|
+
|
11
|
+
ContextRegistry.registerCallback(ContextEvent.ContextCreated, args => {
|
12
|
+
const autoplay = args.context.domElement.getAttribute("autoplay");
|
13
|
+
if (autoplay !== undefined && (autoplay === "" || autoplay === "true" || autoplay === "1")) {
|
14
|
+
if (args.files) {
|
15
|
+
for (const file of args.files) {
|
16
|
+
const hasAnimation = GameObject.foreachComponent(file.file.scene, comp => {
|
17
|
+
if (comp.enabled === false) return undefined;
|
18
|
+
if (comp instanceof Animation && comp.playAutomatically || comp instanceof Animator || comp instanceof PlayableDirector && comp.playOnAwake === true) {
|
19
|
+
return true;
|
20
|
+
}
|
21
|
+
else if (comp instanceof Animation) {
|
22
|
+
comp.playAutomatically = true;
|
23
|
+
return true;
|
24
|
+
}
|
25
|
+
else if (comp instanceof PlayableDirector) {
|
26
|
+
comp.playOnAwake = true;
|
27
|
+
return true;
|
28
|
+
}
|
29
|
+
return undefined;
|
30
|
+
}, true);
|
31
|
+
if (hasAnimation !== true) {
|
32
|
+
AnimationUtils.assignAnimationsFromFile(file.file as GLTF, {
|
33
|
+
createAnimationComponent: (obj, _clip) => {
|
34
|
+
return addComponent(obj, Animation);
|
35
|
+
},
|
36
|
+
});
|
37
|
+
}
|
38
|
+
}
|
39
|
+
}
|
40
|
+
}
|
41
|
+
});
|
42
|
+
|
43
|
+
|