@@ -57,6 +57,8 @@
|
|
57
57
|
}
|
58
58
|
if (debug) {
|
59
59
|
const outputFilePath = path.resolve(projectDir, 'node_modules/.vite/needle.alias.log');
|
60
|
+
const outputDirectory = path.dirname(outputFilePath);
|
61
|
+
if (!existsSync(outputDirectory)) mkdirSync(outputDirectory, { recursive: true });
|
60
62
|
outputDebugFile = createWriteStream(outputFilePath, { flags: "a" });
|
61
63
|
const timestamp = new Date().toISOString();
|
62
64
|
outputDebugFile.write("\n\n\n--------------------------\n");
|
@@ -68,7 +70,7 @@
|
|
68
70
|
const aliasPlugin = {
|
69
71
|
name: "needle-alias",
|
70
72
|
config(config) {
|
71
|
-
if (debug)
|
73
|
+
if (debug) console.log('[needle-alias] ProjectDirectory: ' + projectDir);
|
72
74
|
if (!config.resolve) config.resolve = {};
|
73
75
|
if (!config.resolve.alias) config.resolve.alias = {};
|
74
76
|
const aliasDict = config.resolve.alias;
|
@@ -97,6 +99,7 @@
|
|
97
99
|
let lastImporter = "";
|
98
100
|
/** This plugin logs all imports. This helps to find cases where incorrect folders are found/resolved. */
|
99
101
|
|
102
|
+
/** @type {import("vite").Plugin} */
|
100
103
|
const debuggingPlugin = {
|
101
104
|
name: "needle:alias-debug",
|
102
105
|
// needs to run before regular resolver
|
@@ -115,9 +118,9 @@
|
|
115
118
|
// verbose logging for all imports
|
116
119
|
if (lastImporter !== importer) {
|
117
120
|
lastImporter = importer;
|
118
|
-
log(
|
121
|
+
log(`[needle-alias] Resolving: ${importer} (file${options?.ssr ? ", SSR" : ""})`);
|
119
122
|
}
|
120
|
-
log(
|
123
|
+
log(`[needle-alias] → ${id}`);
|
121
124
|
return;
|
122
125
|
},
|
123
126
|
}
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import fs, { statSync } from 'fs';
|
1
|
+
import fs, { existsSync, statSync } from 'fs';
|
2
2
|
import crypto from 'crypto';
|
3
3
|
|
4
4
|
|
@@ -21,7 +21,12 @@
|
|
21
21
|
const buildInfoPath = `${buildDirectory}/needle.buildinfo.json`;
|
22
22
|
const totalSizeInMB = buildInfo.totalsize / 1024 / 1024;
|
23
23
|
console.log(`[needle-buildinfo] - Collected ${buildInfo.files.length} files (${totalSizeInMB.toFixed(2)} MB). Writing build info to \"${buildInfoPath}\"`);
|
24
|
-
|
24
|
+
const str = JSON.stringify(buildInfo);
|
25
|
+
fs.writeFileSync(buildInfoPath, str);
|
26
|
+
if(!existsSync(buildDirectory)) {
|
27
|
+
console.warn(`WARN: Could not write build info file to \"${buildInfoPath}\"`);
|
28
|
+
return;
|
29
|
+
}
|
25
30
|
}
|
26
31
|
|
27
32
|
/** Recursively collect all files in a directory
|
@@ -42,7 +47,7 @@
|
|
42
47
|
console.log(`[needle-buildinfo] - Collect files in \"/${newPath}\"`);
|
43
48
|
recursivelyCollectFiles(newDirectory, newPath, info);
|
44
49
|
} else {
|
45
|
-
const relpath = `${path}/${entry.name}`;
|
50
|
+
const relpath = path?.length <= 0 ? entry.name : `${path}/${entry.name}`;
|
46
51
|
// console.log("[needle-buildinfo] - New File: " + relpath);
|
47
52
|
const filehash = crypto.createHash('sha256');
|
48
53
|
const fullpath = `${directory}/${entry.name}`;
|
@@ -65,7 +65,7 @@
|
|
65
65
|
|
66
66
|
const outDir = resolve(baseDir, outdirName);
|
67
67
|
if (!existsSync(outDir)) {
|
68
|
-
mkdirSync(outDir);
|
68
|
+
mkdirSync(outDir, { recursive: true });
|
69
69
|
}
|
70
70
|
|
71
71
|
// copy a list of files or directories declared in build.copy = [] in the needle.config.json
|
@@ -11,38 +11,73 @@
|
|
11
11
|
|
12
12
|
const debug = getParam("debuganimator");
|
13
13
|
|
14
|
-
|
14
|
+
/**
|
15
|
+
* Represents an event emitted by an animation mixer
|
16
|
+
* @category Animation and Sequencing
|
17
|
+
*/
|
15
18
|
export declare class MixerEvent {
|
19
|
+
/** The type of event that occurred */
|
16
20
|
type: string;
|
21
|
+
/** The animation action that triggered this event */
|
17
22
|
action: AnimationAction;
|
23
|
+
/** Number of loops completed in this cycle */
|
18
24
|
loopDelta: number;
|
25
|
+
/** The animation mixer that emitted this event */
|
19
26
|
target: AnimationMixer;
|
20
27
|
}
|
21
28
|
|
29
|
+
/**
|
30
|
+
* Configuration options for playing animations
|
31
|
+
* @category Animation and Sequencing
|
32
|
+
*/
|
22
33
|
export declare class PlayOptions {
|
34
|
+
/** Whether the animation should loop, and if so, which loop style to use */
|
23
35
|
loop?: boolean | AnimationActionLoopStyles;
|
36
|
+
/** Whether the final animation state should be maintained after playback completes */
|
24
37
|
clampWhenFinished?: boolean;
|
25
38
|
}
|
26
39
|
|
27
|
-
/**
|
28
|
-
*
|
40
|
+
/**
|
41
|
+
* The Animator component plays and manages animations on a GameObject.
|
42
|
+
* It works with an AnimatorController to handle state transitions and animation blending.
|
43
|
+
* A new AnimatorController can be created from code via `AnimatorController.createFromClips`.
|
29
44
|
* @category Animation and Sequencing
|
30
45
|
* @group Components
|
31
46
|
*/
|
32
47
|
export class Animator extends Behaviour implements IAnimationComponent {
|
33
48
|
|
49
|
+
/**
|
50
|
+
* Identifies this component as an animation component in the engine
|
51
|
+
*/
|
34
52
|
get isAnimationComponent() {
|
35
53
|
return true;
|
36
54
|
}
|
37
55
|
|
56
|
+
/**
|
57
|
+
* When enabled, animation will affect the root transform position and rotation
|
58
|
+
*/
|
38
59
|
@serializable()
|
39
60
|
applyRootMotion: boolean = false;
|
61
|
+
|
62
|
+
/**
|
63
|
+
* Indicates whether this animator contains root motion data
|
64
|
+
*/
|
40
65
|
@serializable()
|
41
66
|
hasRootMotion: boolean = false;
|
67
|
+
|
68
|
+
/**
|
69
|
+
* When enabled, the animator will maintain its state when the component is disabled
|
70
|
+
*/
|
42
71
|
@serializable()
|
43
72
|
keepAnimatorControllerStateOnDisable: boolean = false;
|
44
73
|
|
45
74
|
// set from needle animator extension
|
75
|
+
/**
|
76
|
+
* Sets or replaces the animator controller for this component.
|
77
|
+
* Handles binding the controller to this animator instance and ensures
|
78
|
+
* proper initialization when the controller changes.
|
79
|
+
* @param val The animator controller model or instance to use
|
80
|
+
*/
|
46
81
|
@serializable()
|
47
82
|
set runtimeAnimatorController(val: AnimatorControllerModel | AnimatorController | undefined | null) {
|
48
83
|
if (this._animatorController && this._animatorController.model === val) {
|
@@ -69,41 +104,53 @@
|
|
69
104
|
}
|
70
105
|
else this._animatorController = null;
|
71
106
|
}
|
107
|
+
|
108
|
+
/**
|
109
|
+
* Gets the current animator controller instance
|
110
|
+
* @returns The current animator controller or null if none is assigned
|
111
|
+
*/
|
72
112
|
get runtimeAnimatorController(): AnimatorController | undefined | null {
|
73
113
|
return this._animatorController;
|
74
114
|
}
|
75
115
|
|
76
|
-
/**
|
77
|
-
*
|
78
|
-
* @returns
|
79
|
-
|
116
|
+
/**
|
117
|
+
* Retrieves information about the current animation state
|
118
|
+
* @returns The current state information, or undefined if no state is playing
|
119
|
+
*/
|
80
120
|
getCurrentStateInfo() {
|
81
121
|
return this.runtimeAnimatorController?.getCurrentStateInfo();
|
82
122
|
}
|
83
|
-
/**
|
84
|
-
*
|
85
|
-
|
123
|
+
/**
|
124
|
+
* The currently playing animation action that can be used to modify animation properties
|
125
|
+
* @returns The current animation action, or null if no animation is playing
|
126
|
+
*/
|
86
127
|
get currentAction() {
|
87
128
|
return this.runtimeAnimatorController?.currentAction || null;
|
88
129
|
}
|
89
130
|
|
90
|
-
/**
|
131
|
+
/**
|
132
|
+
* Indicates whether animation parameters have been modified since the last update
|
133
|
+
* @returns True if parameters have been changed
|
134
|
+
*/
|
91
135
|
get parametersAreDirty() { return this._parametersAreDirty; }
|
92
136
|
private _parametersAreDirty: boolean = false;
|
93
137
|
|
94
|
-
/**
|
138
|
+
/**
|
139
|
+
* Indicates whether the animator state has changed since the last update
|
140
|
+
* @returns True if the animator has been changed
|
141
|
+
*/
|
95
142
|
get isDirty() { return this._isDirty; }
|
96
143
|
private _isDirty: boolean = false;
|
97
144
|
|
98
145
|
/**@deprecated use play() */
|
99
146
|
Play(name: string | number, layer: number = -1, normalizedTime: number = Number.NEGATIVE_INFINITY, transitionDurationInSec: number = 0) { this.play(name, layer, normalizedTime, transitionDurationInSec); }
|
100
|
-
/**
|
101
|
-
*
|
102
|
-
* @param
|
103
|
-
* @param
|
104
|
-
* @param
|
105
|
-
* @
|
106
|
-
*
|
147
|
+
/**
|
148
|
+
* Plays an animation on the animator
|
149
|
+
* @param name The name or hash of the animation to play
|
150
|
+
* @param layer The layer to play the animation on (-1 for default layer)
|
151
|
+
* @param normalizedTime The time position to start playing (0-1 range, NEGATIVE_INFINITY for current position)
|
152
|
+
* @param transitionDurationInSec The duration of the blend transition in seconds
|
153
|
+
*/
|
107
154
|
play(name: string | number, layer: number = -1, normalizedTime: number = Number.NEGATIVE_INFINITY, transitionDurationInSec: number = 0) {
|
108
155
|
this.runtimeAnimatorController?.play(name, layer, normalizedTime, transitionDurationInSec);
|
109
156
|
this._isDirty = true;
|
@@ -111,7 +158,9 @@
|
|
111
158
|
|
112
159
|
/**@deprecated use reset */
|
113
160
|
Reset() { this.reset(); }
|
114
|
-
/**
|
161
|
+
/**
|
162
|
+
* Resets the animator controller to its initial state
|
163
|
+
*/
|
115
164
|
reset() {
|
116
165
|
this._animatorController?.reset();
|
117
166
|
this._isDirty = true;
|
@@ -119,6 +168,12 @@
|
|
119
168
|
|
120
169
|
/**@deprecated use setBool */
|
121
170
|
SetBool(name: string | number, val: boolean) { this.setBool(name, val); }
|
171
|
+
|
172
|
+
/**
|
173
|
+
* Sets a boolean parameter in the animator
|
174
|
+
* @param name The name or hash of the parameter
|
175
|
+
* @param value The boolean value to set
|
176
|
+
*/
|
122
177
|
setBool(name: string | number, value: boolean) {
|
123
178
|
if (debug) console.log("setBool", name, value);
|
124
179
|
if (this.runtimeAnimatorController?.getBool(name) !== value)
|
@@ -128,18 +183,34 @@
|
|
128
183
|
|
129
184
|
/**@deprecated use getBool */
|
130
185
|
GetBool(name: string | number) { return this.getBool(name); }
|
186
|
+
|
187
|
+
/**
|
188
|
+
* Gets a boolean parameter from the animator
|
189
|
+
* @param name The name or hash of the parameter
|
190
|
+
* @returns The value of the boolean parameter, or false if not found
|
191
|
+
*/
|
131
192
|
getBool(name: string | number): boolean {
|
132
193
|
const res = this.runtimeAnimatorController?.getBool(name) ?? false;
|
133
194
|
if (debug) console.log("getBool", name, res);
|
134
195
|
return res;
|
135
196
|
}
|
136
197
|
|
198
|
+
/**
|
199
|
+
* Toggles a boolean parameter between true and false
|
200
|
+
* @param name The name or hash of the parameter
|
201
|
+
*/
|
137
202
|
toggleBool(name: string | number) {
|
138
203
|
this.setBool(name, !this.getBool(name));
|
139
204
|
}
|
140
205
|
|
141
206
|
/**@deprecated use setFloat */
|
142
207
|
SetFloat(name: string | number, val: number) { this.setFloat(name, val); }
|
208
|
+
|
209
|
+
/**
|
210
|
+
* Sets a float parameter in the animator
|
211
|
+
* @param name The name or hash of the parameter
|
212
|
+
* @param val The float value to set
|
213
|
+
*/
|
143
214
|
setFloat(name: string | number, val: number) {
|
144
215
|
if (this.runtimeAnimatorController?.getFloat(name) !== val)
|
145
216
|
this._parametersAreDirty = true;
|
@@ -149,6 +220,12 @@
|
|
149
220
|
|
150
221
|
/**@deprecated use getFloat */
|
151
222
|
GetFloat(name: string | number) { return this.getFloat(name); }
|
223
|
+
|
224
|
+
/**
|
225
|
+
* Gets a float parameter from the animator
|
226
|
+
* @param name The name or hash of the parameter
|
227
|
+
* @returns The value of the float parameter, or -1 if not found
|
228
|
+
*/
|
152
229
|
getFloat(name: string | number): number {
|
153
230
|
const res = this.runtimeAnimatorController?.getFloat(name) ?? -1;
|
154
231
|
if (debug) console.log("getFloat", name, res);
|
@@ -157,6 +234,12 @@
|
|
157
234
|
|
158
235
|
/**@deprecated use setInteger */
|
159
236
|
SetInteger(name: string | number, val: number) { this.setInteger(name, val); }
|
237
|
+
|
238
|
+
/**
|
239
|
+
* Sets an integer parameter in the animator
|
240
|
+
* @param name The name or hash of the parameter
|
241
|
+
* @param val The integer value to set
|
242
|
+
*/
|
160
243
|
setInteger(name: string | number, val: number) {
|
161
244
|
if (this.runtimeAnimatorController?.getInteger(name) !== val)
|
162
245
|
this._parametersAreDirty = true;
|
@@ -166,6 +249,12 @@
|
|
166
249
|
|
167
250
|
/**@deprecated use getInteger */
|
168
251
|
GetInteger(name: string | number) { return this.getInteger(name); }
|
252
|
+
|
253
|
+
/**
|
254
|
+
* Gets an integer parameter from the animator
|
255
|
+
* @param name The name or hash of the parameter
|
256
|
+
* @returns The value of the integer parameter, or -1 if not found
|
257
|
+
*/
|
169
258
|
getInteger(name: string | number): number {
|
170
259
|
const res = this.runtimeAnimatorController?.getInteger(name) ?? -1;
|
171
260
|
if (debug) console.log("getInteger", name, res);
|
@@ -174,6 +263,11 @@
|
|
174
263
|
|
175
264
|
/**@deprecated use setTrigger */
|
176
265
|
SetTrigger(name: string | number) { this.setTrigger(name); }
|
266
|
+
|
267
|
+
/**
|
268
|
+
* Activates a trigger parameter in the animator
|
269
|
+
* @param name The name or hash of the trigger parameter
|
270
|
+
*/
|
177
271
|
setTrigger(name: string | number) {
|
178
272
|
this._parametersAreDirty = true;
|
179
273
|
if (debug) console.log("setTrigger", name);
|
@@ -182,6 +276,11 @@
|
|
182
276
|
|
183
277
|
/**@deprecated use resetTrigger */
|
184
278
|
ResetTrigger(name: string | number) { this.resetTrigger(name); }
|
279
|
+
|
280
|
+
/**
|
281
|
+
* Resets a trigger parameter in the animator
|
282
|
+
* @param name The name or hash of the trigger parameter
|
283
|
+
*/
|
185
284
|
resetTrigger(name: string | number) {
|
186
285
|
this._parametersAreDirty = true;
|
187
286
|
if (debug) console.log("resetTrigger", name);
|
@@ -190,6 +289,12 @@
|
|
190
289
|
|
191
290
|
/**@deprecated use getTrigger */
|
192
291
|
GetTrigger(name: string | number) { this.getTrigger(name); }
|
292
|
+
|
293
|
+
/**
|
294
|
+
* Gets the state of a trigger parameter from the animator
|
295
|
+
* @param name The name or hash of the trigger parameter
|
296
|
+
* @returns The state of the trigger parameter
|
297
|
+
*/
|
193
298
|
getTrigger(name: string | number) {
|
194
299
|
const res = this.runtimeAnimatorController?.getTrigger(name);
|
195
300
|
if (debug) console.log("getTrigger", name, res);
|
@@ -198,13 +303,21 @@
|
|
198
303
|
|
199
304
|
/**@deprecated use isInTransition */
|
200
305
|
IsInTransition() { return this.isInTransition(); }
|
201
|
-
/**
|
306
|
+
/**
|
307
|
+
* Checks if the animator is currently in a transition between states
|
308
|
+
* @returns True if the animator is currently blending between animations
|
309
|
+
*/
|
202
310
|
isInTransition(): boolean {
|
203
311
|
return this.runtimeAnimatorController?.isInTransition() ?? false;
|
204
312
|
}
|
205
313
|
|
206
314
|
/**@deprecated use setSpeed */
|
207
315
|
SetSpeed(speed: number) { return this.setSpeed(speed); }
|
316
|
+
|
317
|
+
/**
|
318
|
+
* Sets the playback speed of the animator
|
319
|
+
* @param speed The new playback speed multiplier
|
320
|
+
*/
|
208
321
|
setSpeed(speed: number) {
|
209
322
|
if (speed === this._speed) return;
|
210
323
|
if (debug) console.log("setSpeed", speed);
|
@@ -213,13 +326,20 @@
|
|
213
326
|
this._animatorController.setSpeed(speed);
|
214
327
|
}
|
215
328
|
|
216
|
-
/**
|
329
|
+
/**
|
330
|
+
* Sets a random playback speed between the min and max values
|
331
|
+
* @param minMax Object with x (minimum) and y (maximum) speed values
|
332
|
+
*/
|
217
333
|
set minMaxSpeed(minMax: { x: number, y: number }) {
|
218
334
|
this._speed = Mathf.lerp(minMax.x, minMax.y, Math.random());
|
219
335
|
if (this._animatorController?.animator == this)
|
220
336
|
this._animatorController.setSpeed(this._speed);
|
221
337
|
}
|
222
338
|
|
339
|
+
/**
|
340
|
+
* Sets a random normalized time offset for animations between min (x) and max (y) values
|
341
|
+
* @param minMax Object with x (min) and y (max) values for the offset range
|
342
|
+
*/
|
223
343
|
set minMaxOffsetNormalized(minMax: { x: number, y: number }) {
|
224
344
|
this._normalizedStartOffset = Mathf.lerp(minMax.x, minMax.y, Math.random());
|
225
345
|
if (this.runtimeAnimatorController?.animator == this)
|
@@ -15,6 +15,11 @@
|
|
15
15
|
const debug = getParam("debuganimatorcontroller");
|
16
16
|
const debugRootMotion = getParam("debugrootmotion");
|
17
17
|
|
18
|
+
/**
|
19
|
+
* Generates a hash code for a string
|
20
|
+
* @param str - The string to hash
|
21
|
+
* @returns A numeric hash value
|
22
|
+
*/
|
18
23
|
function stringToHash(str): number {
|
19
24
|
let hash = 0;
|
20
25
|
for (let i = 0; i < str.length; i++) {
|
@@ -25,27 +30,38 @@
|
|
25
30
|
return hash;
|
26
31
|
}
|
27
32
|
|
33
|
+
/**
|
34
|
+
* Configuration options for creating an AnimatorController
|
35
|
+
*/
|
28
36
|
declare type CreateAnimatorControllerOptions = {
|
29
|
-
/** Should each
|
37
|
+
/** Should each animation state loop */
|
30
38
|
looping?: boolean,
|
31
|
-
/** Set to false to disable generating transitions between
|
39
|
+
/** Set to false to disable generating transitions between animation clips */
|
32
40
|
autoTransition?: boolean,
|
33
|
-
/**
|
41
|
+
/** Duration in seconds for transitions between states */
|
34
42
|
transitionDuration?: number,
|
35
43
|
}
|
36
44
|
|
37
45
|
/**
|
38
|
-
*
|
39
|
-
*
|
40
|
-
*
|
41
|
-
*
|
46
|
+
* Controls the playback of animations using a state machine architecture.
|
47
|
+
*
|
48
|
+
* The AnimatorController manages animation states, transitions between states,
|
49
|
+
* and parameters that affect those transitions. It is used by the {@link Animator}
|
50
|
+
* component to control animation behavior on 3D models.
|
51
|
+
*
|
52
|
+
* Use the static method {@link AnimatorController.createFromClips} to create
|
53
|
+
* an animator controller from a set of animation clips.
|
54
|
+
*/
|
42
55
|
export class AnimatorController {
|
43
56
|
|
44
|
-
/**
|
45
|
-
*
|
46
|
-
*
|
47
|
-
*
|
48
|
-
|
57
|
+
/**
|
58
|
+
* Creates an AnimatorController from a set of animation clips.
|
59
|
+
* Each clip becomes a state in the controller's state machine.
|
60
|
+
*
|
61
|
+
* @param clips - The animation clips to use for creating states
|
62
|
+
* @param options - Configuration options for the controller including looping behavior and transitions
|
63
|
+
* @returns A new AnimatorController instance
|
64
|
+
*/
|
49
65
|
static createFromClips(clips: AnimationClip[], options: CreateAnimatorControllerOptions = { looping: false, autoTransition: true, transitionDuration: 0 }): AnimatorController {
|
50
66
|
const states: State[] = [];
|
51
67
|
for (let i = 0; i < clips.length; i++) {
|
@@ -99,6 +115,14 @@
|
|
99
115
|
return controller;
|
100
116
|
}
|
101
117
|
|
118
|
+
/**
|
119
|
+
* Plays an animation state by name or hash.
|
120
|
+
*
|
121
|
+
* @param name - The name or hash identifier of the state to play
|
122
|
+
* @param layerIndex - The layer index (defaults to 0)
|
123
|
+
* @param normalizedTime - The normalized time to start the animation from (0-1)
|
124
|
+
* @param durationInSec - Transition duration in seconds
|
125
|
+
*/
|
102
126
|
play(name: string | number, layerIndex: number = -1, normalizedTime: number = Number.NEGATIVE_INFINITY, durationInSec: number = 0) {
|
103
127
|
if (layerIndex < 0) layerIndex = 0;
|
104
128
|
else if (layerIndex >= this.model.layers.length) {
|
@@ -118,20 +142,42 @@
|
|
118
142
|
console.warn("Could not find " + name + " to play");
|
119
143
|
}
|
120
144
|
|
145
|
+
/**
|
146
|
+
* Resets the controller to its initial state.
|
147
|
+
*/
|
121
148
|
reset() {
|
122
149
|
this.setStartTransition();
|
123
150
|
}
|
124
151
|
|
152
|
+
/**
|
153
|
+
* Sets a boolean parameter value by name or hash.
|
154
|
+
*
|
155
|
+
* @param name - The name or hash identifier of the parameter
|
156
|
+
* @param value - The boolean value to set
|
157
|
+
*/
|
125
158
|
setBool(name: string | number, value: boolean) {
|
126
159
|
const key = typeof name === "string" ? "name" : "hash";
|
127
160
|
return this.model?.parameters?.filter(p => p[key] === name).forEach(p => p.value = value);
|
128
161
|
}
|
129
162
|
|
163
|
+
/**
|
164
|
+
* Gets a boolean parameter value by name or hash.
|
165
|
+
*
|
166
|
+
* @param name - The name or hash identifier of the parameter
|
167
|
+
* @returns The boolean value of the parameter, or false if not found
|
168
|
+
*/
|
130
169
|
getBool(name: string | number): boolean {
|
131
170
|
const key = typeof name === "string" ? "name" : "hash";
|
132
171
|
return this.model?.parameters?.find(p => p[key] === name)?.value as boolean ?? false;
|
133
172
|
}
|
134
173
|
|
174
|
+
/**
|
175
|
+
* Sets a float parameter value by name or hash.
|
176
|
+
*
|
177
|
+
* @param name - The name or hash identifier of the parameter
|
178
|
+
* @param val - The float value to set
|
179
|
+
* @returns True if the parameter was found and set, false otherwise
|
180
|
+
*/
|
135
181
|
setFloat(name: string | number, val: number) {
|
136
182
|
const key = typeof name === "string" ? "name" : "hash";
|
137
183
|
const filtered = this.model?.parameters?.filter(p => p[key] === name);
|
@@ -139,21 +185,45 @@
|
|
139
185
|
return filtered?.length > 0;
|
140
186
|
}
|
141
187
|
|
188
|
+
/**
|
189
|
+
* Gets a float parameter value by name or hash.
|
190
|
+
*
|
191
|
+
* @param name - The name or hash identifier of the parameter
|
192
|
+
* @returns The float value of the parameter, or 0 if not found
|
193
|
+
*/
|
142
194
|
getFloat(name: string | number): number {
|
143
195
|
const key = typeof name === "string" ? "name" : "hash";
|
144
196
|
return this.model?.parameters?.find(p => p[key] === name)?.value as number ?? 0;
|
145
197
|
}
|
146
198
|
|
199
|
+
/**
|
200
|
+
* Sets an integer parameter value by name or hash.
|
201
|
+
*
|
202
|
+
* @param name - The name or hash identifier of the parameter
|
203
|
+
* @param val - The integer value to set
|
204
|
+
*/
|
147
205
|
setInteger(name: string | number, val: number) {
|
148
206
|
const key = typeof name === "string" ? "name" : "hash";
|
149
207
|
return this.model?.parameters?.filter(p => p[key] === name).forEach(p => p.value = val);
|
150
208
|
}
|
151
209
|
|
210
|
+
/**
|
211
|
+
* Gets an integer parameter value by name or hash.
|
212
|
+
*
|
213
|
+
* @param name - The name or hash identifier of the parameter
|
214
|
+
* @returns The integer value of the parameter, or 0 if not found
|
215
|
+
*/
|
152
216
|
getInteger(name: string | number): number {
|
153
217
|
const key = typeof name === "string" ? "name" : "hash";
|
154
218
|
return this.model?.parameters?.find(p => p[key] === name)?.value as number ?? 0;
|
155
219
|
}
|
156
220
|
|
221
|
+
/**
|
222
|
+
* Sets a trigger parameter to active (true).
|
223
|
+
* Trigger parameters are automatically reset after they are consumed by a transition.
|
224
|
+
*
|
225
|
+
* @param name - The name or hash identifier of the trigger parameter
|
226
|
+
*/
|
157
227
|
setTrigger(name: string | number) {
|
158
228
|
if (debug)
|
159
229
|
console.log("SET TRIGGER", name);
|
@@ -161,16 +231,32 @@
|
|
161
231
|
return this.model?.parameters?.filter(p => p[key] === name).forEach(p => p.value = true);
|
162
232
|
}
|
163
233
|
|
234
|
+
/**
|
235
|
+
* Resets a trigger parameter to inactive (false).
|
236
|
+
*
|
237
|
+
* @param name - The name or hash identifier of the trigger parameter
|
238
|
+
*/
|
164
239
|
resetTrigger(name: string | number) {
|
165
240
|
const key = typeof name === "string" ? "name" : "hash";
|
166
241
|
return this.model?.parameters?.filter(p => p[key] === name).forEach(p => p.value = false);
|
167
242
|
}
|
168
243
|
|
244
|
+
/**
|
245
|
+
* Gets the current state of a trigger parameter.
|
246
|
+
*
|
247
|
+
* @param name - The name or hash identifier of the trigger parameter
|
248
|
+
* @returns The boolean state of the trigger, or false if not found
|
249
|
+
*/
|
169
250
|
getTrigger(name: string | number): boolean {
|
170
251
|
const key = typeof name === "string" ? "name" : "hash";
|
171
252
|
return this.model?.parameters?.find(p => p[key] === name)?.value as boolean ?? false;
|
172
253
|
}
|
173
254
|
|
255
|
+
/**
|
256
|
+
* Checks if the controller is currently in a transition between states.
|
257
|
+
*
|
258
|
+
* @returns True if a transition is in progress, false otherwise
|
259
|
+
*/
|
174
260
|
isInTransition(): boolean {
|
175
261
|
return this._activeStates.length > 1;
|
176
262
|
}
|
@@ -182,8 +268,21 @@
|
|
182
268
|
private _speed: number = 1;
|
183
269
|
|
184
270
|
|
185
|
-
/**
|
271
|
+
/**
|
272
|
+
* Finds an animation state by name or hash.
|
273
|
+
* @deprecated Use findState instead
|
274
|
+
*
|
275
|
+
* @param name - The name or hash identifier of the state to find
|
276
|
+
* @returns The found state or null if not found
|
277
|
+
*/
|
186
278
|
FindState(name: string | number | undefined | null): State | null { return this.findState(name); }
|
279
|
+
|
280
|
+
/**
|
281
|
+
* Finds an animation state by name or hash.
|
282
|
+
*
|
283
|
+
* @param name - The name or hash identifier of the state to find
|
284
|
+
* @returns The found state or null if not found
|
285
|
+
*/
|
187
286
|
findState(name: string | number | undefined | null): State | null {
|
188
287
|
if (!name) return null;
|
189
288
|
if (Array.isArray(this.model.layers)) {
|
@@ -196,9 +295,11 @@
|
|
196
295
|
return null;
|
197
296
|
}
|
198
297
|
|
199
|
-
/**
|
200
|
-
*
|
201
|
-
|
298
|
+
/**
|
299
|
+
* Gets information about the current playing animation state.
|
300
|
+
*
|
301
|
+
* @returns An AnimatorStateInfo object with data about the current state, or null if no state is active
|
302
|
+
*/
|
202
303
|
getCurrentStateInfo() {
|
203
304
|
if (!this._activeState) return null;
|
204
305
|
const action = this._activeState.motion.action;
|
@@ -208,10 +309,11 @@
|
|
208
309
|
return new AnimatorStateInfo(this._activeState, normalizedTime, dur, this._speed);
|
209
310
|
}
|
210
311
|
|
211
|
-
/**
|
212
|
-
*
|
213
|
-
*
|
214
|
-
*
|
312
|
+
/**
|
313
|
+
* Gets the animation action currently playing.
|
314
|
+
*
|
315
|
+
* @returns The current animation action, or null if no action is playing
|
316
|
+
*/
|
215
317
|
get currentAction(): AnimationAction | null {
|
216
318
|
if (!this._activeState) return null;
|
217
319
|
const action = this._activeState.motion.action;
|
@@ -219,24 +321,37 @@
|
|
219
321
|
return action;
|
220
322
|
}
|
221
323
|
|
222
|
-
/**
|
324
|
+
/**
|
325
|
+
* The normalized time (0-1) to start playing the first state at.
|
326
|
+
* This affects the initial state when the animator is first enabled.
|
327
|
+
*/
|
223
328
|
normalizedStartOffset: number = 0;
|
224
329
|
|
225
|
-
/**
|
330
|
+
/**
|
331
|
+
* The Animator component this controller is bound to.
|
332
|
+
*/
|
226
333
|
animator?: Animator;
|
227
|
-
|
334
|
+
|
335
|
+
/**
|
336
|
+
* The data model describing the animation states and transitions.
|
337
|
+
*/
|
228
338
|
model: AnimatorControllerModel;
|
229
339
|
|
230
|
-
/**
|
340
|
+
/**
|
341
|
+
* Gets the engine context from the bound animator.
|
342
|
+
*/
|
231
343
|
get context(): Context | undefined | null { return this.animator?.context; }
|
232
344
|
|
233
|
-
/**
|
345
|
+
/**
|
346
|
+
* Gets the animation mixer used by this controller.
|
347
|
+
*/
|
234
348
|
get mixer() {
|
235
349
|
return this._mixer;
|
236
350
|
}
|
237
351
|
|
238
352
|
/**
|
239
|
-
*
|
353
|
+
* Cleans up resources used by this controller.
|
354
|
+
* Stops all animations and unregisters the mixer from the animation system.
|
240
355
|
*/
|
241
356
|
dispose() {
|
242
357
|
this._mixer.stopAllAction();
|
@@ -254,7 +369,12 @@
|
|
254
369
|
// // this.internalApplyRootMotion(obj);
|
255
370
|
// }
|
256
371
|
|
257
|
-
/**
|
372
|
+
/**
|
373
|
+
* Binds this controller to an animator component.
|
374
|
+
* Creates a new animation mixer and sets up animation actions.
|
375
|
+
*
|
376
|
+
* @param animator - The animator to bind this controller to
|
377
|
+
*/
|
258
378
|
bind(animator: Animator) {
|
259
379
|
if (!animator) console.error("AnimatorController.bind: animator is null");
|
260
380
|
else if (this.animator !== animator) {
|
@@ -269,7 +389,12 @@
|
|
269
389
|
}
|
270
390
|
}
|
271
391
|
|
272
|
-
/**
|
392
|
+
/**
|
393
|
+
* Creates a deep copy of this controller.
|
394
|
+
* Clones the model data but does not copy runtime state.
|
395
|
+
*
|
396
|
+
* @returns A new AnimatorController instance with the same configuration
|
397
|
+
*/
|
273
398
|
clone() {
|
274
399
|
if (typeof this.model === "string") {
|
275
400
|
console.warn("AnimatorController has not been resolved, can not create model from string", this.model);
|
@@ -297,7 +422,12 @@
|
|
297
422
|
return controller;
|
298
423
|
}
|
299
424
|
|
300
|
-
/**
|
425
|
+
/**
|
426
|
+
* Updates the controller's state machine and animations.
|
427
|
+
* Called each frame by the animator component.
|
428
|
+
*
|
429
|
+
* @param weight - The weight to apply to the animations (for blending)
|
430
|
+
*/
|
301
431
|
update(weight: number) {
|
302
432
|
if (!this.animator) return;
|
303
433
|
this.evaluateTransitions();
|
@@ -318,9 +448,11 @@
|
|
318
448
|
private _mixer!: AnimationMixer;
|
319
449
|
private _activeState?: State;
|
320
450
|
|
321
|
-
/**
|
322
|
-
*
|
323
|
-
*
|
451
|
+
/**
|
452
|
+
* Gets the currently active animation state.
|
453
|
+
*
|
454
|
+
* @returns The active state or undefined if no state is active
|
455
|
+
*/
|
324
456
|
get activeState(): State | undefined { return this._activeState; }
|
325
457
|
|
326
458
|
constructor(model: AnimatorControllerModel) {
|
@@ -770,6 +902,10 @@
|
|
770
902
|
}
|
771
903
|
}
|
772
904
|
|
905
|
+
/**
|
906
|
+
* Yields all animation actions managed by this controller.
|
907
|
+
* Iterates through all states in all layers and returns their actions.
|
908
|
+
*/
|
773
909
|
*enumerateActions() {
|
774
910
|
if (!this.model.layers) return;
|
775
911
|
for (const layer of this.model.layers) {
|
@@ -807,6 +943,10 @@
|
|
807
943
|
// }
|
808
944
|
}
|
809
945
|
|
946
|
+
/**
|
947
|
+
* Wraps a KeyframeTrack to allow custom evaluation of animation values.
|
948
|
+
* Used internally to modify animation behavior without changing the original data.
|
949
|
+
*/
|
810
950
|
class TrackEvaluationWrapper {
|
811
951
|
|
812
952
|
track?: KeyframeTrack;
|
@@ -844,6 +984,10 @@
|
|
844
984
|
}
|
845
985
|
}
|
846
986
|
|
987
|
+
/**
|
988
|
+
* Handles root motion extraction from animation tracks.
|
989
|
+
* Captures movement from animations and applies it to the root object.
|
990
|
+
*/
|
847
991
|
class RootMotionAction {
|
848
992
|
|
849
993
|
private static lastObjPosition: { [key: string]: Vector3 } = {};
|
@@ -1043,6 +1187,10 @@
|
|
1043
1187
|
}
|
1044
1188
|
}
|
1045
1189
|
|
1190
|
+
/**
|
1191
|
+
* Manages root motion for a character.
|
1192
|
+
* Extracts motion from animation tracks and applies it to the character's transform.
|
1193
|
+
*/
|
1046
1194
|
class RootMotionHandler {
|
1047
1195
|
|
1048
1196
|
private controller: AnimatorController;
|
@@ -1129,8 +1277,10 @@
|
|
1129
1277
|
}
|
1130
1278
|
}
|
1131
1279
|
|
1132
|
-
|
1133
|
-
|
1280
|
+
/**
|
1281
|
+
* Serialization handler for AnimatorController instances.
|
1282
|
+
* Handles conversion between serialized data and runtime objects.
|
1283
|
+
*/
|
1134
1284
|
class AnimatorControllerSerializator extends TypeSerializer {
|
1135
1285
|
onSerialize(_: any, _context: SerializationContext) {
|
1136
1286
|
|
@@ -5,15 +5,18 @@
|
|
5
5
|
import { Behaviour, GameObject } from "./Component.js";
|
6
6
|
|
7
7
|
/**
|
8
|
-
* AudioListener represents a listener that can
|
8
|
+
* AudioListener represents a listener that can hear audio sources in the scene.
|
9
|
+
* This component creates and manages a Three.js {@link three#AudioListener}, automatically connecting it
|
10
|
+
* to the main camera or a Camera in the parent hierarchy.
|
9
11
|
* @category Multimedia
|
10
12
|
* @group Components
|
11
13
|
*/
|
12
14
|
export class AudioListener extends Behaviour {
|
13
15
|
|
14
16
|
/**
|
15
|
-
* Gets the existing or creates a new
|
16
|
-
*
|
17
|
+
* Gets the existing Three.js {@link three#AudioListener} instance or creates a new one if it doesn't exist.
|
18
|
+
* This listener is responsible for capturing audio in the 3D scene.
|
19
|
+
* @returns The {@link three#AudioListener} instance
|
17
20
|
*/
|
18
21
|
get listener(): ThreeAudioListener {
|
19
22
|
if (this._listener == null)
|
@@ -23,13 +26,21 @@
|
|
23
26
|
|
24
27
|
private _listener: ThreeAudioListener | null = null;
|
25
28
|
|
26
|
-
/**
|
29
|
+
/**
|
30
|
+
* Registers for interaction events and initializes the audio listener
|
31
|
+
* when this component is enabled.
|
32
|
+
* @internal
|
33
|
+
*/
|
27
34
|
onEnable(): void {
|
28
35
|
Application.registerWaitForInteraction(this.onInteraction);
|
29
36
|
this.addListenerIfItExists();
|
30
37
|
}
|
31
38
|
|
32
|
-
/**
|
39
|
+
/**
|
40
|
+
* Cleans up event registrations and removes the audio listener
|
41
|
+
* when this component is disabled.
|
42
|
+
* @internal
|
43
|
+
*/
|
33
44
|
onDisable(): void {
|
34
45
|
Application.unregisterWaitForInteraction(this.onInteraction);
|
35
46
|
this.removeListenerIfItExists();
|
@@ -3,6 +3,7 @@
|
|
3
3
|
|
4
4
|
import { isDevEnvironment } from "../engine/debug/index.js";
|
5
5
|
import { Application, ApplicationEvents } from "../engine/engine_application.js";
|
6
|
+
import { findObjectOfType } from "../engine/engine_components.js";
|
6
7
|
import { Mathf } from "../engine/engine_math.js";
|
7
8
|
import { serializable } from "../engine/engine_serialization_decorator.js";
|
8
9
|
import { DeviceUtilities, getParam } from "../engine/engine_utils.js";
|
@@ -13,84 +14,121 @@
|
|
13
14
|
const debug = getParam("debugaudio");
|
14
15
|
|
15
16
|
/**
|
16
|
-
*
|
17
|
+
* Defines how audio volume attenuates over distance from the listener.
|
17
18
|
*/
|
18
19
|
export enum AudioRolloffMode {
|
19
|
-
/
|
20
|
-
|
21
|
-
|
20
|
+
/**
|
21
|
+
* Logarithmic rolloff provides a natural, real-world attenuation where volume decreases
|
22
|
+
* exponentially with distance.
|
23
|
+
*/
|
22
24
|
Logarithmic = 0,
|
23
|
-
|
24
|
-
/
|
25
|
-
|
25
|
+
|
26
|
+
/**
|
27
|
+
* Linear rolloff provides a straightforward volume reduction that decreases at a constant
|
28
|
+
* rate with distance.
|
29
|
+
*/
|
26
30
|
Linear = 1,
|
27
|
-
|
28
|
-
/
|
29
|
-
|
31
|
+
|
32
|
+
/**
|
33
|
+
* Custom rolloff allows for defining specialized distance-based attenuation curves.
|
34
|
+
* Note: Custom rolloff is not fully implemented in this version.
|
35
|
+
*/
|
30
36
|
Custom = 2,
|
31
37
|
}
|
32
38
|
|
33
39
|
|
34
|
-
/**
|
35
|
-
*
|
40
|
+
/**
|
41
|
+
* Plays audio clips in the scene, with support for spatial positioning.
|
42
|
+
*
|
43
|
+
* The AudioSource component can play audio files or media streams with
|
44
|
+
* options for spatial blending, volume control, looping, and more.
|
45
|
+
*
|
46
|
+
* When a page loses visibility (tab becomes inactive), audio will automatically
|
47
|
+
* pause unless {@link playInBackground} is set to true. On mobile devices, audio always
|
48
|
+
* pauses regardless of this setting. When the page becomes visible again,
|
49
|
+
* previously playing audio will resume.
|
50
|
+
*
|
51
|
+
* AudioSource also responds to application mute state changes. When the application
|
52
|
+
* is muted, the volume is set to 0. When unmuted, the volume
|
53
|
+
* returns to its previous value.
|
54
|
+
*
|
36
55
|
* @category Multimedia
|
37
56
|
* @group Components
|
38
57
|
*/
|
39
58
|
export class AudioSource extends Behaviour {
|
40
59
|
|
41
|
-
/**
|
42
|
-
*
|
60
|
+
/**
|
61
|
+
* Checks if the user has interacted with the page to allow audio playback.
|
62
|
+
* Audio playback often requires a user gesture first due to browser autoplay policies.
|
63
|
+
* This is the same as calling {@link Application.userInteractionRegistered}.
|
64
|
+
*
|
65
|
+
* @returns Whether user interaction has been registered to allow audio playback
|
43
66
|
*/
|
44
67
|
public static get userInteractionRegistered(): boolean {
|
45
68
|
return Application.userInteractionRegistered;
|
46
69
|
}
|
47
70
|
|
48
|
-
/**
|
49
|
-
*
|
71
|
+
/**
|
72
|
+
* Registers a callback that will be executed once the user has interacted with the page,
|
73
|
+
* allowing audio playback to begin.
|
74
|
+
* This is the same as calling {@link Application.registerWaitForInteraction}.
|
75
|
+
*
|
76
|
+
* @param cb - The callback function to execute when user interaction is registered
|
50
77
|
*/
|
51
78
|
public static registerWaitForAllowAudio(cb: Function) {
|
52
79
|
Application.registerWaitForInteraction(cb);
|
53
80
|
}
|
54
81
|
|
55
82
|
/**
|
56
|
-
* The audio clip to play. Can be a string
|
83
|
+
* The audio clip to play. Can be a URL string pointing to an audio file or a {@link MediaStream} object.
|
57
84
|
*/
|
58
85
|
@serializable(URL)
|
59
86
|
clip: string | MediaStream = "";
|
60
87
|
|
61
88
|
/**
|
62
|
-
*
|
63
|
-
*
|
89
|
+
* When true, the audio will automatically start playing when the component is enabled.
|
90
|
+
* When false, you must call play() manually to start audio playback.
|
64
91
|
* @default false
|
65
|
-
|
92
|
+
*/
|
66
93
|
@serializable()
|
67
94
|
playOnAwake: boolean = false;
|
68
95
|
|
69
96
|
/**
|
70
|
-
*
|
71
|
-
*
|
97
|
+
* When true, the audio clip will be loaded during initialization rather than when play() is called.
|
98
|
+
* This can reduce playback delay but increases initial loading time.
|
72
99
|
* @default false
|
73
100
|
*/
|
74
101
|
@serializable()
|
75
102
|
preload: boolean = false;
|
76
103
|
|
77
104
|
/**
|
78
|
-
* When true,
|
105
|
+
* When true, audio will continue playing when the browser tab loses focus.
|
106
|
+
* When false, audio will pause when the tab is minimized or not active.
|
79
107
|
* @default true
|
80
108
|
*/
|
81
109
|
@serializable()
|
82
110
|
playInBackground: boolean = true;
|
83
111
|
|
84
112
|
/**
|
85
|
-
*
|
113
|
+
* Indicates whether the audio is currently playing.
|
114
|
+
*
|
115
|
+
* @returns True if the audio is playing, false otherwise
|
86
116
|
*/
|
87
117
|
get isPlaying(): boolean { return this.sound?.isPlaying ?? false; }
|
88
118
|
|
89
|
-
/**
|
119
|
+
/**
|
120
|
+
* The total duration of the current audio clip in seconds.
|
121
|
+
*
|
122
|
+
* @returns Duration in seconds or undefined if no clip is loaded
|
123
|
+
*/
|
90
124
|
get duration() {
|
91
125
|
return this.sound?.buffer?.duration;
|
92
126
|
}
|
93
|
-
|
127
|
+
|
128
|
+
/**
|
129
|
+
* The current playback position as a normalized value between 0 and 1.
|
130
|
+
* Can be set to seek to a specific position in the audio.
|
131
|
+
*/
|
94
132
|
get time01() {
|
95
133
|
const duration = this.duration;
|
96
134
|
if (duration && this.sound) {
|
@@ -106,7 +144,8 @@
|
|
106
144
|
}
|
107
145
|
|
108
146
|
/**
|
109
|
-
* The current
|
147
|
+
* The current playback position in seconds.
|
148
|
+
* Can be set to seek to a specific time in the audio.
|
110
149
|
*/
|
111
150
|
get time(): number { return this.sound?.source ? (this.sound.source?.context.currentTime - this._lastContextTime + this.sound.offset) : 0; }
|
112
151
|
set time(val: number) {
|
@@ -121,8 +160,8 @@
|
|
121
160
|
}
|
122
161
|
|
123
162
|
/**
|
124
|
-
*
|
125
|
-
*
|
163
|
+
* When true, the audio will repeat after reaching the end.
|
164
|
+
* When false, audio will play once and stop.
|
126
165
|
* @default false
|
127
166
|
*/
|
128
167
|
@serializable()
|
@@ -134,10 +173,12 @@
|
|
134
173
|
this._loop = val;
|
135
174
|
if (this.sound) this.sound.setLoop(val);
|
136
175
|
}
|
137
|
-
|
138
|
-
|
139
|
-
*
|
140
|
-
*
|
176
|
+
|
177
|
+
/**
|
178
|
+
* Controls how the audio is positioned in space.
|
179
|
+
* Values range from 0 (2D, non-positional) to 1 (fully 3D positioned).
|
180
|
+
* Note: 2D playback is not fully supported in the current implementation.
|
181
|
+
*/
|
141
182
|
@serializable()
|
142
183
|
get spatialBlend(): number {
|
143
184
|
return this._spatialBlend;
|
@@ -147,6 +188,11 @@
|
|
147
188
|
this._spatialBlend = val;
|
148
189
|
this._needUpdateSpatialDistanceSettings = true;
|
149
190
|
}
|
191
|
+
|
192
|
+
/**
|
193
|
+
* The minimum distance from the audio source at which the volume starts to attenuate.
|
194
|
+
* Within this radius, the audio plays at full volume regardless of distance.
|
195
|
+
*/
|
150
196
|
@serializable()
|
151
197
|
get minDistance(): number {
|
152
198
|
return this._minDistance;
|
@@ -156,6 +202,11 @@
|
|
156
202
|
this._minDistance = val;
|
157
203
|
this._needUpdateSpatialDistanceSettings = true;
|
158
204
|
}
|
205
|
+
|
206
|
+
/**
|
207
|
+
* The maximum distance from the audio source beyond which the volume no longer decreases.
|
208
|
+
* This defines the outer limit of the attenuation curve.
|
209
|
+
*/
|
159
210
|
@serializable()
|
160
211
|
get maxDistance(): number {
|
161
212
|
return this._maxDistance;
|
@@ -170,6 +221,11 @@
|
|
170
221
|
private _minDistance: number = 1;
|
171
222
|
private _maxDistance: number = 100;
|
172
223
|
|
224
|
+
/**
|
225
|
+
* Controls the overall volume/loudness of the audio.
|
226
|
+
* Values range from 0 (silent) to 1 (full volume).
|
227
|
+
* @default 1
|
228
|
+
*/
|
173
229
|
@serializable()
|
174
230
|
get volume(): number { return this._volume; }
|
175
231
|
set volume(val: number) {
|
@@ -181,6 +237,12 @@
|
|
181
237
|
}
|
182
238
|
private _volume: number = 1;
|
183
239
|
|
240
|
+
/**
|
241
|
+
* Controls the playback rate (speed) of the audio.
|
242
|
+
* Values greater than 1 increase speed, values less than 1 decrease it.
|
243
|
+
* This affects both speed and pitch of the audio.
|
244
|
+
* @default 1
|
245
|
+
*/
|
184
246
|
@serializable()
|
185
247
|
set pitch(val: number) {
|
186
248
|
if (this.sound) this.sound.setPlaybackRate(val);
|
@@ -189,6 +251,11 @@
|
|
189
251
|
return this.sound ? this.sound.getPlaybackRate() : 1;
|
190
252
|
}
|
191
253
|
|
254
|
+
/**
|
255
|
+
* Determines how audio volume decreases with distance from the listener.
|
256
|
+
* @default AudioRolloffMode.Logarithmic
|
257
|
+
* @see {@link AudioRolloffMode}
|
258
|
+
*/
|
192
259
|
@serializable()
|
193
260
|
rollOffMode: AudioRolloffMode = 0;
|
194
261
|
|
@@ -204,10 +271,21 @@
|
|
204
271
|
private _lastClipStartedLoading: string | MediaStream | null = null;
|
205
272
|
private _audioElement: HTMLAudioElement | null = null;
|
206
273
|
|
274
|
+
/**
|
275
|
+
* Returns the underlying {@link PositionalAudio} object, creating it if necessary.
|
276
|
+
* The audio source needs a user interaction to be initialized due to browser autoplay policies.
|
277
|
+
*
|
278
|
+
* @returns The three.js PositionalAudio object or null if unavailable
|
279
|
+
*/
|
207
280
|
public get Sound(): PositionalAudio | null {
|
208
281
|
if (!this.sound && AudioSource.userInteractionRegistered) {
|
209
|
-
|
210
|
-
|
282
|
+
// Get or create an audiolistener in the scene
|
283
|
+
let listener = this.gameObject.getComponent(AudioListener) // AudioListener on AudioSource?
|
284
|
+
?? this.context.mainCamera.getComponent(AudioListener) // AudioListener on current main camera?
|
285
|
+
?? findObjectOfType(AudioListener, this.context, false); // Active AudioListener in scene?
|
286
|
+
|
287
|
+
if (!listener && this.context.mainCamera) listener = this.context.mainCamera.addComponent(AudioListener);
|
288
|
+
|
211
289
|
if (listener?.listener) {
|
212
290
|
this.sound = new PositionalAudio(listener.listener);
|
213
291
|
this.gameObject?.add(this.sound);
|
@@ -250,9 +328,19 @@
|
|
250
328
|
// }
|
251
329
|
// }
|
252
330
|
|
331
|
+
/**
|
332
|
+
* Indicates whether the audio source is queued to play when possible.
|
333
|
+
* This may be true before user interaction has been registered.
|
334
|
+
*
|
335
|
+
* @returns Whether the audio source intends to play
|
336
|
+
*/
|
253
337
|
public get ShouldPlay(): boolean { return this.shouldPlay; }
|
254
338
|
|
255
|
-
/**
|
339
|
+
/**
|
340
|
+
* Returns the Web Audio API context associated with this audio source.
|
341
|
+
*
|
342
|
+
* @returns The {@link AudioContext} or null if not available
|
343
|
+
*/
|
256
344
|
public get audioContext() {
|
257
345
|
return this.sound?.context;
|
258
346
|
}
|
@@ -344,6 +432,9 @@
|
|
344
432
|
if (sound.isPlaying)
|
345
433
|
sound.stop();
|
346
434
|
|
435
|
+
// const panner = sound.panner;
|
436
|
+
// panner.coneOuterGain = 1;
|
437
|
+
// sound.setDirectionalCone(360, 360, 1);
|
347
438
|
// const src = sound.context.createBufferSource();
|
348
439
|
// src.buffer = sound.buffer;
|
349
440
|
// src.connect(sound.panner);
|
@@ -424,7 +515,12 @@
|
|
424
515
|
}
|
425
516
|
}
|
426
517
|
|
427
|
-
/**
|
518
|
+
/**
|
519
|
+
* Plays the audio clip or media stream.
|
520
|
+
* If no argument is provided, plays the currently assigned clip.
|
521
|
+
*
|
522
|
+
* @param clip - Optional audio clip or {@link MediaStream} to play
|
523
|
+
*/
|
428
524
|
play(clip: string | MediaStream | undefined = undefined) {
|
429
525
|
// use audio source's clip when no clip is passed in
|
430
526
|
if (!clip && this.clip)
|
@@ -483,7 +579,8 @@
|
|
483
579
|
}
|
484
580
|
|
485
581
|
/**
|
486
|
-
*
|
582
|
+
* Pauses audio playback while maintaining the current position.
|
583
|
+
* Use play() to resume from the paused position.
|
487
584
|
*/
|
488
585
|
pause() {
|
489
586
|
if (debug) console.log("Pause", this);
|
@@ -497,7 +594,8 @@
|
|
497
594
|
}
|
498
595
|
|
499
596
|
/**
|
500
|
-
*
|
597
|
+
* Stops audio playback completely and resets the playback position to the beginning.
|
598
|
+
* Unlike pause(), calling play() after stop() will start from the beginning.
|
501
599
|
*/
|
502
600
|
stop() {
|
503
601
|
if (debug) console.log("Pause", this);
|
@@ -10,17 +10,37 @@
|
|
10
10
|
|
11
11
|
const debug = utils.getParam("debugavatar");
|
12
12
|
|
13
|
+
/**
|
14
|
+
* Represents an avatar model with head and hands references.
|
15
|
+
* Used for representing characters in 3D space.
|
16
|
+
*/
|
13
17
|
export class AvatarModel {
|
18
|
+
/** The root object of the avatar model */
|
14
19
|
root: Object3D;
|
20
|
+
/** The head object of the avatar model */
|
15
21
|
head: Object3D;
|
22
|
+
/** The left hand object of the avatar model, if available */
|
16
23
|
leftHand: Object3D | null;
|
24
|
+
/** The right hand object of the avatar model, if available */
|
17
25
|
rigthHand: Object3D | null;
|
18
26
|
|
19
27
|
|
28
|
+
/**
|
29
|
+
* Checks if the avatar model has a valid configuration.
|
30
|
+
* An avatar is considered valid if it has a head.
|
31
|
+
* @returns Whether the avatar has a valid setup
|
32
|
+
*/
|
20
33
|
get isValid(): boolean {
|
21
34
|
return this.head !== null && this.head !== undefined;
|
22
35
|
}
|
23
36
|
|
37
|
+
/**
|
38
|
+
* Creates a new avatar model.
|
39
|
+
* @param root The root object of the avatar
|
40
|
+
* @param head The head object of the avatar
|
41
|
+
* @param leftHand The left hand object of the avatar
|
42
|
+
* @param rigthHand The right hand object of the avatar
|
43
|
+
*/
|
24
44
|
constructor(root: Object3D, head: Object3D, leftHand: Object3D | null, rigthHand: Object3D | null) {
|
25
45
|
this.root = root;
|
26
46
|
this.head = head;
|
@@ -33,12 +53,25 @@
|
|
33
53
|
}
|
34
54
|
}
|
35
55
|
|
56
|
+
/**
|
57
|
+
* Handles loading and instantiating avatar models from various sources.
|
58
|
+
* Provides functionality to find and extract important parts of an avatar (head, hands).
|
59
|
+
*
|
60
|
+
* Debug mode can be enabled with the URL parameter `?debugavatar`,
|
61
|
+
* which will log detailed information about avatar loading and configuration.
|
62
|
+
*/
|
36
63
|
export class AvatarLoader {
|
37
64
|
|
38
65
|
private readonly avatarRegistryUrl: string | null = null;
|
39
66
|
// private loader: GLTFLoader | null;
|
40
67
|
// private avatarModelCache: Map<string, AvatarModel | null> = new Map<string, AvatarModel | null>();
|
41
68
|
|
69
|
+
/**
|
70
|
+
* Retrieves or creates a new avatar instance from an ID or existing Object3D.
|
71
|
+
* @param context The application context
|
72
|
+
* @param avatarId Either a string ID to load an avatar or an existing Object3D to use as avatar
|
73
|
+
* @returns Promise resolving to an AvatarModel if successful, or null if failed
|
74
|
+
*/
|
42
75
|
public async getOrCreateNewAvatarInstance(context: Context, avatarId: string | Object3D): Promise<AvatarModel | null> {
|
43
76
|
|
44
77
|
if (!avatarId) {
|
@@ -76,6 +109,12 @@
|
|
76
109
|
}
|
77
110
|
|
78
111
|
|
112
|
+
/**
|
113
|
+
* Loads an avatar model from a file or registry using the provided ID.
|
114
|
+
* @param context The engine context
|
115
|
+
* @param avatarId The ID of the avatar to load
|
116
|
+
* @returns Promise resolving to the loaded avatar's Object3D, or null if failed
|
117
|
+
*/
|
79
118
|
private async loadAvatar(context: Context, avatarId: string): Promise<Object3D | null> {
|
80
119
|
|
81
120
|
console.assert(avatarId !== undefined && avatarId !== null && typeof avatarId === "string", "Avatar id must not be null");
|
@@ -140,11 +179,20 @@
|
|
140
179
|
});
|
141
180
|
}
|
142
181
|
|
182
|
+
/**
|
183
|
+
* Caches an avatar model for reuse.
|
184
|
+
* @param _id The ID to associate with the model
|
185
|
+
* @param _model The avatar model to cache
|
186
|
+
*/
|
143
187
|
private cacheModel(_id: string, _model: AvatarModel) {
|
144
188
|
// this.avatarModelCache.set(id, model);
|
145
189
|
}
|
146
190
|
|
147
|
-
/
|
191
|
+
/**
|
192
|
+
* Analyzes an Object3D to find avatar parts (head, hands) based on naming conventions.
|
193
|
+
* @param obj The Object3D to search for avatar parts
|
194
|
+
* @returns A structured AvatarModel with references to found parts
|
195
|
+
*/
|
148
196
|
private findAvatar(obj: Object3D): AvatarModel {
|
149
197
|
|
150
198
|
const root: Object3D = obj;
|
@@ -175,7 +223,12 @@
|
|
175
223
|
return model;
|
176
224
|
}
|
177
225
|
|
178
|
-
|
226
|
+
/**
|
227
|
+
* Recursively searches for an avatar part by name within an Object3D hierarchy.
|
228
|
+
* @param obj The Object3D to search within
|
229
|
+
* @param searchString Array of strings that should all be present in the object name
|
230
|
+
* @returns The found Object3D part or null if not found
|
231
|
+
*/
|
179
232
|
private findAvatarPart(obj: Object3D, searchString: string[]): Object3D | null {
|
180
233
|
|
181
234
|
const name = obj.name.toLowerCase();
|
@@ -196,6 +249,12 @@
|
|
196
249
|
return null;
|
197
250
|
}
|
198
251
|
|
252
|
+
/**
|
253
|
+
* Handles HTTP response errors from avatar loading operations.
|
254
|
+
* @param response The fetch API response to check
|
255
|
+
* @returns The response if it was ok
|
256
|
+
* @throws Error with status text if response was not ok
|
257
|
+
*/
|
199
258
|
private handleCustomAvatarErrors(response) {
|
200
259
|
if (!response.ok) {
|
201
260
|
throw Error(response.statusText);
|
@@ -5,20 +5,37 @@
|
|
5
5
|
import { Behaviour } from "./Component.js";
|
6
6
|
|
7
7
|
/**
|
8
|
-
*
|
8
|
+
* Component that visualizes the axes of an object in the scene.
|
9
|
+
* Renders colored lines representing the X (red), Y (green) and Z (blue) axes.
|
9
10
|
* @category Helpers
|
10
11
|
* @group Components
|
11
12
|
*/
|
12
13
|
export class AxesHelper extends Behaviour {
|
14
|
+
/**
|
15
|
+
* The length of each axis line in scene units.
|
16
|
+
*/
|
13
17
|
@serializable()
|
14
18
|
public length: number = 1;
|
19
|
+
|
20
|
+
/**
|
21
|
+
* Whether the axes should be occluded by objects in the scene.
|
22
|
+
* When set to false, axes will always appear on top regardless of their depth.
|
23
|
+
*/
|
15
24
|
@serializable()
|
16
25
|
public depthTest: boolean = true;
|
26
|
+
|
27
|
+
/**
|
28
|
+
* When true, this helper will only be visible if the debug flag `?gizmos` is enabled.
|
29
|
+
*/
|
17
30
|
@serializable()
|
18
31
|
public isGizmo: boolean = false;
|
19
32
|
|
20
33
|
private _axes: _AxesHelper | null = null;
|
21
34
|
|
35
|
+
/**
|
36
|
+
* Creates and adds the axes visualization to the scene when the component is enabled.
|
37
|
+
* If marked as a gizmo, it will only be shown when gizmos are enabled in the global parameters.
|
38
|
+
*/
|
22
39
|
onEnable(): void {
|
23
40
|
if (this.isGizmo && !params.showGizmos) return;
|
24
41
|
if (!this._axes)
|
@@ -33,6 +50,9 @@
|
|
33
50
|
}
|
34
51
|
}
|
35
52
|
|
53
|
+
/**
|
54
|
+
* Removes the axes visualization from the scene when the component is disabled.
|
55
|
+
*/
|
36
56
|
onDisable(): void {
|
37
57
|
if (!this._axes) return;
|
38
58
|
this.gameObject.remove(this._axes);
|
@@ -9,11 +9,17 @@
|
|
9
9
|
const debug = getParam("debugboxhelper");
|
10
10
|
|
11
11
|
/**
|
12
|
+
* A component that creates a bounding box around an object and provides intersection testing functionality.
|
13
|
+
*
|
14
|
+
* Debug mode can be enabled with the URL parameter `?debugboxhelper`, which will visualize intersection tests.
|
15
|
+
* Helper visualization can be enabled with the URL parameter `?gizmos`.
|
16
|
+
*
|
12
17
|
* @category Helpers
|
13
18
|
* @group Components
|
14
19
|
*/
|
15
20
|
export class BoxHelperComponent extends Behaviour {
|
16
21
|
|
22
|
+
/** The bounding box for this component */
|
17
23
|
private box: Box3 | null = null;
|
18
24
|
private static testBox: Box3 = new Box3();
|
19
25
|
private _lastMatrixUpdateFrame: number = -1;
|
@@ -21,6 +27,11 @@
|
|
21
27
|
private static _size: Vector3 = new Vector3(.01, .01, .01);
|
22
28
|
private static _emptyObjectSize: Vector3 = new Vector3(.01, .01, .01);
|
23
29
|
|
30
|
+
/**
|
31
|
+
* Tests if an object intersects with this helper's bounding box
|
32
|
+
* @param obj The object to test for intersection
|
33
|
+
* @returns True if objects intersect, false if not, undefined if the provided object is invalid
|
34
|
+
*/
|
24
35
|
public isInBox(obj: Object3D): boolean | undefined {
|
25
36
|
if (!obj) return undefined;
|
26
37
|
|
@@ -44,11 +55,21 @@
|
|
44
55
|
return intersects;
|
45
56
|
}
|
46
57
|
|
58
|
+
/**
|
59
|
+
* Tests if this helper's bounding box intersects with another box
|
60
|
+
* @param box The {@link Box3} to test for intersection
|
61
|
+
* @returns True if boxes intersect, false otherwise
|
62
|
+
*/
|
47
63
|
public intersects(box: Box3): boolean {
|
48
64
|
if (!box) return false;
|
49
65
|
return this.updateBox(false).intersectsBox(box);
|
50
66
|
}
|
51
67
|
|
68
|
+
/**
|
69
|
+
* Updates the helper's bounding box based on the gameObject's position and scale
|
70
|
+
* @param force Whether to force an update regardless of frame count
|
71
|
+
* @returns The updated {@link Box3}
|
72
|
+
*/
|
52
73
|
public updateBox(force: boolean = false): Box3 {
|
53
74
|
if (!this.box) {
|
54
75
|
this.box = new Box3();
|
@@ -74,6 +95,11 @@
|
|
74
95
|
this.box = null;
|
75
96
|
}
|
76
97
|
|
98
|
+
/**
|
99
|
+
* Creates and displays a visual wireframe representation of this box helper
|
100
|
+
* @param col Optional color for the wireframe. If not provided, uses default color
|
101
|
+
* @param force If true, shows the helper even if gizmos are disabled
|
102
|
+
*/
|
77
103
|
public showHelper(col: ColorRepresentation | null = null, force: boolean = false) {
|
78
104
|
if (!gizmos && !force) return;
|
79
105
|
if (this._helper) {
|
@@ -13,8 +13,11 @@
|
|
13
13
|
import { Behaviour, GameObject } from "./Component.js";
|
14
14
|
import { OrbitControls } from "./OrbitControls.js";
|
15
15
|
|
16
|
-
/**
|
16
|
+
/**
|
17
|
+
* The ClearFlags enum is used to determine how the camera clears the background
|
18
|
+
*/
|
17
19
|
export enum ClearFlags {
|
20
|
+
/** Don't clear the background */
|
18
21
|
None = 0,
|
19
22
|
/** Clear the background with a skybox */
|
20
23
|
Skybox = 1,
|
@@ -28,16 +31,28 @@
|
|
28
31
|
const debugscreenpointtoray = getParam("debugscreenpointtoray");
|
29
32
|
|
30
33
|
/**
|
34
|
+
* Camera component that handles rendering from a specific viewpoint in the scene.
|
35
|
+
* Supports both perspective and orthographic cameras with various rendering options.
|
36
|
+
* Internally, this component uses {@link PerspectiveCamera} and {@link OrthographicCamera} three.js objects.
|
37
|
+
*
|
31
38
|
* @category Camera Controls
|
32
39
|
* @group Components
|
33
40
|
*/
|
34
41
|
export class Camera extends Behaviour implements ICamera {
|
35
42
|
|
43
|
+
/**
|
44
|
+
* Returns whether this component is a camera
|
45
|
+
* @returns {boolean} Always returns true
|
46
|
+
*/
|
36
47
|
get isCamera() {
|
37
48
|
return true;
|
38
49
|
}
|
39
50
|
|
40
|
-
/**
|
51
|
+
/**
|
52
|
+
* Gets or sets the camera's aspect ratio (width divided by height).
|
53
|
+
* For perspective cameras, this directly affects the camera's projection matrix.
|
54
|
+
* When set, automatically updates the projection matrix.
|
55
|
+
*/
|
41
56
|
get aspect(): number {
|
42
57
|
if (this._cam instanceof PerspectiveCamera) return this._cam.aspect;
|
43
58
|
return (this.context.domWidth / this.context.domHeight);
|
@@ -51,7 +66,11 @@
|
|
51
66
|
}
|
52
67
|
}
|
53
68
|
}
|
54
|
-
|
69
|
+
|
70
|
+
/**
|
71
|
+
* Gets or sets the camera's field of view in degrees for perspective cameras.
|
72
|
+
* When set, automatically updates the projection matrix.
|
73
|
+
*/
|
55
74
|
get fieldOfView(): number | undefined {
|
56
75
|
if (this._cam instanceof PerspectiveCamera) {
|
57
76
|
return this._cam.fov;
|
@@ -74,7 +93,11 @@
|
|
74
93
|
}
|
75
94
|
}
|
76
95
|
|
77
|
-
/**
|
96
|
+
/**
|
97
|
+
* Gets or sets the camera's near clipping plane distance.
|
98
|
+
* Objects closer than this distance won't be rendered.
|
99
|
+
* When set, automatically updates the projection matrix.
|
100
|
+
*/
|
78
101
|
get nearClipPlane(): number { return this._nearClipPlane; }
|
79
102
|
@serializable()
|
80
103
|
set nearClipPlane(val) {
|
@@ -87,7 +110,11 @@
|
|
87
110
|
}
|
88
111
|
private _nearClipPlane: number = 0.1;
|
89
112
|
|
90
|
-
/**
|
113
|
+
/**
|
114
|
+
* Gets or sets the camera's far clipping plane distance.
|
115
|
+
* Objects farther than this distance won't be rendered.
|
116
|
+
* When set, automatically updates the projection matrix.
|
117
|
+
*/
|
91
118
|
get farClipPlane(): number { return this._farClipPlane; }
|
92
119
|
@serializable()
|
93
120
|
set farClipPlane(val) {
|
@@ -101,7 +128,8 @@
|
|
101
128
|
private _farClipPlane: number = 1000;
|
102
129
|
|
103
130
|
/**
|
104
|
-
*
|
131
|
+
* Applies both the camera's near and far clipping planes and updates the projection matrix.
|
132
|
+
* This ensures rendering occurs only within the specified distance range.
|
105
133
|
*/
|
106
134
|
applyClippingPlane() {
|
107
135
|
if (this._cam) {
|
@@ -111,7 +139,10 @@
|
|
111
139
|
}
|
112
140
|
}
|
113
141
|
|
114
|
-
/**
|
142
|
+
/**
|
143
|
+
* Gets or sets the camera's clear flags that determine how the background is rendered.
|
144
|
+
* Options include skybox, solid color, or transparent background.
|
145
|
+
*/
|
115
146
|
@serializable()
|
116
147
|
public get clearFlags(): ClearFlags {
|
117
148
|
return this._clearFlags;
|
@@ -121,18 +152,31 @@
|
|
121
152
|
this._clearFlags = val;
|
122
153
|
this.applyClearFlagsIfIsActiveCamera();
|
123
154
|
}
|
155
|
+
|
156
|
+
/**
|
157
|
+
* Determines if the camera should use orthographic projection instead of perspective.
|
158
|
+
*/
|
124
159
|
@serializable()
|
125
160
|
public orthographic: boolean = false;
|
161
|
+
|
162
|
+
/**
|
163
|
+
* The size of the orthographic camera's view volume when in orthographic mode.
|
164
|
+
* Larger values show more of the scene.
|
165
|
+
*/
|
126
166
|
@serializable()
|
127
167
|
public orthographicSize: number = 5;
|
128
168
|
|
169
|
+
/**
|
170
|
+
* Controls the transparency level of the camera background in AR mode on supported devices.
|
171
|
+
* Value from 0 (fully transparent) to 1 (fully opaque).
|
172
|
+
*/
|
129
173
|
@serializable()
|
130
174
|
public ARBackgroundAlpha: number = 0;
|
131
175
|
|
132
176
|
/**
|
133
|
-
*
|
134
|
-
*
|
135
|
-
|
177
|
+
* Gets or sets the layers mask that determines which objects this camera will render.
|
178
|
+
* Uses the {@link https://threejs.org/docs/#api/en/core/Layers.mask|three.js layers mask} convention.
|
179
|
+
*/
|
136
180
|
@serializable()
|
137
181
|
public set cullingMask(val: number) {
|
138
182
|
this._cullingMask = val;
|
@@ -146,14 +190,19 @@
|
|
146
190
|
}
|
147
191
|
private _cullingMask: number = 0xffffffff;
|
148
192
|
|
149
|
-
/**
|
150
|
-
*
|
151
|
-
*
|
193
|
+
/**
|
194
|
+
* Sets only a specific layer to be active for rendering by this camera.
|
195
|
+
* This is equivalent to calling `layers.set(val)` on the three.js camera object.
|
196
|
+
* @param val The layer index to set active
|
197
|
+
*/
|
152
198
|
public set cullingLayer(val: number) {
|
153
199
|
this.cullingMask = (1 << val | 0) >>> 0;
|
154
200
|
}
|
155
201
|
|
156
|
-
/**
|
202
|
+
/**
|
203
|
+
* Gets or sets the blurriness of the skybox background.
|
204
|
+
* Values range from 0 (sharp) to 1 (maximum blur).
|
205
|
+
*/
|
157
206
|
@serializable()
|
158
207
|
public set backgroundBlurriness(val: number | undefined) {
|
159
208
|
if (val === this._backgroundBlurriness) return;
|
@@ -168,7 +217,10 @@
|
|
168
217
|
}
|
169
218
|
private _backgroundBlurriness?: number = undefined;
|
170
219
|
|
171
|
-
/**
|
220
|
+
/**
|
221
|
+
* Gets or sets the intensity of the skybox background.
|
222
|
+
* Values range from 0 (dark) to 10 (very bright).
|
223
|
+
*/
|
172
224
|
@serializable()
|
173
225
|
public set backgroundIntensity(val: number | undefined) {
|
174
226
|
if (val === this._backgroundIntensity) return;
|
@@ -183,7 +235,10 @@
|
|
183
235
|
}
|
184
236
|
private _backgroundIntensity?: number = undefined;
|
185
237
|
|
186
|
-
/**
|
238
|
+
/**
|
239
|
+
* Gets or sets the rotation of the skybox background.
|
240
|
+
* Controls the orientation of the environment map.
|
241
|
+
*/
|
187
242
|
@serializable(Euler)
|
188
243
|
public set backgroundRotation(val: Euler | undefined) {
|
189
244
|
if (val === this._backgroundRotation) return;
|
@@ -198,7 +253,10 @@
|
|
198
253
|
}
|
199
254
|
private _backgroundRotation?: Euler = undefined;
|
200
255
|
|
201
|
-
/**
|
256
|
+
/**
|
257
|
+
* Gets or sets the intensity of the environment lighting.
|
258
|
+
* Controls how strongly the environment map affects scene lighting.
|
259
|
+
*/
|
202
260
|
@serializable()
|
203
261
|
public set environmentIntensity(val: number | undefined) {
|
204
262
|
this._environmentIntensity = val;
|
@@ -208,7 +266,10 @@
|
|
208
266
|
}
|
209
267
|
private _environmentIntensity?: number = undefined;
|
210
268
|
|
211
|
-
/**
|
269
|
+
/**
|
270
|
+
* Gets or sets the background color of the camera when {@link ClearFlags} is set to {@link ClearFlags.SolidColor}.
|
271
|
+
* The alpha component controls transparency.
|
272
|
+
*/
|
212
273
|
@serializable(RGBAColor)
|
213
274
|
public get backgroundColor(): RGBAColor | null {
|
214
275
|
return this._backgroundColor ?? null;
|
@@ -225,9 +286,10 @@
|
|
225
286
|
this.applyClearFlagsIfIsActiveCamera();
|
226
287
|
}
|
227
288
|
|
228
|
-
/**
|
229
|
-
*
|
230
|
-
|
289
|
+
/**
|
290
|
+
* Gets or sets the texture that the camera should render to instead of the screen.
|
291
|
+
* Useful for creating effects like mirrors, portals or custom post processing.
|
292
|
+
*/
|
231
293
|
@serializable(RenderTexture)
|
232
294
|
public set targetTexture(rt: RenderTexture | null) {
|
233
295
|
this._targetTexture = rt;
|
@@ -244,16 +306,17 @@
|
|
244
306
|
private _skybox?: CameraSkybox;
|
245
307
|
|
246
308
|
/**
|
247
|
-
*
|
248
|
-
* @returns {PerspectiveCamera | OrthographicCamera}
|
249
|
-
* @deprecated
|
309
|
+
* Gets the three.js camera object. Creates one if it doesn't exist yet.
|
310
|
+
* @returns {PerspectiveCamera | OrthographicCamera} The three.js camera object
|
311
|
+
* @deprecated Use {@link threeCamera} instead
|
250
312
|
*/
|
251
313
|
public get cam(): PerspectiveCamera | OrthographicCamera {
|
252
314
|
return this.threeCamera;
|
253
315
|
}
|
316
|
+
|
254
317
|
/**
|
255
|
-
*
|
256
|
-
* @returns {PerspectiveCamera | OrthographicCamera}
|
318
|
+
* Gets the three.js camera object. Creates one if it doesn't exist yet.
|
319
|
+
* @returns {PerspectiveCamera | OrthographicCamera} The three.js camera object
|
257
320
|
*/
|
258
321
|
public get threeCamera(): PerspectiveCamera | OrthographicCamera {
|
259
322
|
if (this.activeAndEnabled)
|
@@ -263,6 +326,16 @@
|
|
263
326
|
|
264
327
|
private static _origin: Vector3 = new Vector3();
|
265
328
|
private static _direction: Vector3 = new Vector3();
|
329
|
+
|
330
|
+
/**
|
331
|
+
* Converts screen coordinates to a ray in world space.
|
332
|
+
* Useful for implementing picking or raycasting from screen to world.
|
333
|
+
*
|
334
|
+
* @param x The x screen coordinate
|
335
|
+
* @param y The y screen coordinate
|
336
|
+
* @param ray Optional ray object to reuse instead of creating a new one
|
337
|
+
* @returns {Ray} A ray originating from the camera position pointing through the screen point
|
338
|
+
*/
|
266
339
|
public screenPointToRay(x: number, y: number, ray?: Ray): Ray {
|
267
340
|
const cam = this.threeCamera;
|
268
341
|
const origin = Camera._origin;
|
@@ -285,9 +358,12 @@
|
|
285
358
|
}
|
286
359
|
|
287
360
|
private _frustum?: Frustum;
|
361
|
+
|
288
362
|
/**
|
289
|
-
*
|
290
|
-
*
|
363
|
+
* Gets the camera's view frustum for culling and visibility checks.
|
364
|
+
* Creates the frustum if it doesn't exist and returns it.
|
365
|
+
*
|
366
|
+
* @returns {Frustum} The camera's view frustum
|
291
367
|
*/
|
292
368
|
public getFrustum(): Frustum {
|
293
369
|
if (!this._frustum) {
|
@@ -296,13 +372,22 @@
|
|
296
372
|
}
|
297
373
|
return this._frustum;
|
298
374
|
}
|
299
|
-
|
375
|
+
|
376
|
+
/**
|
377
|
+
* Forces an update of the camera's frustum.
|
378
|
+
* This is automatically called every frame in onBeforeRender.
|
379
|
+
*/
|
300
380
|
public updateFrustum() {
|
301
381
|
if (!this._frustum) this._frustum = new Frustum();
|
302
382
|
this._frustum.setFromProjectionMatrix(this.getProjectionScreenMatrix(this._projScreenMatrix, true), this.context.renderer.coordinateSystem);
|
303
383
|
}
|
384
|
+
|
304
385
|
/**
|
305
|
-
*
|
386
|
+
* Gets this camera's projection-screen matrix.
|
387
|
+
*
|
388
|
+
* @param target Matrix4 object to store the result in
|
389
|
+
* @param forceUpdate Whether to force recalculation of the matrix
|
390
|
+
* @returns {Matrix4} The requested projection screen matrix
|
306
391
|
*/
|
307
392
|
public getProjectionScreenMatrix(target: Matrix4, forceUpdate?: boolean) {
|
308
393
|
if (forceUpdate) {
|
@@ -313,7 +398,6 @@
|
|
313
398
|
}
|
314
399
|
private readonly _projScreenMatrix = new Matrix4();
|
315
400
|
|
316
|
-
|
317
401
|
/** @internal */
|
318
402
|
awake() {
|
319
403
|
if (debugscreenpointtoray) {
|
@@ -382,8 +466,9 @@
|
|
382
466
|
}
|
383
467
|
|
384
468
|
/**
|
385
|
-
* Creates a
|
386
|
-
*
|
469
|
+
* Creates a three.js camera object if it doesn't exist yet and sets its properties.
|
470
|
+
* This is called internally when accessing the {@link threeCamera} property.
|
471
|
+
*/
|
387
472
|
buildCamera() {
|
388
473
|
if (this._cam) return;
|
389
474
|
|
@@ -428,13 +513,21 @@
|
|
428
513
|
}
|
429
514
|
}
|
430
515
|
|
516
|
+
/**
|
517
|
+
* Applies clear flags if this is the active main camera.
|
518
|
+
* @param opts Options for applying clear flags
|
519
|
+
*/
|
431
520
|
applyClearFlagsIfIsActiveCamera(opts?: { applySkybox: boolean }) {
|
432
521
|
if (this.context.mainCameraComponent === this) {
|
433
522
|
this.applyClearFlags(opts);
|
434
523
|
}
|
435
524
|
}
|
436
525
|
|
437
|
-
/**
|
526
|
+
/**
|
527
|
+
* Applies this camera's clear flags and related settings to the renderer.
|
528
|
+
* This controls how the background is rendered (skybox, solid color, transparent).
|
529
|
+
* @param opts Options for applying clear flags
|
530
|
+
*/
|
438
531
|
applyClearFlags(opts?: { applySkybox: boolean }) {
|
439
532
|
if (!this._cam) {
|
440
533
|
if (debug) console.log("Camera does not exist (apply clear flags)")
|
@@ -503,7 +596,7 @@
|
|
503
596
|
}
|
504
597
|
|
505
598
|
/**
|
506
|
-
*
|
599
|
+
* Applies the skybox texture to the scene background.
|
507
600
|
*/
|
508
601
|
applySceneSkybox() {
|
509
602
|
if (!this._skybox)
|
@@ -511,9 +604,12 @@
|
|
511
604
|
this._skybox.apply();
|
512
605
|
}
|
513
606
|
|
514
|
-
/**
|
515
|
-
*
|
516
|
-
*
|
607
|
+
/**
|
608
|
+
* Determines if the background should be transparent when in passthrough AR mode.
|
609
|
+
*
|
610
|
+
* @param context The current rendering context
|
611
|
+
* @returns {boolean} True when in XR on a pass through device where the background should be invisible
|
612
|
+
*/
|
517
613
|
static backgroundShouldBeTransparent(context: Context) {
|
518
614
|
const session = context.renderer.xr?.getSession();
|
519
615
|
if (!session) return false;
|
@@ -543,7 +639,10 @@
|
|
543
639
|
}
|
544
640
|
}
|
545
641
|
|
546
|
-
|
642
|
+
/**
|
643
|
+
* Helper class for managing skybox textures for cameras.
|
644
|
+
* Handles retrieving and applying skybox textures to the scene.
|
645
|
+
*/
|
547
646
|
class CameraSkybox {
|
548
647
|
|
549
648
|
private _camera: Camera;
|
@@ -555,6 +654,10 @@
|
|
555
654
|
this._camera = camera;
|
556
655
|
}
|
557
656
|
|
657
|
+
/**
|
658
|
+
* Applies the skybox texture to the scene background.
|
659
|
+
* Retrieves the texture based on the camera's source ID.
|
660
|
+
*/
|
558
661
|
apply() {
|
559
662
|
this._skybox = this.context.lightmaps.tryGetSkybox(this._camera.sourceId) as Texture;
|
560
663
|
if (!this._skybox) {
|
@@ -572,13 +675,16 @@
|
|
572
675
|
}
|
573
676
|
}
|
574
677
|
|
575
|
-
|
678
|
+
/**
|
679
|
+
* Adds orbit controls to the camera if the freecam URL parameter is enabled.
|
680
|
+
*
|
681
|
+
* @param cam The camera to potentially add orbit controls to
|
682
|
+
*/
|
576
683
|
function handleFreeCam(cam: Camera) {
|
577
684
|
const isFreecam = getParam("freecam");
|
578
685
|
if (isFreecam) {
|
579
686
|
if (cam.context.mainCameraComponent === cam) {
|
580
687
|
GameObject.getOrAddComponent(cam.gameObject, OrbitControls);
|
581
688
|
}
|
582
|
-
|
583
689
|
}
|
584
690
|
}
|
@@ -13,6 +13,13 @@
|
|
13
13
|
|
14
14
|
const debug = getParam("debugmissingcamera");
|
15
15
|
|
16
|
+
/**
|
17
|
+
* Handler for missing camera events. Creates a default fallback camera when no camera is found in the scene.
|
18
|
+
* Sets up camera properties based on the context and HTML element attributes.
|
19
|
+
*
|
20
|
+
* @param evt The context event containing scene and configuration information
|
21
|
+
* @returns The created camera component
|
22
|
+
*/
|
16
23
|
ContextRegistry.registerCallback(ContextEvent.MissingCamera, (evt) => {
|
17
24
|
if (debug) console.warn("Creating missing camera")
|
18
25
|
const scene = evt.context.scene;
|
@@ -57,6 +64,12 @@
|
|
57
64
|
return cam;
|
58
65
|
});
|
59
66
|
|
67
|
+
/**
|
68
|
+
* Handler for context creation events. Checks if camera controls should be added
|
69
|
+
* to the main camera when the context is created.
|
70
|
+
*
|
71
|
+
* @param evt The context creation event containing the context information
|
72
|
+
*/
|
60
73
|
ContextRegistry.registerCallback(ContextEvent.ContextCreated, (evt) => {
|
61
74
|
if (!evt.context.mainCamera) {
|
62
75
|
if (debug) console.log("Will not auto-fit because a default camera exists");
|
@@ -77,6 +90,13 @@
|
|
77
90
|
}
|
78
91
|
})
|
79
92
|
|
93
|
+
/**
|
94
|
+
* Creates default orbit camera controls for the specified camera.
|
95
|
+
* Configures auto-rotation and auto-fit settings based on HTML attributes.
|
96
|
+
*
|
97
|
+
* @param context The rendering context
|
98
|
+
* @param cam Optional camera component to attach controls to (uses main camera if not specified)
|
99
|
+
*/
|
80
100
|
function createDefaultCameraControls(context: IContext, cam?: ICamera) {
|
81
101
|
|
82
102
|
cam = cam ?? context.mainCameraComponent;
|
@@ -141,14 +141,14 @@
|
|
141
141
|
}
|
142
142
|
|
143
143
|
start() {
|
144
|
-
this.
|
144
|
+
this.applyRenderSettings();
|
145
145
|
}
|
146
146
|
|
147
147
|
onEnable() {
|
148
148
|
super.onEnable();
|
149
149
|
this._updateRenderSettingsRoutine = undefined;
|
150
150
|
this._lastMatrixWorld = new Matrix4();
|
151
|
-
this.
|
151
|
+
this.applyRenderSettings();
|
152
152
|
document.addEventListener("resize", this._boundRenderSettingsChanged);
|
153
153
|
// We want to run AFTER all regular onBeforeRender callbacks
|
154
154
|
this.context.pre_render_callbacks.push(this.onBeforeRenderRoutine);
|
@@ -1,59 +1,63 @@
|
|
1
1
|
import { BufferGeometry, Group, Mesh, Object3D, Vector3 } from "three"
|
2
2
|
|
3
|
+
import { isDevEnvironment } from "../engine/debug/index.js";
|
3
4
|
import { addComponent } from "../engine/engine_components.js";
|
4
5
|
import { Gizmos } from "../engine/engine_gizmos.js";
|
5
6
|
import type { PhysicsMaterial } from "../engine/engine_physics.types.js";
|
6
7
|
import { serializable } from "../engine/engine_serialization_decorator.js";
|
7
|
-
import { getBoundingBox
|
8
|
-
// import { IColliderProvider, registerColliderProvider } from "../engine/engine_physics.js";
|
8
|
+
import { getBoundingBox } from "../engine/engine_three_utils.js";
|
9
9
|
import type { IBoxCollider, ICollider, ISphereCollider } from "../engine/engine_types.js";
|
10
10
|
import { validate } from "../engine/engine_util_decorator.js";
|
11
|
-
import { unwatchWrite, watchWrite } from "../engine/engine_utils.js";
|
11
|
+
import { getParam, unwatchWrite, watchWrite } from "../engine/engine_utils.js";
|
12
12
|
import { NEEDLE_progressive } from "../engine/extensions/NEEDLE_progressive.js";
|
13
13
|
import { Behaviour } from "./Component.js";
|
14
14
|
import { Rigidbody } from "./RigidBody.js";
|
15
15
|
|
16
16
|
/**
|
17
17
|
* Collider is the base class for all colliders. A collider is a physical shape that is used to detect collisions with other objects in the scene.
|
18
|
-
* Colliders are used in combination with a Rigidbody to create physical interactions between objects.
|
18
|
+
* Colliders are used in combination with a {@link Rigidbody} to create physical interactions between objects.
|
19
19
|
* Colliders are registered with the physics engine when they are enabled and removed when they are disabled.
|
20
20
|
* @category Physics
|
21
21
|
* @group Components
|
22
22
|
*/
|
23
23
|
export class Collider extends Behaviour implements ICollider {
|
24
24
|
|
25
|
-
/**
|
25
|
+
/**
|
26
|
+
* Identifies this component as a collider.
|
27
|
+
* @internal
|
28
|
+
*/
|
26
29
|
get isCollider(): any {
|
27
30
|
return true;
|
28
31
|
}
|
29
32
|
|
30
33
|
/**
|
31
|
-
* The Rigidbody that this collider is attached to.
|
34
|
+
* The {@link Rigidbody} that this collider is attached to. This handles the physics simulation for this collider.
|
32
35
|
*/
|
33
36
|
@serializable(Rigidbody)
|
34
37
|
attachedRigidbody: Rigidbody | null = null;
|
35
38
|
|
36
39
|
/**
|
37
40
|
* When `true` the collider will not be used for collision detection but will still trigger events.
|
41
|
+
* Trigger colliders can trigger events when other colliders enter their space, without creating a physical response/collision.
|
38
42
|
*/
|
39
43
|
@serializable()
|
40
44
|
isTrigger: boolean = false;
|
41
45
|
|
42
46
|
/**
|
43
|
-
* The physics material that
|
47
|
+
* The physics material that defines physical properties of the collider such as friction and bounciness.
|
44
48
|
*/
|
45
49
|
@serializable()
|
46
50
|
sharedMaterial?: PhysicsMaterial;
|
47
51
|
|
48
52
|
/**
|
49
|
-
* The layers that
|
53
|
+
* The layers that this collider belongs to. Used for filtering collision detection.
|
54
|
+
* @default [0]
|
50
55
|
*/
|
51
56
|
@serializable()
|
52
57
|
membership: number[] = [0];
|
53
58
|
|
54
59
|
/**
|
55
|
-
* The layers that
|
56
|
-
* @inheritdoc
|
60
|
+
* The layers that this collider will interact with. Used for filtering collision detection.
|
57
61
|
*/
|
58
62
|
@serializable()
|
59
63
|
filter?: number[];
|
@@ -80,62 +84,91 @@
|
|
80
84
|
this.context.physics.engine?.removeBody(this);
|
81
85
|
}
|
82
86
|
|
83
|
-
/**
|
87
|
+
/**
|
88
|
+
* Returns the underlying physics body from the physics engine.
|
89
|
+
* Only available if the component is enabled and active in the scene.
|
90
|
+
*/
|
84
91
|
get body() {
|
85
92
|
return this.context.physics.engine?.getBody(this);
|
86
93
|
}
|
87
94
|
|
88
95
|
/**
|
89
|
-
*
|
96
|
+
* Updates the collider's properties in the physics engine.
|
97
|
+
* Use this when you've changed collider properties and need to sync with the physics engine.
|
90
98
|
*/
|
91
99
|
updateProperties = () => {
|
92
100
|
this.context.physics.engine?.updateProperties(this);
|
93
101
|
}
|
94
102
|
|
95
|
-
/**
|
103
|
+
/**
|
104
|
+
* Updates the physics material in the physics engine.
|
105
|
+
* Call this after changing the sharedMaterial property.
|
106
|
+
*/
|
96
107
|
updatePhysicsMaterial() {
|
97
108
|
this.context.physics.engine?.updatePhysicsMaterial(this);
|
98
|
-
|
99
109
|
}
|
100
110
|
}
|
101
111
|
|
102
112
|
/**
|
103
|
-
* SphereCollider
|
113
|
+
* SphereCollider represents a sphere-shaped collision volume.
|
114
|
+
* Useful for objects that are roughly spherical in shape or need a simple collision boundary.
|
104
115
|
* @category Physics
|
105
116
|
* @group Components
|
106
117
|
*/
|
107
118
|
export class SphereCollider extends Collider implements ISphereCollider {
|
108
119
|
|
120
|
+
/**
|
121
|
+
* The radius of the sphere collider.
|
122
|
+
*/
|
109
123
|
@validate()
|
110
124
|
@serializable()
|
111
125
|
radius: number = .5;
|
112
126
|
|
127
|
+
/**
|
128
|
+
* The center position of the sphere collider relative to the transform's position.
|
129
|
+
*/
|
113
130
|
@serializable(Vector3)
|
114
131
|
center: Vector3 = new Vector3(0, 0, 0);
|
115
132
|
|
133
|
+
/**
|
134
|
+
* Registers the sphere collider with the physics engine and sets up scale change monitoring.
|
135
|
+
*/
|
116
136
|
onEnable() {
|
117
137
|
super.onEnable();
|
118
138
|
this.context.physics.engine?.addSphereCollider(this);
|
119
139
|
watchWrite(this.gameObject.scale, this.updateProperties);
|
120
140
|
}
|
121
141
|
|
142
|
+
/**
|
143
|
+
* Removes scale change monitoring when the collider is disabled.
|
144
|
+
*/
|
122
145
|
onDisable(): void {
|
123
146
|
super.onDisable();
|
124
147
|
unwatchWrite(this.gameObject.scale, this.updateProperties);
|
125
148
|
}
|
126
149
|
|
150
|
+
/**
|
151
|
+
* Updates collider properties when validated in the editor or inspector.
|
152
|
+
*/
|
127
153
|
onValidate(): void {
|
128
154
|
this.updateProperties();
|
129
155
|
}
|
130
156
|
}
|
131
157
|
|
132
158
|
/**
|
133
|
-
* BoxCollider
|
159
|
+
* BoxCollider represents a box-shaped collision volume.
|
160
|
+
* Ideal for rectangular objects or objects that need a simple cuboid collision boundary.
|
134
161
|
* @category Physics
|
135
162
|
* @group Components
|
136
163
|
*/
|
137
164
|
export class BoxCollider extends Collider implements IBoxCollider {
|
138
165
|
|
166
|
+
/**
|
167
|
+
* Creates and adds a BoxCollider to the given object.
|
168
|
+
* @param obj The object to add the collider to
|
169
|
+
* @param opts Configuration options for the collider and optional rigidbody
|
170
|
+
* @returns The newly created BoxCollider
|
171
|
+
*/
|
139
172
|
static add(obj: Mesh | Object3D, opts?: { rigidbody: boolean, debug?: boolean }) {
|
140
173
|
const collider = addComponent(obj, BoxCollider);
|
141
174
|
collider.autoFit();
|
@@ -146,31 +179,51 @@
|
|
146
179
|
return collider;
|
147
180
|
}
|
148
181
|
|
182
|
+
/**
|
183
|
+
* The size of the box collider along each axis.
|
184
|
+
*/
|
149
185
|
@validate()
|
150
186
|
@serializable(Vector3)
|
151
187
|
size: Vector3 = new Vector3(1, 1, 1);
|
152
188
|
|
189
|
+
/**
|
190
|
+
* The center position of the box collider relative to the transform's position.
|
191
|
+
*/
|
153
192
|
@serializable(Vector3)
|
154
193
|
center: Vector3 = new Vector3(0, 0, 0);
|
155
194
|
|
156
|
-
/**
|
195
|
+
/**
|
196
|
+
* Registers the box collider with the physics engine and sets up scale change monitoring.
|
197
|
+
* @internal
|
198
|
+
*/
|
157
199
|
onEnable() {
|
158
200
|
super.onEnable();
|
159
201
|
this.context.physics.engine?.addBoxCollider(this, this.size);
|
160
202
|
watchWrite(this.gameObject.scale, this.updateProperties);
|
161
203
|
}
|
162
204
|
|
163
|
-
/**
|
205
|
+
/**
|
206
|
+
* Removes scale change monitoring when the collider is disabled.
|
207
|
+
* @internal
|
208
|
+
*/
|
164
209
|
onDisable(): void {
|
165
210
|
super.onDisable();
|
166
211
|
unwatchWrite(this.gameObject.scale, this.updateProperties);
|
167
212
|
}
|
168
213
|
|
169
|
-
/**
|
214
|
+
/**
|
215
|
+
* Updates collider properties when validated in the editor or inspector.
|
216
|
+
* @internal
|
217
|
+
*/
|
170
218
|
onValidate(): void {
|
171
219
|
this.updateProperties();
|
172
220
|
}
|
173
221
|
|
222
|
+
/**
|
223
|
+
* Automatically fits the collider to the geometry of the object.
|
224
|
+
* Sets the size and center based on the object's bounding box.
|
225
|
+
* @param opts Options object with a debug flag to visualize the bounding box
|
226
|
+
*/
|
174
227
|
autoFit(opts?: { debug?: boolean }) {
|
175
228
|
const obj = this.gameObject;
|
176
229
|
|
@@ -205,24 +258,31 @@
|
|
205
258
|
}
|
206
259
|
|
207
260
|
/**
|
208
|
-
* MeshCollider
|
209
|
-
*
|
261
|
+
* MeshCollider creates a collision shape from a mesh geometry.
|
262
|
+
* Allows for complex collision shapes that match the exact geometry of an object.
|
210
263
|
* @category Physics
|
211
264
|
* @group Components
|
212
265
|
*/
|
213
266
|
export class MeshCollider extends Collider {
|
214
267
|
|
215
268
|
/**
|
216
|
-
* The mesh that is used
|
269
|
+
* The mesh that is used to create the collision shape.
|
270
|
+
* If not set, the collider will try to use the mesh of the object it's attached to.
|
217
271
|
*/
|
218
272
|
@serializable(Mesh)
|
219
273
|
sharedMesh?: Mesh;
|
220
274
|
|
221
|
-
/**
|
222
|
-
*
|
275
|
+
/**
|
276
|
+
* When `true` the collider is treated as a solid object without holes.
|
277
|
+
* Set to `false` if you want this mesh collider to be able to contain other objects.
|
278
|
+
*/
|
223
279
|
@serializable()
|
224
280
|
convex: boolean = false;
|
225
281
|
|
282
|
+
/**
|
283
|
+
* Creates and registers the mesh collider with the physics engine.
|
284
|
+
* Handles both individual meshes and mesh groups.
|
285
|
+
*/
|
226
286
|
onEnable() {
|
227
287
|
super.onEnable();
|
228
288
|
if (!this.context.physics.engine) return;
|
@@ -272,29 +332,44 @@
|
|
272
332
|
});
|
273
333
|
}
|
274
334
|
else {
|
275
|
-
|
335
|
+
if (isDevEnvironment() || getParam("showcolliders")) {
|
336
|
+
console.warn(`[MeshCollider] A MeshCollider mesh is assigned to an unknown object on \"${this.gameObject.name}\", but it's neither a Mesh nor a Group. Please double check that you attached the collider component to the right object and report a bug otherwise!`, this);
|
337
|
+
}
|
276
338
|
}
|
277
339
|
}
|
278
340
|
}
|
279
341
|
}
|
280
342
|
|
281
343
|
/**
|
282
|
-
* CapsuleCollider
|
344
|
+
* CapsuleCollider represents a capsule-shaped collision volume (cylinder with hemispherical ends).
|
345
|
+
* Ideal for character controllers and objects that need a rounded collision shape.
|
283
346
|
* @category Physics
|
284
347
|
* @group Components
|
285
348
|
*/
|
286
349
|
export class CapsuleCollider extends Collider {
|
350
|
+
/**
|
351
|
+
* The center position of the capsule collider relative to the transform's position.
|
352
|
+
*/
|
287
353
|
@serializable(Vector3)
|
288
354
|
center: Vector3 = new Vector3(0, 0, 0);
|
289
355
|
|
356
|
+
/**
|
357
|
+
* The radius of the capsule's cylindrical body and hemispherical ends.
|
358
|
+
*/
|
290
359
|
@serializable()
|
291
360
|
radius: number = .5;
|
361
|
+
|
362
|
+
/**
|
363
|
+
* The total height of the capsule including both hemispherical ends.
|
364
|
+
*/
|
292
365
|
@serializable()
|
293
366
|
height: number = 2;
|
294
367
|
|
368
|
+
/**
|
369
|
+
* Registers the capsule collider with the physics engine.
|
370
|
+
*/
|
295
371
|
onEnable() {
|
296
372
|
super.onEnable();
|
297
373
|
this.context.physics.engine?.addCapsuleCollider(this, this.height, this.radius);
|
298
374
|
}
|
299
|
-
|
300
375
|
}
|
@@ -23,57 +23,167 @@
|
|
23
23
|
// }
|
24
24
|
|
25
25
|
/**
|
26
|
-
*
|
27
|
-
*
|
26
|
+
* Base class for objects in Needle Engine. Extends {@link Object3D} from three.js.
|
27
|
+
* GameObjects can have components attached to them, which can be used to add functionality to the object.
|
28
|
+
* They manage their components and provide methods to add, remove and get components.
|
29
|
+
*
|
30
|
+
* All {@link Object3D} types loaded in Needle Engine have methods like {@link addComponent}.
|
31
|
+
* These methods are available directly on the GameObject instance:
|
28
32
|
* ```typescript
|
29
|
-
*
|
33
|
+
* target.addComponent(MyComponent);
|
30
34
|
* ```
|
35
|
+
*
|
36
|
+
* And can be called statically on the GameObject class as well:
|
37
|
+
* ```typescript
|
38
|
+
* GameObject.setActive(target, true);
|
39
|
+
* ```
|
31
40
|
*/
|
32
41
|
export abstract class GameObject extends Object3D implements Object3D, IGameObject {
|
33
42
|
|
34
|
-
/
|
43
|
+
/**
|
44
|
+
* Indicates if the GameObject is currently active. Inactive GameObjects will not be rendered or updated.
|
45
|
+
* When the activeSelf state changes, components will receive {@link Component.onEnable} or {@link Component.onDisable} callbacks.
|
46
|
+
*/
|
35
47
|
abstract activeSelf: boolean;
|
36
48
|
|
37
|
-
/** @deprecated
|
49
|
+
/** @deprecated Use {@link addComponent} instead */
|
38
50
|
// eslint-disable-next-line deprecation/deprecation
|
39
51
|
abstract addNewComponent<T extends IComponent>(type: ConstructorConcrete<T>, init?: ComponentInit<T>): T;
|
40
|
-
|
52
|
+
|
53
|
+
/**
|
54
|
+
* Creates a new component on this gameObject or adds an existing component instance
|
55
|
+
* @param comp Component type constructor or existing component instance
|
56
|
+
* @param init Optional initialization values for the component
|
57
|
+
* @returns The newly created or added component
|
58
|
+
*/
|
41
59
|
abstract addComponent<T extends IComponent>(comp: T | ConstructorConcrete<T>, init?: ComponentInit<T>): T;
|
60
|
+
|
61
|
+
/**
|
62
|
+
* Removes a component from this GameObject
|
63
|
+
* @param comp Component instance to remove
|
64
|
+
* @returns The removed component
|
65
|
+
*/
|
42
66
|
abstract removeComponent<T extends IComponent>(comp: T): T;
|
67
|
+
|
68
|
+
/**
|
69
|
+
* Gets an existing component of the specified type or adds a new one if it doesn't exist
|
70
|
+
* @param typeName Constructor of the component type to get or add
|
71
|
+
* @returns The existing or newly added component
|
72
|
+
*/
|
43
73
|
abstract getOrAddComponent<T>(typeName: ConstructorConcrete<T> | null): T;
|
74
|
+
|
75
|
+
/**
|
76
|
+
* Gets a component of the specified type attached to this GameObject
|
77
|
+
* @param type Constructor of the component type to get
|
78
|
+
* @returns The component if found, otherwise null
|
79
|
+
*/
|
44
80
|
abstract getComponent<T>(type: Constructor<T>): T | null;
|
81
|
+
|
82
|
+
/**
|
83
|
+
* Gets all components of the specified type attached to this GameObject
|
84
|
+
* @param type Constructor of the component type to get
|
85
|
+
* @param arr Optional array to populate with the components
|
86
|
+
* @returns Array of components
|
87
|
+
*/
|
45
88
|
abstract getComponents<T>(type: Constructor<T>, arr?: T[]): Array<T>;
|
89
|
+
|
90
|
+
/**
|
91
|
+
* Gets a component of the specified type in this GameObject's children hierarchy
|
92
|
+
* @param type Constructor of the component type to get
|
93
|
+
* @returns The first matching component if found, otherwise null
|
94
|
+
*/
|
46
95
|
abstract getComponentInChildren<T>(type: Constructor<T>): T | null;
|
96
|
+
|
97
|
+
/**
|
98
|
+
* Gets all components of the specified type in this GameObject's children hierarchy
|
99
|
+
* @param type Constructor of the component type to get
|
100
|
+
* @param arr Optional array to populate with the components
|
101
|
+
* @returns Array of components
|
102
|
+
*/
|
47
103
|
abstract getComponentsInChildren<T>(type: Constructor<T>, arr?: T[]): Array<T>;
|
104
|
+
|
105
|
+
/**
|
106
|
+
* Gets a component of the specified type in this GameObject's parent hierarchy
|
107
|
+
* @param type Constructor of the component type to get
|
108
|
+
* @returns The first matching component if found, otherwise null
|
109
|
+
*/
|
48
110
|
abstract getComponentInParent<T>(type: Constructor<T>): T | null;
|
111
|
+
|
112
|
+
/**
|
113
|
+
* Gets all components of the specified type in this GameObject's parent hierarchy
|
114
|
+
* @param type Constructor of the component type to get
|
115
|
+
* @param arr Optional array to populate with the components
|
116
|
+
* @returns Array of components
|
117
|
+
*/
|
49
118
|
abstract getComponentsInParent<T>(type: Constructor<T>, arr?: T[]): Array<T>;
|
50
119
|
|
51
120
|
|
121
|
+
/**
|
122
|
+
* The position of this GameObject in world space
|
123
|
+
*/
|
52
124
|
abstract get worldPosition(): Vector3
|
53
125
|
abstract set worldPosition(val: Vector3);
|
126
|
+
|
127
|
+
/**
|
128
|
+
* The rotation of this GameObject in world space as a quaternion
|
129
|
+
*/
|
54
130
|
abstract set worldQuaternion(val: Quaternion);
|
55
131
|
abstract get worldQuaternion(): Quaternion;
|
132
|
+
|
133
|
+
/**
|
134
|
+
* The rotation of this GameObject in world space in euler angles (degrees)
|
135
|
+
*/
|
56
136
|
abstract set worldRotation(val: Vector3);
|
57
137
|
abstract get worldRotation(): Vector3;
|
138
|
+
|
139
|
+
/**
|
140
|
+
* The scale of this GameObject in world space
|
141
|
+
*/
|
58
142
|
abstract set worldScale(val: Vector3);
|
59
143
|
abstract get worldScale(): Vector3;
|
60
144
|
|
145
|
+
/**
|
146
|
+
* The forward direction vector of this GameObject in world space
|
147
|
+
*/
|
61
148
|
abstract get worldForward(): Vector3;
|
149
|
+
|
150
|
+
/**
|
151
|
+
* The right direction vector of this GameObject in world space
|
152
|
+
*/
|
62
153
|
abstract get worldRight(): Vector3;
|
154
|
+
|
155
|
+
/**
|
156
|
+
* The up direction vector of this GameObject in world space
|
157
|
+
*/
|
63
158
|
abstract get worldUp(): Vector3;
|
64
159
|
|
160
|
+
/**
|
161
|
+
* Unique identifier for this GameObject
|
162
|
+
*/
|
65
163
|
guid: string | undefined;
|
66
164
|
|
67
|
-
/
|
165
|
+
/**
|
166
|
+
* Destroys this GameObject and all its components.
|
167
|
+
* Internally, this is added to the three.js {@link Object3D} prototype.
|
168
|
+
*/
|
68
169
|
abstract destroy();
|
69
170
|
|
70
171
|
|
71
|
-
|
72
|
-
|
172
|
+
/**
|
173
|
+
* Checks if a GameObject has been destroyed
|
174
|
+
* @param go The GameObject to check
|
175
|
+
* @returns True if the GameObject has been destroyed
|
176
|
+
*/
|
73
177
|
public static isDestroyed(go: Object3D): boolean {
|
74
178
|
return isDestroyed(go);
|
75
179
|
}
|
76
180
|
|
181
|
+
/**
|
182
|
+
* Sets the active state of a GameObject
|
183
|
+
* @param go The GameObject to modify
|
184
|
+
* @param active Whether the GameObject should be active
|
185
|
+
* @param processStart Whether to process the start callbacks if being activated
|
186
|
+
*/
|
77
187
|
public static setActive(go: Object3D, active: boolean, processStart: boolean = true) {
|
78
188
|
if (!go) return;
|
79
189
|
setActive(go, active);
|
@@ -84,47 +194,68 @@
|
|
84
194
|
main.processStart(Context.Current, go);
|
85
195
|
}
|
86
196
|
|
87
|
-
/**
|
197
|
+
/**
|
198
|
+
* Checks if the GameObject itself is active (same as go.visible)
|
199
|
+
* @param go The GameObject to check
|
200
|
+
* @returns True if the GameObject is active
|
201
|
+
*/
|
88
202
|
public static isActiveSelf(go: Object3D): boolean {
|
89
203
|
return isActiveSelf(go);
|
90
204
|
}
|
91
205
|
|
92
|
-
/**
|
93
|
-
*
|
94
|
-
|
206
|
+
/**
|
207
|
+
* Checks if the GameObject is active in the hierarchy (e.g. if any parent is invisible or not in the scene it will be false)
|
208
|
+
* @param go The GameObject to check
|
209
|
+
* @returns True if the GameObject is active in the hierarchy
|
210
|
+
*/
|
95
211
|
public static isActiveInHierarchy(go: Object3D): boolean {
|
96
212
|
return isActiveInHierarchy(go);
|
97
213
|
}
|
98
214
|
|
215
|
+
/**
|
216
|
+
* Marks a GameObject to be rendered using instancing
|
217
|
+
* @param go The GameObject to mark
|
218
|
+
* @param instanced Whether the GameObject should use instanced rendering
|
219
|
+
*/
|
99
220
|
public static markAsInstancedRendered(go: Object3D, instanced: boolean) {
|
100
221
|
markAsInstancedRendered(go, instanced);
|
101
222
|
}
|
102
223
|
|
224
|
+
/**
|
225
|
+
* Checks if a GameObject is using instanced rendering
|
226
|
+
* @param instance The GameObject to check
|
227
|
+
* @returns True if the GameObject is using instanced rendering
|
228
|
+
*/
|
103
229
|
public static isUsingInstancing(instance: Object3D): boolean { return isUsingInstancing(instance); }
|
104
230
|
|
105
|
-
/**
|
106
|
-
*
|
107
|
-
* @param
|
108
|
-
* @param
|
109
|
-
* @
|
231
|
+
/**
|
232
|
+
* Executes a callback for all components of the provided type on the provided object and its children
|
233
|
+
* @param instance Object to run the method on
|
234
|
+
* @param cb Callback to run on each component, "return undefined;" to continue and "return <anything>;" to break the loop
|
235
|
+
* @param recursive If true, the method will be run on all children as well
|
236
|
+
* @returns The last return value of the callback
|
110
237
|
*/
|
111
238
|
public static foreachComponent(instance: Object3D, cb: (comp: Component) => any, recursive: boolean = true): any {
|
112
239
|
return foreachComponent(instance, cb as (comp: IComponent) => any, recursive);
|
113
240
|
}
|
114
241
|
|
115
|
-
/**
|
116
|
-
*
|
117
|
-
* @param
|
242
|
+
/**
|
243
|
+
* Creates a new instance of the provided object that will be replicated to all connected clients
|
244
|
+
* @param instance Object to instantiate
|
245
|
+
* @param opts Options for the instantiation
|
246
|
+
* @returns The newly created instance or null if creation failed
|
118
247
|
*/
|
119
248
|
public static instantiateSynced(instance: GameObject | Object3D | null, opts: SyncInstantiateOptions): GameObject | null {
|
120
249
|
if (!instance) return null;
|
121
250
|
return syncInstantiate(instance, opts) as GameObject | null;
|
122
251
|
}
|
123
252
|
|
124
|
-
/**
|
125
|
-
*
|
126
|
-
* @param
|
127
|
-
|
253
|
+
/**
|
254
|
+
* Creates a new instance of the provided object (like cloning it including all components and children)
|
255
|
+
* @param instance Object to instantiate
|
256
|
+
* @param opts Options for the instantiation (e.g. with what parent, position, etc.)
|
257
|
+
* @returns The newly created instance
|
258
|
+
*/
|
128
259
|
public static instantiate(instance: AssetReference, opts?: IInstantiateOptions | null | undefined): Promise<Object3D | null>
|
129
260
|
public static instantiate(instance: GameObject | Object3D, opts?: IInstantiateOptions | null | undefined): GameObject
|
130
261
|
public static instantiate(instance: AssetReference | GameObject | Object3D, opts: IInstantiateOptions | null | undefined = null): GameObject | Promise<Object3D | null> {
|
@@ -134,9 +265,12 @@
|
|
134
265
|
return instantiate(instance as GameObject | Object3D, opts) as GameObject;
|
135
266
|
}
|
136
267
|
|
137
|
-
/**
|
138
|
-
*
|
139
|
-
|
268
|
+
/**
|
269
|
+
* Destroys an object on all connected clients (if in a networked session)
|
270
|
+
* @param instance Object to destroy
|
271
|
+
* @param context Optional context to use
|
272
|
+
* @param recursive If true, all children will be destroyed as well
|
273
|
+
*/
|
140
274
|
public static destroySynced(instance: Object3D | Component, context?: Context, recursive: boolean = true) {
|
141
275
|
if (!instance) return;
|
142
276
|
const go = instance as GameObject;
|
@@ -144,16 +278,20 @@
|
|
144
278
|
syncDestroy(go as any, context.connection, recursive);
|
145
279
|
}
|
146
280
|
|
147
|
-
/**
|
148
|
-
*
|
149
|
-
* @param
|
281
|
+
/**
|
282
|
+
* Destroys an object
|
283
|
+
* @param instance Object to destroy
|
284
|
+
* @param recursive If true, all children will be destroyed as well. Default: true
|
150
285
|
*/
|
151
286
|
public static destroy(instance: Object3D | Component, recursive: boolean = true) {
|
152
287
|
return destroy(instance, recursive);
|
153
288
|
}
|
154
289
|
|
155
290
|
/**
|
156
|
-
*
|
291
|
+
* Adds an object to parent and ensures all components are properly registered
|
292
|
+
* @param instance Object to add
|
293
|
+
* @param parent Parent to add the object to
|
294
|
+
* @param context Optional context to use
|
157
295
|
*/
|
158
296
|
public static add(instance: Object3D | null | undefined, parent: Object3D, context?: Context) {
|
159
297
|
if (!instance || !parent) return;
|
@@ -183,6 +321,7 @@
|
|
183
321
|
|
184
322
|
/**
|
185
323
|
* Removes the object from its parent and deactivates all of its components
|
324
|
+
* @param instance Object to remove
|
186
325
|
*/
|
187
326
|
public static remove(instance: Object3D | null | undefined) {
|
188
327
|
if (!instance) return;
|
@@ -194,14 +333,22 @@
|
|
194
333
|
}, true);
|
195
334
|
}
|
196
335
|
|
197
|
-
/**
|
336
|
+
/**
|
337
|
+
* Invokes a method on all components including children (if a method with that name exists)
|
338
|
+
* @param go GameObject to invoke the method on
|
339
|
+
* @param functionName Name of the method to invoke
|
340
|
+
* @param args Arguments to pass to the method
|
341
|
+
*/
|
198
342
|
public static invokeOnChildren(go: Object3D | null | undefined, functionName: string, ...args: any) {
|
199
343
|
this.invoke(go, functionName, true, args);
|
200
344
|
}
|
201
345
|
|
202
|
-
/**
|
203
|
-
*
|
204
|
-
* @param
|
346
|
+
/**
|
347
|
+
* Invokes a method on all components that have a method matching the provided name
|
348
|
+
* @param go GameObject to invoke the method on
|
349
|
+
* @param functionName Name of the method to invoke
|
350
|
+
* @param children Whether to invoke on children as well
|
351
|
+
* @param args Arguments to pass to the method
|
205
352
|
*/
|
206
353
|
public static invoke(go: Object3D | null | undefined, functionName: string, children: boolean = false, ...args: any) {
|
207
354
|
if (!go) return;
|
@@ -220,38 +367,53 @@
|
|
220
367
|
}
|
221
368
|
|
222
369
|
/**
|
223
|
-
*
|
224
|
-
* @param go
|
225
|
-
* @param instanceOrType
|
226
|
-
* @param init
|
227
|
-
* @param
|
370
|
+
* Adds a new component (or moves an existing component) to the provided object
|
371
|
+
* @param go Object to add the component to
|
372
|
+
* @param instanceOrType If an instance is provided it will be moved to the new object, if a type is provided a new instance will be created
|
373
|
+
* @param init Optional init object to initialize the component with
|
374
|
+
* @param opts Optional options for adding the component
|
375
|
+
* @returns The added or moved component
|
228
376
|
*/
|
229
377
|
public static addComponent<T extends IComponent>(go: IGameObject | Object3D, instanceOrType: T | ConstructorConcrete<T>, init?: ComponentInit<T>, opts?: { callAwake: boolean }): T {
|
230
378
|
return addComponent(go, instanceOrType, init, opts);
|
231
379
|
}
|
232
380
|
|
233
381
|
/**
|
234
|
-
* Moves a component to a new object
|
235
|
-
* @param go
|
236
|
-
* @param instance
|
382
|
+
* Moves a component to a new object
|
383
|
+
* @param go GameObject to move the component to
|
384
|
+
* @param instance Component to move
|
385
|
+
* @returns The moved component
|
237
386
|
*/
|
238
387
|
public static moveComponent<T extends IComponent>(go: IGameObject | Object3D, instance: T | ConstructorConcrete<T>): T {
|
239
388
|
return addComponent(go, instance);
|
240
389
|
}
|
241
390
|
|
242
|
-
/**
|
243
|
-
*
|
391
|
+
/**
|
392
|
+
* Removes a component from its object
|
393
|
+
* @param instance Component to remove
|
394
|
+
* @returns The removed component
|
244
395
|
*/
|
245
396
|
public static removeComponent<T extends IComponent>(instance: T): T {
|
246
397
|
removeComponent(instance.gameObject, instance as any);
|
247
398
|
return instance;
|
248
399
|
}
|
249
400
|
|
401
|
+
/**
|
402
|
+
* Gets or adds a component of the specified type
|
403
|
+
* @param go GameObject to get or add the component to
|
404
|
+
* @param typeName Constructor of the component type
|
405
|
+
* @returns The existing or newly added component
|
406
|
+
*/
|
250
407
|
public static getOrAddComponent<T extends IComponent>(go: IGameObject | Object3D, typeName: ConstructorConcrete<T>): T {
|
251
408
|
return getOrAddComponent<any>(go, typeName);
|
252
409
|
}
|
253
410
|
|
254
|
-
/**
|
411
|
+
/**
|
412
|
+
* Gets a component on the provided object
|
413
|
+
* @param go GameObject to get the component from
|
414
|
+
* @param typeName Constructor of the component type
|
415
|
+
* @returns The component if found, otherwise null
|
416
|
+
*/
|
255
417
|
public static getComponent<T extends IComponent>(go: IGameObject | Object3D | null, typeName: Constructor<T> | null): T | null {
|
256
418
|
if (go === null) return null;
|
257
419
|
// if names are minified we could also use the type store and work with strings everywhere
|
@@ -261,42 +423,99 @@
|
|
261
423
|
return getComponent(go, typeName as any);
|
262
424
|
}
|
263
425
|
|
426
|
+
/**
|
427
|
+
* Gets all components of the specified type on the provided object
|
428
|
+
* @param go GameObject to get the components from
|
429
|
+
* @param typeName Constructor of the component type
|
430
|
+
* @param arr Optional array to populate with the components
|
431
|
+
* @returns Array of components
|
432
|
+
*/
|
264
433
|
public static getComponents<T extends IComponent>(go: IGameObject | Object3D | null, typeName: Constructor<T>, arr: T[] | null = null): T[] {
|
265
434
|
if (go === null) return arr ?? [];
|
266
435
|
return getComponents(go, typeName, arr);
|
267
436
|
}
|
268
437
|
|
438
|
+
/**
|
439
|
+
* Finds an object or component by its unique identifier
|
440
|
+
* @param guid Unique identifier to search for
|
441
|
+
* @param hierarchy Root object to search in
|
442
|
+
* @returns The found GameObject or Component, or null/undefined if not found
|
443
|
+
*/
|
269
444
|
public static findByGuid(guid: string, hierarchy: Object3D): GameObject | Component | null | undefined {
|
270
445
|
const res = findByGuid(guid, hierarchy);
|
271
446
|
return res as GameObject | Component | null | undefined;
|
272
447
|
}
|
273
448
|
|
449
|
+
/**
|
450
|
+
* Finds the first object of the specified component type in the scene
|
451
|
+
* @param typeName Constructor of the component type
|
452
|
+
* @param context Context or root object to search in
|
453
|
+
* @param includeInactive Whether to include inactive objects in the search
|
454
|
+
* @returns The first matching component if found, otherwise null
|
455
|
+
*/
|
274
456
|
public static findObjectOfType<T extends IComponent>(typeName: Constructor<T>, context?: Context | Object3D, includeInactive: boolean = true): T | null {
|
275
457
|
return findObjectOfType(typeName, context ?? Context.Current, includeInactive);
|
276
458
|
}
|
277
459
|
|
460
|
+
/**
|
461
|
+
* Finds all objects of the specified component type in the scene
|
462
|
+
* @param typeName Constructor of the component type
|
463
|
+
* @param context Context or root object to search in
|
464
|
+
* @returns Array of matching components
|
465
|
+
*/
|
278
466
|
public static findObjectsOfType<T extends IComponent>(typeName: Constructor<T>, context?: Context | Object3D): Array<T> {
|
279
467
|
const arr = [];
|
280
468
|
findObjectsOfType(typeName, arr, context);
|
281
469
|
return arr;
|
282
470
|
}
|
283
471
|
|
472
|
+
/**
|
473
|
+
* Gets a component of the specified type in the gameObject's children hierarchy
|
474
|
+
* @param go GameObject to search in
|
475
|
+
* @param typeName Constructor of the component type
|
476
|
+
* @returns The first matching component if found, otherwise null
|
477
|
+
*/
|
284
478
|
public static getComponentInChildren<T extends IComponent>(go: IGameObject | Object3D, typeName: Constructor<T>): T | null {
|
285
479
|
return getComponentInChildren(go, typeName);
|
286
480
|
}
|
287
481
|
|
482
|
+
/**
|
483
|
+
* Gets all components of the specified type in the gameObject's children hierarchy
|
484
|
+
* @param go GameObject to search in
|
485
|
+
* @param typeName Constructor of the component type
|
486
|
+
* @param arr Optional array to populate with the components
|
487
|
+
* @returns Array of components
|
488
|
+
*/
|
288
489
|
public static getComponentsInChildren<T extends IComponent>(go: IGameObject | Object3D, typeName: Constructor<T>, arr: T[] | null = null): Array<T> {
|
289
490
|
return getComponentsInChildren<T>(go, typeName, arr ?? undefined) as T[]
|
290
491
|
}
|
291
492
|
|
493
|
+
/**
|
494
|
+
* Gets a component of the specified type in the gameObject's parent hierarchy
|
495
|
+
* @param go GameObject to search in
|
496
|
+
* @param typeName Constructor of the component type
|
497
|
+
* @returns The first matching component if found, otherwise null
|
498
|
+
*/
|
292
499
|
public static getComponentInParent<T extends IComponent>(go: IGameObject | Object3D, typeName: Constructor<T>): T | null {
|
293
500
|
return getComponentInParent(go, typeName);
|
294
501
|
}
|
295
502
|
|
503
|
+
/**
|
504
|
+
* Gets all components of the specified type in the gameObject's parent hierarchy
|
505
|
+
* @param go GameObject to search in
|
506
|
+
* @param typeName Constructor of the component type
|
507
|
+
* @param arr Optional array to populate with the components
|
508
|
+
* @returns Array of components
|
509
|
+
*/
|
296
510
|
public static getComponentsInParent<T extends IComponent>(go: IGameObject | Object3D, typeName: Constructor<T>, arr: Array<T> | null = null): Array<T> {
|
297
511
|
return getComponentsInParent(go, typeName, arr);
|
298
512
|
}
|
299
513
|
|
514
|
+
/**
|
515
|
+
* Gets all components on the gameObject
|
516
|
+
* @param go GameObject to get components from
|
517
|
+
* @returns Array of all components
|
518
|
+
*/
|
300
519
|
public static getAllComponents(go: IGameObject | Object3D): Component[] {
|
301
520
|
const componentsList = go.userData?.components;
|
302
521
|
if (!componentsList) return [];
|
@@ -304,6 +523,11 @@
|
|
304
523
|
return newList;
|
305
524
|
}
|
306
525
|
|
526
|
+
/**
|
527
|
+
* Iterates through all components on the gameObject
|
528
|
+
* @param go GameObject to iterate components on
|
529
|
+
* @returns Generator yielding each component
|
530
|
+
*/
|
307
531
|
public static *iterateComponents(go: IGameObject | Object3D) {
|
308
532
|
const list = go?.userData?.components;
|
309
533
|
if (list && Array.isArray(list)) {
|
@@ -346,27 +570,43 @@
|
|
346
570
|
Partial<INeedleXRSessionEventReceiver>,
|
347
571
|
Partial<IPointerEventHandler>
|
348
572
|
{
|
349
|
-
/**
|
573
|
+
/**
|
574
|
+
* Indicates whether this object is a component
|
575
|
+
* @internal
|
576
|
+
*/
|
350
577
|
get isComponent(): boolean { return true; }
|
351
578
|
|
352
579
|
private __context: Context | undefined;
|
353
|
-
|
580
|
+
|
581
|
+
/**
|
582
|
+
* The context this component belongs to, providing access to the runtime environment
|
583
|
+
* including physics, timing utilities, camera, and scene
|
584
|
+
*/
|
354
585
|
get context(): Context {
|
355
586
|
return this.__context ?? Context.Current;
|
356
587
|
}
|
357
588
|
set context(context: Context) {
|
358
589
|
this.__context = context;
|
359
590
|
}
|
360
|
-
|
361
|
-
|
591
|
+
|
592
|
+
/**
|
593
|
+
* Shorthand accessor for the current scene from the context
|
594
|
+
* @returns The scene this component belongs to
|
595
|
+
*/
|
362
596
|
get scene(): Scene { return this.context.scene; }
|
363
597
|
|
364
|
-
/**
|
598
|
+
/**
|
599
|
+
* The layer value of the GameObject this component is attached to
|
600
|
+
* Used for visibility and physics filtering
|
601
|
+
*/
|
365
602
|
get layer(): number {
|
366
603
|
return this.gameObject?.userData?.layer;
|
367
604
|
}
|
368
605
|
|
369
|
-
/**
|
606
|
+
/**
|
607
|
+
* The name of the GameObject this component is attached to
|
608
|
+
* Used for debugging and finding objects
|
609
|
+
*/
|
370
610
|
get name(): string {
|
371
611
|
if (this.gameObject?.name) {
|
372
612
|
return this.gameObject.name;
|
@@ -384,7 +624,11 @@
|
|
384
624
|
this.__name = str;
|
385
625
|
}
|
386
626
|
}
|
387
|
-
|
627
|
+
|
628
|
+
/**
|
629
|
+
* The tag of the GameObject this component is attached to
|
630
|
+
* Used for categorizing objects and efficient lookup
|
631
|
+
*/
|
388
632
|
get tag(): string {
|
389
633
|
return this.gameObject?.userData.tag;
|
390
634
|
}
|
@@ -394,7 +638,11 @@
|
|
394
638
|
this.gameObject.userData.tag = str;
|
395
639
|
}
|
396
640
|
}
|
397
|
-
|
641
|
+
|
642
|
+
/**
|
643
|
+
* Indicates whether the GameObject is marked as static
|
644
|
+
* Static objects typically don't move and can be optimized by the engine
|
645
|
+
*/
|
398
646
|
get static() {
|
399
647
|
return this.gameObject?.userData.static;
|
400
648
|
}
|
@@ -408,7 +656,11 @@
|
|
408
656
|
// return this.gameObject?.hideFlags;
|
409
657
|
// }
|
410
658
|
|
411
|
-
/**
|
659
|
+
/**
|
660
|
+
* Checks if this component is currently active (enabled and part of an active GameObject hierarchy)
|
661
|
+
* Components that are inactive won't receive lifecycle method calls
|
662
|
+
* @returns True if the component is enabled and all parent GameObjects are active
|
663
|
+
*/
|
412
664
|
get activeAndEnabled(): boolean {
|
413
665
|
if (this.destroyed) return false;
|
414
666
|
if (this.__isEnabled === false) return false;
|
@@ -426,6 +678,7 @@
|
|
426
678
|
private get __isActive(): boolean {
|
427
679
|
return this.gameObject.visible;
|
428
680
|
}
|
681
|
+
|
429
682
|
private get __isActiveInHierarchy(): boolean {
|
430
683
|
if (!this.gameObject) return false;
|
431
684
|
const res = this.gameObject[activeInHierarchyFieldName];
|
@@ -438,139 +691,286 @@
|
|
438
691
|
this.gameObject[activeInHierarchyFieldName] = val;
|
439
692
|
}
|
440
693
|
|
441
|
-
/**
|
694
|
+
/**
|
695
|
+
* Reference to the GameObject this component is attached to
|
696
|
+
* This is a three.js Object3D with additional GameObject functionality
|
697
|
+
*/
|
442
698
|
gameObject!: GameObject;
|
443
|
-
|
699
|
+
|
700
|
+
/**
|
701
|
+
* Unique identifier for this component instance,
|
702
|
+
* used for finding and tracking components
|
703
|
+
*/
|
444
704
|
guid: string = "invalid";
|
445
|
-
|
705
|
+
|
706
|
+
/**
|
707
|
+
* Identifier for the source asset that created this component.
|
708
|
+
* For example, URL to the glTF file this component was loaded from
|
709
|
+
*/
|
446
710
|
sourceId?: SourceIdentifier;
|
447
|
-
// transform: Object3D = nullObject;
|
448
711
|
|
449
|
-
/**
|
712
|
+
/**
|
713
|
+
* Called when this component needs to remap guids after an instantiate operation.
|
714
|
+
* @param guidsMap Mapping from old guids to newly generated guids
|
715
|
+
*/
|
450
716
|
resolveGuids?(guidsMap: GuidsMap): void;
|
451
717
|
|
452
|
-
/**
|
453
|
-
*
|
718
|
+
/**
|
719
|
+
* Called once when the component becomes active for the first time.
|
720
|
+
* This is the first lifecycle callback to be invoked
|
721
|
+
*/
|
454
722
|
awake() { }
|
455
|
-
|
456
|
-
|
457
|
-
|
723
|
+
|
724
|
+
/**
|
725
|
+
* Called every time the component becomes enabled or active in the hierarchy.
|
726
|
+
* Invoked after {@link awake} and before {@link start}.
|
727
|
+
*/
|
458
728
|
onEnable() { }
|
459
|
-
|
729
|
+
|
730
|
+
/**
|
731
|
+
* Called every time the component becomes disabled or inactive in the hierarchy.
|
732
|
+
* Invoked when the component or any parent GameObject becomes invisible
|
733
|
+
*/
|
460
734
|
onDisable() { }
|
461
|
-
|
735
|
+
|
736
|
+
/**
|
737
|
+
* Called when the component is destroyed.
|
738
|
+
* Use for cleanup operations like removing event listeners
|
739
|
+
*/
|
462
740
|
onDestroy() {
|
463
741
|
this.__destroyed = true;
|
464
742
|
}
|
465
|
-
|
466
|
-
|
743
|
+
|
744
|
+
/**
|
745
|
+
* Called when a field decorated with @validate() is modified.
|
746
|
+
* @param prop The name of the field that was changed
|
467
747
|
*/
|
468
748
|
onValidate?(prop?: string): void;
|
469
749
|
|
470
|
-
/**
|
750
|
+
/**
|
751
|
+
* Called when the context's pause state changes.
|
752
|
+
* @param isPaused Whether the context is currently paused
|
753
|
+
* @param wasPaused The previous pause state
|
754
|
+
*/
|
471
755
|
onPausedChanged?(isPaused: boolean, wasPaused: boolean): void;
|
472
756
|
|
473
|
-
/**
|
757
|
+
/**
|
758
|
+
* Called once at the beginning of the first frame after the component is enabled.
|
759
|
+
* Use for initialization that requires other components to be awake.
|
760
|
+
*/
|
474
761
|
start?(): void;
|
475
|
-
|
762
|
+
|
763
|
+
/**
|
764
|
+
* Called at the beginning of each frame before regular updates.
|
765
|
+
* Use for logic that needs to run before standard update callbacks.
|
766
|
+
*/
|
476
767
|
earlyUpdate?(): void;
|
477
|
-
|
768
|
+
|
769
|
+
/**
|
770
|
+
* Called once per frame during the main update loop.
|
771
|
+
* The primary location for frame-based game logic.
|
772
|
+
*/
|
478
773
|
update?(): void;
|
479
|
-
|
774
|
+
|
775
|
+
/**
|
776
|
+
* Called after all update functions have been called.
|
777
|
+
* Use for calculations that depend on other components being updated first.
|
778
|
+
*/
|
480
779
|
lateUpdate?(): void;
|
481
|
-
|
780
|
+
|
781
|
+
/**
|
782
|
+
* Called immediately before the scene is rendered.
|
783
|
+
* @param frame Current XRFrame if in an XR session, null otherwise
|
784
|
+
*/
|
482
785
|
onBeforeRender?(frame: XRFrame | null): void;
|
483
|
-
|
786
|
+
|
787
|
+
/**
|
788
|
+
* Called after the scene has been rendered.
|
789
|
+
* Use for post-processing or UI updates that should happen after rendering
|
790
|
+
*/
|
484
791
|
onAfterRender?(): void;
|
485
792
|
|
793
|
+
/**
|
794
|
+
* Called when this component's collider begins colliding with another collider.
|
795
|
+
* @param col Information about the collision that occurred
|
796
|
+
*/
|
486
797
|
onCollisionEnter?(col: Collision);
|
798
|
+
|
799
|
+
/**
|
800
|
+
* Called when this component's collider stops colliding with another collider.
|
801
|
+
* @param col Information about the collision that ended
|
802
|
+
*/
|
487
803
|
onCollisionExit?(col: Collision);
|
804
|
+
|
805
|
+
/**
|
806
|
+
* Called each frame while this component's collider is colliding with another collider
|
807
|
+
* @param col Information about the ongoing collision
|
808
|
+
*/
|
488
809
|
onCollisionStay?(col: Collision);
|
489
810
|
|
811
|
+
/**
|
812
|
+
* Called when this component's trigger collider is entered by another collider
|
813
|
+
* @param col The collider that entered this trigger
|
814
|
+
*/
|
490
815
|
onTriggerEnter?(col: ICollider);
|
816
|
+
|
817
|
+
/**
|
818
|
+
* Called each frame while another collider is inside this component's trigger collider
|
819
|
+
* @param col The collider that is inside this trigger
|
820
|
+
*/
|
491
821
|
onTriggerStay?(col: ICollider);
|
822
|
+
|
823
|
+
/**
|
824
|
+
* Called when another collider exits this component's trigger collider
|
825
|
+
* @param col The collider that exited this trigger
|
826
|
+
*/
|
492
827
|
onTriggerExit?(col: ICollider);
|
493
828
|
|
494
|
-
|
495
|
-
|
496
|
-
* @
|
497
|
-
|
829
|
+
/**
|
830
|
+
* Determines if this component supports a specific XR mode
|
831
|
+
* @param mode The XR session mode to check support for
|
832
|
+
* @returns True if the component supports the specified mode
|
833
|
+
*/
|
498
834
|
supportsXR?(mode: XRSessionMode): boolean;
|
499
|
-
|
835
|
+
|
836
|
+
/**
|
837
|
+
* Called before an XR session is requested
|
838
|
+
* Use to modify session initialization parameters
|
839
|
+
* @param mode The XR session mode being requested
|
840
|
+
* @param args The session initialization parameters that can be modified
|
841
|
+
*/
|
500
842
|
onBeforeXR?(mode: XRSessionMode, args: XRSessionInit): void;
|
501
|
-
|
843
|
+
|
844
|
+
/**
|
845
|
+
* Called when this component joins an XR session or becomes active in a running session
|
846
|
+
* @param args Event data for the XR session
|
847
|
+
*/
|
502
848
|
onEnterXR?(args: NeedleXREventArgs): void;
|
503
|
-
|
849
|
+
|
850
|
+
/**
|
851
|
+
* Called each frame while this component is active in an XR session
|
852
|
+
* @param args Event data for the current XR frame
|
853
|
+
*/
|
504
854
|
onUpdateXR?(args: NeedleXREventArgs): void;
|
505
|
-
|
855
|
+
|
856
|
+
/**
|
857
|
+
* Called when this component exits an XR session or becomes inactive during a session
|
858
|
+
* @param args Event data for the XR session
|
859
|
+
*/
|
506
860
|
onLeaveXR?(args: NeedleXREventArgs): void;
|
507
|
-
|
508
|
-
|
509
|
-
*
|
861
|
+
|
862
|
+
/**
|
863
|
+
* Called when an XR controller is connected or when this component becomes active
|
864
|
+
* in a session with existing controllers
|
865
|
+
* @param args Event data for the controller that was added
|
866
|
+
*/
|
510
867
|
onXRControllerAdded?(args: NeedleXRControllerEventArgs): void;
|
511
|
-
|
512
|
-
|
513
|
-
|
868
|
+
|
869
|
+
/**
|
870
|
+
* Called when an XR controller is disconnected or when this component becomes inactive
|
871
|
+
* during a session with controllers
|
872
|
+
* @param args Event data for the controller that was removed
|
873
|
+
*/
|
514
874
|
onXRControllerRemoved?(args: NeedleXRControllerEventArgs): void;
|
515
875
|
|
516
|
-
|
517
|
-
|
518
|
-
|
876
|
+
/**
|
877
|
+
* Called when a pointer enters this component's GameObject
|
878
|
+
* @param args Data about the pointer event
|
879
|
+
*/
|
519
880
|
onPointerEnter?(args: PointerEventData);
|
881
|
+
|
882
|
+
/**
|
883
|
+
* Called when a pointer moves while over this component's GameObject
|
884
|
+
* @param args Data about the pointer event
|
885
|
+
*/
|
520
886
|
onPointerMove?(args: PointerEventData);
|
887
|
+
|
888
|
+
/**
|
889
|
+
* Called when a pointer exits this component's GameObject
|
890
|
+
* @param args Data about the pointer event
|
891
|
+
*/
|
521
892
|
onPointerExit?(args: PointerEventData);
|
893
|
+
|
894
|
+
/**
|
895
|
+
* Called when a pointer button is pressed while over this component's GameObject
|
896
|
+
* @param args Data about the pointer event
|
897
|
+
*/
|
522
898
|
onPointerDown?(args: PointerEventData);
|
899
|
+
|
900
|
+
/**
|
901
|
+
* Called when a pointer button is released while over this component's GameObject
|
902
|
+
* @param args Data about the pointer event
|
903
|
+
*/
|
523
904
|
onPointerUp?(args: PointerEventData);
|
905
|
+
|
906
|
+
/**
|
907
|
+
* Called when a pointer completes a click interaction with this component's GameObject
|
908
|
+
* @param args Data about the pointer event
|
909
|
+
*/
|
524
910
|
onPointerClick?(args: PointerEventData);
|
525
911
|
|
526
|
-
|
527
|
-
|
528
|
-
*
|
529
|
-
*
|
530
|
-
*
|
531
|
-
*
|
532
|
-
* @param
|
533
|
-
* @
|
534
|
-
* @
|
535
|
-
*
|
912
|
+
/**
|
913
|
+
* Starts a coroutine that can yield to wait for events.
|
914
|
+
* Coroutines allow for time-based sequencing of operations without blocking.
|
915
|
+
* Coroutines are based on generator functions, a JavaScript language feature.
|
916
|
+
*
|
917
|
+
* @param routine Generator function to start
|
918
|
+
* @param evt Event to register the coroutine for (default: FrameEvent.Update)
|
919
|
+
* @returns The generator function that can be used to stop the coroutine
|
920
|
+
* @example
|
921
|
+
* Time-based sequencing of operations
|
536
922
|
* ```ts
|
537
|
-
*
|
923
|
+
* *myCoroutine() {
|
924
|
+
* yield WaitForSeconds(1); // wait for 1 second
|
925
|
+
* yield WaitForFrames(10); // wait for 10 frames
|
926
|
+
* yield new Promise(resolve => setTimeout(resolve, 1000)); // wait for a promise to resolve
|
927
|
+
* }
|
928
|
+
* ```
|
929
|
+
* @example
|
930
|
+
* Coroutine that logs a message every 5 frames
|
931
|
+
* ```ts
|
932
|
+
* onEnable() {
|
933
|
+
* this.startCoroutine(this.myCoroutine());
|
934
|
+
* }
|
538
935
|
* private *myCoroutine() {
|
539
|
-
*
|
540
|
-
*
|
541
|
-
*
|
542
|
-
*
|
543
|
-
*
|
936
|
+
* while(this.activeAndEnabled) {
|
937
|
+
* console.log("Hello World", this.context.time.frame);
|
938
|
+
* // wait for 5 frames
|
939
|
+
* for(let i = 0; i < 5; i++) yield;
|
940
|
+
* }
|
544
941
|
* }
|
545
942
|
* ```
|
546
943
|
*/
|
547
944
|
startCoroutine(routine: Generator, evt: FrameEvent = FrameEvent.Update): Generator {
|
548
945
|
return this.context.registerCoroutineUpdate(this, routine, evt);
|
549
946
|
}
|
947
|
+
|
550
948
|
/**
|
551
|
-
*
|
552
|
-
* @param routine
|
553
|
-
* @param evt
|
949
|
+
* Stops a coroutine that was previously started with startCoroutine
|
950
|
+
* @param routine The routine to be stopped
|
951
|
+
* @param evt The frame event the routine was registered with
|
554
952
|
*/
|
555
953
|
stopCoroutine(routine: Generator, evt: FrameEvent = FrameEvent.Update): void {
|
556
954
|
this.context.unregisterCoroutineUpdate(routine, evt);
|
557
955
|
}
|
558
956
|
|
559
|
-
/**
|
957
|
+
/**
|
958
|
+
* Checks if this component has been destroyed
|
959
|
+
* @returns True if the component or its GameObject has been destroyed
|
960
|
+
*/
|
560
961
|
public get destroyed(): boolean {
|
561
962
|
return this.__destroyed;
|
562
963
|
}
|
563
964
|
|
564
965
|
/**
|
565
|
-
* Destroys this component
|
966
|
+
* Destroys this component and removes it from its GameObject
|
967
|
+
* After destruction, the component will no longer receive lifecycle callbacks
|
566
968
|
*/
|
567
969
|
public destroy() {
|
568
970
|
if (this.__destroyed) return;
|
569
971
|
this.__internalDestroy();
|
570
972
|
}
|
571
973
|
|
572
|
-
|
573
|
-
|
574
974
|
/** @internal */
|
575
975
|
protected __didAwake: boolean = false;
|
576
976
|
|
@@ -589,7 +989,6 @@
|
|
589
989
|
/** @internal */
|
590
990
|
get __internalDidAwakeAndStart() { return this.__didAwake && this.__didStart; }
|
591
991
|
|
592
|
-
|
593
992
|
/** @internal */
|
594
993
|
constructor(init?: ComponentInit<Component>) {
|
595
994
|
this.__didAwake = false;
|
@@ -611,6 +1010,11 @@
|
|
611
1010
|
return this;
|
612
1011
|
}
|
613
1012
|
|
1013
|
+
/**
|
1014
|
+
* Initializes component properties from an initialization object
|
1015
|
+
* @param init Object with properties to copy to this component
|
1016
|
+
* @internal
|
1017
|
+
*/
|
614
1018
|
_internalInit(init?: ComponentInit<this>) {
|
615
1019
|
if (typeof init === "object") {
|
616
1020
|
for (const key of Object.keys(init)) {
|
@@ -690,7 +1094,10 @@
|
|
690
1094
|
destroyComponentInstance(this as any);
|
691
1095
|
}
|
692
1096
|
|
693
|
-
|
1097
|
+
/**
|
1098
|
+
* Controls whether this component is enabled
|
1099
|
+
* Disabled components don't receive lifecycle callbacks
|
1100
|
+
*/
|
694
1101
|
get enabled(): boolean {
|
695
1102
|
return typeof this.__isEnabled === "boolean" ? this.__isEnabled : true; // if it has no enabled field it is always enabled
|
696
1103
|
}
|
@@ -720,85 +1127,154 @@
|
|
720
1127
|
}
|
721
1128
|
}
|
722
1129
|
|
1130
|
+
/**
|
1131
|
+
* Gets the position of this component's GameObject in world space
|
1132
|
+
*/
|
723
1133
|
get worldPosition(): Vector3 {
|
724
1134
|
return threeutils.getWorldPosition(this.gameObject);
|
725
1135
|
}
|
726
1136
|
|
1137
|
+
/**
|
1138
|
+
* Sets the position of this component's GameObject in world space
|
1139
|
+
* @param val The world position vector to set
|
1140
|
+
*/
|
727
1141
|
set worldPosition(val: Vector3) {
|
728
1142
|
threeutils.setWorldPosition(this.gameObject, val);
|
729
1143
|
}
|
730
1144
|
|
1145
|
+
/**
|
1146
|
+
* Sets the position of this component's GameObject in world space using individual coordinates
|
1147
|
+
* @param x X-coordinate in world space
|
1148
|
+
* @param y Y-coordinate in world space
|
1149
|
+
* @param z Z-coordinate in world space
|
1150
|
+
*/
|
731
1151
|
setWorldPosition(x: number, y: number, z: number) {
|
732
1152
|
threeutils.setWorldPositionXYZ(this.gameObject, x, y, z);
|
733
1153
|
}
|
734
1154
|
|
735
|
-
|
1155
|
+
/**
|
1156
|
+
* Gets the rotation of this component's GameObject in world space as a quaternion
|
1157
|
+
*/
|
736
1158
|
get worldQuaternion(): Quaternion {
|
737
1159
|
return threeutils.getWorldQuaternion(this.gameObject);
|
738
1160
|
}
|
1161
|
+
|
1162
|
+
/**
|
1163
|
+
* Sets the rotation of this component's GameObject in world space using a quaternion
|
1164
|
+
* @param val The world rotation quaternion to set
|
1165
|
+
*/
|
739
1166
|
set worldQuaternion(val: Quaternion) {
|
740
1167
|
threeutils.setWorldQuaternion(this.gameObject, val);
|
741
1168
|
}
|
1169
|
+
|
1170
|
+
/**
|
1171
|
+
* Sets the rotation of this component's GameObject in world space using quaternion components
|
1172
|
+
* @param x X component of the quaternion
|
1173
|
+
* @param y Y component of the quaternion
|
1174
|
+
* @param z Z component of the quaternion
|
1175
|
+
* @param w W component of the quaternion
|
1176
|
+
*/
|
742
1177
|
setWorldQuaternion(x: number, y: number, z: number, w: number) {
|
743
1178
|
threeutils.setWorldQuaternionXYZW(this.gameObject, x, y, z, w);
|
744
1179
|
}
|
745
1180
|
|
746
|
-
/
|
1181
|
+
/**
|
1182
|
+
* Gets the rotation of this component's GameObject in world space as Euler angles (in radians)
|
1183
|
+
*/
|
747
1184
|
get worldEuler(): Euler {
|
748
1185
|
return threeutils.getWorldEuler(this.gameObject);
|
749
1186
|
}
|
750
1187
|
|
751
|
-
/
|
1188
|
+
/**
|
1189
|
+
* Sets the rotation of this component's GameObject in world space using Euler angles (in radians)
|
1190
|
+
* @param val The world rotation Euler angles to set
|
1191
|
+
*/
|
752
1192
|
set worldEuler(val: Euler) {
|
753
1193
|
threeutils.setWorldEuler(this.gameObject, val);
|
754
1194
|
}
|
755
1195
|
|
756
|
-
/
|
1196
|
+
/**
|
1197
|
+
* Gets the rotation of this component's GameObject in world space as Euler angles (in degrees)
|
1198
|
+
*/
|
757
1199
|
get worldRotation(): Vector3 {
|
758
|
-
return this.gameObject.worldRotation;
|
1200
|
+
return this.gameObject.worldRotation;
|
759
1201
|
}
|
760
1202
|
|
1203
|
+
/**
|
1204
|
+
* Sets the rotation of this component's GameObject in world space using Euler angles (in degrees)
|
1205
|
+
* @param val The world rotation vector to set (in degrees)
|
1206
|
+
*/
|
761
1207
|
set worldRotation(val: Vector3) {
|
762
1208
|
this.setWorldRotation(val.x, val.y, val.z, true);
|
763
1209
|
}
|
764
1210
|
|
1211
|
+
/**
|
1212
|
+
* Sets the rotation of this component's GameObject in world space using individual Euler angles
|
1213
|
+
* @param x X-axis rotation
|
1214
|
+
* @param y Y-axis rotation
|
1215
|
+
* @param z Z-axis rotation
|
1216
|
+
* @param degrees Whether the values are in degrees (true) or radians (false)
|
1217
|
+
*/
|
765
1218
|
setWorldRotation(x: number, y: number, z: number, degrees: boolean = true) {
|
766
1219
|
threeutils.setWorldRotationXYZ(this.gameObject, x, y, z, degrees);
|
767
1220
|
}
|
768
1221
|
|
769
1222
|
private static _forward: Vector3 = new Vector3();
|
770
|
-
/**
|
1223
|
+
/**
|
1224
|
+
* Gets the forward direction vector (0,0,-1) of this component's GameObject in world space
|
1225
|
+
*/
|
771
1226
|
public get forward(): Vector3 {
|
772
1227
|
return Component._forward.set(0, 0, -1).applyQuaternion(this.worldQuaternion);
|
773
1228
|
}
|
774
1229
|
private static _right: Vector3 = new Vector3();
|
775
|
-
/**
|
1230
|
+
/**
|
1231
|
+
* Gets the right direction vector (1,0,0) of this component's GameObject in world space
|
1232
|
+
*/
|
776
1233
|
public get right(): Vector3 {
|
777
1234
|
return Component._right.set(1, 0, 0).applyQuaternion(this.worldQuaternion);
|
778
1235
|
}
|
779
1236
|
private static _up: Vector3 = new Vector3();
|
780
|
-
/**
|
1237
|
+
/**
|
1238
|
+
* Gets the up direction vector (0,1,0) of this component's GameObject in world space
|
1239
|
+
*/
|
781
1240
|
public get up(): Vector3 {
|
782
1241
|
return Component._up.set(0, 1, 0).applyQuaternion(this.worldQuaternion);
|
783
1242
|
}
|
784
1243
|
|
785
|
-
|
786
|
-
|
787
1244
|
// EventTarget implementation:
|
788
1245
|
|
1246
|
+
/**
|
1247
|
+
* Storage for event listeners registered to this component
|
1248
|
+
* @private
|
1249
|
+
*/
|
789
1250
|
private _eventListeners = new Map<string, EventListener[]>();
|
790
1251
|
|
1252
|
+
/**
|
1253
|
+
* Registers an event listener for the specified event type
|
1254
|
+
* @param type The event type to listen for
|
1255
|
+
* @param listener The callback function to execute when the event occurs
|
1256
|
+
*/
|
791
1257
|
addEventListener<T extends Event>(type: string, listener: (evt: T) => any) {
|
792
1258
|
this._eventListeners[type] = this._eventListeners[type] || [];
|
793
1259
|
this._eventListeners[type].push(listener);
|
794
1260
|
}
|
795
1261
|
|
1262
|
+
/**
|
1263
|
+
* Removes a previously registered event listener
|
1264
|
+
* @param type The event type the listener was registered for
|
1265
|
+
* @param listener The callback function to remove
|
1266
|
+
*/
|
796
1267
|
removeEventListener<T extends Event>(type: string, listener: (arg: T) => any) {
|
797
1268
|
if (!this._eventListeners[type]) return;
|
798
1269
|
const index = this._eventListeners[type].indexOf(listener);
|
799
1270
|
if (index >= 0) this._eventListeners[type].splice(index, 1);
|
800
1271
|
}
|
801
1272
|
|
1273
|
+
/**
|
1274
|
+
* Dispatches an event to all registered listeners
|
1275
|
+
* @param evt The event object to dispatch
|
1276
|
+
* @returns Always returns false (standard implementation of EventTarget)
|
1277
|
+
*/
|
802
1278
|
dispatchEvent(evt: Event): boolean {
|
803
1279
|
if (!evt || !this._eventListeners[evt.type]) return false;
|
804
1280
|
const listeners = this._eventListeners[evt.type];
|
@@ -18,8 +18,10 @@
|
|
18
18
|
import type { IPointerEventHandler, PointerEventData } from "./ui/PointerEvents.js";
|
19
19
|
import { ObjectRaycaster } from "./ui/Raycaster.js";
|
20
20
|
|
21
|
+
/** Enable debug visualization and logging for DragControls by using the URL parameter `?debugdrag`. */
|
21
22
|
const debug = getParam("debugdrag");
|
22
23
|
|
24
|
+
/** Buffer to store currently active DragControls components */
|
23
25
|
const dragControlsBuffer: DragControls[] = [];
|
24
26
|
|
25
27
|
/**
|
@@ -34,7 +36,7 @@
|
|
34
36
|
HitNormal = 2,
|
35
37
|
/** Combination of XZ and Screen based on the viewing angle. Low angles result in Screen dragging and higher angles in XZ dragging. */
|
36
38
|
DynamicViewAngle = 3,
|
37
|
-
/** The drag plane is
|
39
|
+
/** The drag plane is snapped to surfaces in the scene while dragging. */
|
38
40
|
SnapToSurfaces = 4,
|
39
41
|
/** Don't allow dragging the object */
|
40
42
|
None = 5,
|
@@ -42,18 +44,24 @@
|
|
42
44
|
|
43
45
|
/**
|
44
46
|
* 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).
|
47
|
+
* Debug mode can be enabled with the URL parameter `?debugdrag`, which shows visual helpers and logs drag operations.
|
48
|
+
*
|
45
49
|
* @category Interactivity
|
46
50
|
* @group Components
|
47
51
|
*/
|
48
52
|
export class DragControls extends Behaviour implements IPointerEventHandler {
|
49
53
|
|
50
54
|
/**
|
55
|
+
* Checks if any DragControls component is currently active with selected objects
|
51
56
|
* @returns True if any DragControls component is currently active
|
52
57
|
*/
|
53
58
|
public static get HasAnySelected(): boolean { return this._active > 0; }
|
54
59
|
private static _active: number = 0;
|
55
60
|
|
56
|
-
/**
|
61
|
+
/**
|
62
|
+
* Retrieves a list of all DragControl components that are currently dragging objects.
|
63
|
+
* @returns Array of currently active DragControls components
|
64
|
+
*/
|
57
65
|
public static get CurrentlySelected() {
|
58
66
|
dragControlsBuffer.length = 0;
|
59
67
|
for (const dc of this._instances) {
|
@@ -63,51 +71,71 @@
|
|
63
71
|
}
|
64
72
|
return dragControlsBuffer;
|
65
73
|
}
|
66
|
-
/**
|
74
|
+
/** Registry of currently active and enabled DragControls components */
|
67
75
|
private static _instances: DragControls[] = [];
|
68
76
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
// two-handed drag (scale, rotate, move)
|
74
|
-
// keep upright (no tilt)
|
75
|
-
|
76
|
-
/** How and where the object is dragged along. */
|
77
|
+
/**
|
78
|
+
* Determines how and where the object is dragged along. Different modes include
|
79
|
+
* dragging along a plane, attached to the pointer, or following surface normals.
|
80
|
+
*/
|
77
81
|
@serializable()
|
78
82
|
public dragMode: DragMode = DragMode.DynamicViewAngle;
|
79
83
|
|
80
|
-
/**
|
84
|
+
/**
|
85
|
+
* Snaps dragged objects to a 3D grid with the specified resolution.
|
86
|
+
* Set to 0 to disable snapping.
|
87
|
+
*/
|
81
88
|
@serializable()
|
82
89
|
public snapGridResolution: number = 0.0;
|
83
90
|
|
84
|
-
/**
|
91
|
+
/**
|
92
|
+
* When true, maintains the original rotation of the dragged object while moving it.
|
93
|
+
* When false, allows the object to rotate freely during dragging.
|
94
|
+
*/
|
85
95
|
@serializable()
|
86
96
|
public keepRotation: boolean = true;
|
87
97
|
|
88
|
-
/**
|
98
|
+
/**
|
99
|
+
* Determines how and where the object is dragged along while dragging in XR.
|
100
|
+
* Uses a separate setting from regular drag mode for better XR interaction.
|
101
|
+
*/
|
89
102
|
@serializable()
|
90
103
|
public xrDragMode: DragMode = DragMode.Attached;
|
91
104
|
|
92
|
-
/**
|
105
|
+
/**
|
106
|
+
* When true, maintains the original rotation of the dragged object during XR dragging.
|
107
|
+
* When false, allows the object to rotate freely during XR dragging.
|
108
|
+
*/
|
93
109
|
@serializable()
|
94
110
|
public xrKeepRotation: boolean = false;
|
95
111
|
|
96
|
-
/**
|
112
|
+
/**
|
113
|
+
* Multiplier that affects how quickly objects move closer or further away when dragging in XR.
|
114
|
+
* Higher values make distance changes more pronounced.
|
115
|
+
* This is similar to mouse acceleration on a screen.
|
116
|
+
*/
|
97
117
|
@serializable()
|
98
118
|
public xrDistanceDragFactor: number = 1;
|
99
119
|
|
100
|
-
/**
|
120
|
+
/**
|
121
|
+
* When enabled, draws a visual line from the dragged object downwards to the next raycast hit,
|
122
|
+
* providing visual feedback about the object's position relative to surfaces below it.
|
123
|
+
*/
|
101
124
|
@serializable()
|
102
125
|
public showGizmo: boolean = false;
|
103
126
|
|
104
|
-
/**
|
127
|
+
/**
|
128
|
+
* Returns the object currently being dragged by this DragControls component, if any.
|
129
|
+
* @returns The object being dragged or null if no object is currently dragged
|
130
|
+
*/
|
105
131
|
get draggedObject() {
|
106
132
|
return this._targetObject;
|
107
133
|
}
|
108
134
|
|
109
135
|
/**
|
110
|
-
*
|
136
|
+
* Updates the object that is being dragged by the DragControls.
|
137
|
+
* This can be used to change the target during a drag operation.
|
138
|
+
* @param obj The new object to drag, or null to stop dragging
|
111
139
|
*/
|
112
140
|
setTargetObject(obj: Object3D | null) {
|
113
141
|
this._targetObject = obj;
|
@@ -133,8 +161,8 @@
|
|
133
161
|
this._rigidbody[wasKinematicKey] = false;
|
134
162
|
}
|
135
163
|
}
|
136
|
-
|
137
164
|
}
|
165
|
+
|
138
166
|
private _rigidbody: Rigidbody | null = null;
|
139
167
|
|
140
168
|
// future:
|
@@ -182,11 +210,20 @@
|
|
182
210
|
DragControls._instances = DragControls._instances.filter(i => i !== this);
|
183
211
|
}
|
184
212
|
|
213
|
+
/**
|
214
|
+
* Checks if editing is allowed for the current networking connection.
|
215
|
+
* @param _obj Optional object to check edit permissions for
|
216
|
+
* @returns True if editing is allowed
|
217
|
+
*/
|
185
218
|
private allowEdit(_obj: Object3D | null = null) {
|
186
219
|
return this.context.connection.allowEditing;
|
187
220
|
}
|
188
221
|
|
189
|
-
/**
|
222
|
+
/**
|
223
|
+
* Handles pointer enter events. Sets the cursor style and tracks the hovered object.
|
224
|
+
* @param evt Pointer event data containing information about the interaction
|
225
|
+
* @internal
|
226
|
+
*/
|
190
227
|
onPointerEnter?(evt: PointerEventData) {
|
191
228
|
if (!this.allowEdit(this.gameObject)) return;
|
192
229
|
if (evt.mode !== "screen") return;
|
@@ -202,12 +239,20 @@
|
|
202
239
|
this.context.domElement.style.cursor = 'pointer';
|
203
240
|
}
|
204
241
|
|
205
|
-
/**
|
242
|
+
/**
|
243
|
+
* Handles pointer movement events. Marks the event as used if dragging is active.
|
244
|
+
* @param args Pointer event data containing information about the movement
|
245
|
+
* @internal
|
246
|
+
*/
|
206
247
|
onPointerMove?(args: PointerEventData) {
|
207
248
|
if (this._isDragging || this._potentialDragStartEvt !== null) args.use();
|
208
249
|
}
|
209
250
|
|
210
|
-
/**
|
251
|
+
/**
|
252
|
+
* Handles pointer exit events. Resets the cursor style when the pointer leaves a draggable object.
|
253
|
+
* @param evt Pointer event data containing information about the interaction
|
254
|
+
* @internal
|
255
|
+
*/
|
211
256
|
onPointerExit?(evt: PointerEventData) {
|
212
257
|
if (!this.allowEdit(this.gameObject)) return;
|
213
258
|
if (evt.mode !== "screen") return;
|
@@ -215,7 +260,11 @@
|
|
215
260
|
this.context.domElement.style.cursor = 'auto';
|
216
261
|
}
|
217
262
|
|
218
|
-
/**
|
263
|
+
/**
|
264
|
+
* Handles pointer down events. Initiates the potential drag operation if conditions are met.
|
265
|
+
* @param args Pointer event data containing information about the interaction
|
266
|
+
* @internal
|
267
|
+
*/
|
219
268
|
onPointerDown(args: PointerEventData) {
|
220
269
|
if (!this.allowEdit(this.gameObject)) return;
|
221
270
|
if (args.used) return;
|
@@ -262,7 +311,11 @@
|
|
262
311
|
}
|
263
312
|
}
|
264
313
|
|
265
|
-
/**
|
314
|
+
/**
|
315
|
+
* Handles pointer up events. Finalizes or cancels the drag operation.
|
316
|
+
* @param args Pointer event data containing information about the interaction
|
317
|
+
* @internal
|
318
|
+
*/
|
266
319
|
onPointerUp(args: PointerEventData) {
|
267
320
|
if (debug) Gizmos.DrawLabel(args.point ?? this.gameObject.worldPosition, "POINTERUP:" + args.pointerId + ", " + args.button, .03, 3);
|
268
321
|
if (!this.allowEdit(this.gameObject)) return;
|
@@ -293,9 +346,12 @@
|
|
293
346
|
}
|
294
347
|
}
|
295
348
|
|
296
|
-
/**
|
349
|
+
/**
|
350
|
+
* Updates the drag operation every frame. Processes pointer movement, accumulates drag distance
|
351
|
+
* and triggers drag start once there's enough movement.
|
352
|
+
* @internal
|
353
|
+
*/
|
297
354
|
update(): void {
|
298
|
-
|
299
355
|
for (const handler of this._dragHandlers.values()) {
|
300
356
|
if (handler.collectMovementInfo) handler.collectMovementInfo();
|
301
357
|
// TODO this doesn't make sense, we should instead just use the max here
|
@@ -324,7 +380,12 @@
|
|
324
380
|
this.onAnyDragUpdate();
|
325
381
|
}
|
326
382
|
|
327
|
-
/**
|
383
|
+
/**
|
384
|
+
* Called when the first pointer starts dragging on this object.
|
385
|
+
* Sets up network synchronization and marks rigidbodies for dragging.
|
386
|
+
* Not called for subsequent pointers on the same object.
|
387
|
+
* @param evt Pointer event data that initiated the drag
|
388
|
+
*/
|
328
389
|
private onFirstDragStart(evt: PointerEventData) {
|
329
390
|
if (!evt || !evt.object) return;
|
330
391
|
|
@@ -356,7 +417,10 @@
|
|
356
417
|
this._draggingRigidbodies.push(...rbs);
|
357
418
|
}
|
358
419
|
|
359
|
-
/**
|
420
|
+
/**
|
421
|
+
* Called each frame as long as any pointer is dragging this object.
|
422
|
+
* Updates visuals and keeps rigidbodies awake during the drag.
|
423
|
+
*/
|
360
424
|
private onAnyDragUpdate() {
|
361
425
|
if (!this._dragHelper) return;
|
362
426
|
this._dragHelper.showGizmo = this.showGizmo;
|
@@ -373,7 +437,11 @@
|
|
373
437
|
InstancingUtil.markDirty(object);
|
374
438
|
}
|
375
439
|
|
376
|
-
/**
|
440
|
+
/**
|
441
|
+
* Called when the last pointer has been removed from this object.
|
442
|
+
* Cleans up drag state and applies final velocities to rigidbodies.
|
443
|
+
* @param evt Pointer event data for the last pointer that was lifted
|
444
|
+
*/
|
377
445
|
private onLastDragEnd(evt: PointerEventData | null) {
|
378
446
|
if (!this || !this._isDragging) return;
|
379
447
|
this._isDragging = false;
|
@@ -399,7 +467,10 @@
|
|
399
467
|
}
|
400
468
|
}
|
401
469
|
|
402
|
-
/**
|
470
|
+
/**
|
471
|
+
* Common interface for pointer handlers (single touch and multi touch).
|
472
|
+
* Defines methods for tracking movement and managing target objects during drag operations.
|
473
|
+
*/
|
403
474
|
interface IDragHandler {
|
404
475
|
/** Used to determine if a drag has happened for this handler */
|
405
476
|
getTotalMovement?(): Vector3;
|
@@ -414,7 +485,10 @@
|
|
414
485
|
onDragUpdate?(numberOfPointers: number): void;
|
415
486
|
}
|
416
487
|
|
417
|
-
/**
|
488
|
+
/**
|
489
|
+
* Handles two touch points affecting one object.
|
490
|
+
* Enables multi-touch interactions that allow movement, scaling, and rotation of objects.
|
491
|
+
*/
|
418
492
|
class MultiTouchDragHandler implements IDragHandler {
|
419
493
|
|
420
494
|
handlerA: DragPointerHandler;
|
@@ -660,15 +734,27 @@
|
|
660
734
|
}
|
661
735
|
|
662
736
|
|
663
|
-
/**
|
664
|
-
*
|
737
|
+
/**
|
738
|
+
* Handles a single pointer on an object.
|
739
|
+
* DragPointerHandlers manage determining if a drag operation has started, tracking pointer movement,
|
740
|
+
* and controlling object translation based on the drag mode.
|
665
741
|
*/
|
666
742
|
class DragPointerHandler implements IDragHandler {
|
667
743
|
|
668
|
-
/**
|
669
|
-
*
|
744
|
+
/**
|
745
|
+
* Returns the accumulated movement of the pointer in world units.
|
746
|
+
* Used for determining if enough motion has occurred to start a drag.
|
747
|
+
*/
|
670
748
|
getTotalMovement(): Vector3 { return this._totalMovement; }
|
749
|
+
|
750
|
+
/**
|
751
|
+
* Returns the object that follows the pointer during dragging operations.
|
752
|
+
*/
|
671
753
|
get followObject(): GameObject { return this._followObject; }
|
754
|
+
|
755
|
+
/**
|
756
|
+
* Returns the point where the pointer initially hit the object in local space.
|
757
|
+
*/
|
672
758
|
get hitPointInLocalSpace(): Vector3 { return this._hitPointInLocalSpace; }
|
673
759
|
|
674
760
|
private context: Context;
|
@@ -1249,18 +1335,28 @@
|
|
1249
1335
|
}
|
1250
1336
|
}
|
1251
1337
|
|
1252
|
-
/**
|
1253
|
-
*
|
1338
|
+
/**
|
1339
|
+
* Provides visual helper elements for DragControls.
|
1340
|
+
* Shows where objects will be placed and their relation to surfaces below them.
|
1254
1341
|
*/
|
1255
1342
|
class LegacyDragVisualsHelper {
|
1256
1343
|
|
1344
|
+
/** Controls whether visual helpers like lines and markers are displayed */
|
1257
1345
|
showGizmo: boolean = true;
|
1346
|
+
|
1347
|
+
/** When true, drag plane alignment changes based on view angle */
|
1258
1348
|
useViewAngle: boolean = true;
|
1259
1349
|
|
1350
|
+
/**
|
1351
|
+
* Checks if there is a currently selected object being visualized
|
1352
|
+
*/
|
1260
1353
|
public get hasSelected(): boolean {
|
1261
1354
|
return this._selected !== null && this._selected !== undefined;
|
1262
1355
|
}
|
1263
1356
|
|
1357
|
+
/**
|
1358
|
+
* Returns the currently selected object being visualized, if any
|
1359
|
+
*/
|
1264
1360
|
public get selected(): Object3D | null {
|
1265
1361
|
return this._selected;
|
1266
1362
|
}
|
@@ -19,63 +19,104 @@
|
|
19
19
|
import { Behaviour } from "./Component.js";
|
20
20
|
import { EventList } from "./EventList.js";
|
21
21
|
|
22
|
+
/**
|
23
|
+
* Debug mode can be enabled with the URL parameter `?debugdroplistener`, which
|
24
|
+
* logs additional information during drag and drop events and visualizes hit points.
|
25
|
+
*/
|
22
26
|
const debug = getParam("debugdroplistener");
|
23
27
|
|
28
|
+
/**
|
29
|
+
* Events dispatched by the DropListener component
|
30
|
+
* @enum {string}
|
31
|
+
*/
|
24
32
|
export enum DropListenerEvents {
|
25
33
|
/**
|
26
|
-
* Dispatched when a file is dropped into the scene. The detail of the event is the
|
34
|
+
* Dispatched when a file is dropped into the scene. The detail of the event is the {@link File} that was dropped.
|
35
|
+
* The event is called once for each dropped file.
|
27
36
|
*/
|
28
37
|
FileDropped = "file-dropped",
|
29
38
|
/**
|
30
|
-
* Dispatched when a new object is added to the scene. The detail of the event
|
39
|
+
* Dispatched when a new object is added to the scene. The detail of the event contains {@link DropListenerOnDropArguments} for the content that was added.
|
31
40
|
*/
|
32
41
|
ObjectAdded = "object-added",
|
33
42
|
}
|
34
43
|
|
44
|
+
/**
|
45
|
+
* Context information for a drop operation
|
46
|
+
*/
|
35
47
|
declare type DropContext = {
|
48
|
+
/** Position where the file was dropped in screen coordinates */
|
36
49
|
screenposition: Vector2;
|
50
|
+
/** URL of the dropped content, if applicable */
|
37
51
|
url?: string,
|
52
|
+
/** File object of the dropped content, if applicable */
|
38
53
|
file?: File;
|
54
|
+
/** 3D position where the content should be placed */
|
39
55
|
point?: Vec3;
|
56
|
+
/** Size dimensions for the content */
|
40
57
|
size?: Vec3;
|
41
58
|
}
|
42
59
|
|
43
60
|
|
44
|
-
/**
|
61
|
+
/**
|
62
|
+
* Network event arguments passed between clients when using the DropListener with networking
|
63
|
+
*/
|
45
64
|
export declare type DropListenerNetworkEventArguments = {
|
65
|
+
/** Unique identifier of the sender */
|
46
66
|
guid: string,
|
67
|
+
/** Name of the dropped object */
|
47
68
|
name: string,
|
69
|
+
/** URL or array of URLs to the dropped content */
|
48
70
|
url: string | string[],
|
49
71
|
/** Worldspace point where the object was placed in the scene */
|
50
72
|
point: Vec3;
|
51
73
|
/** Bounding box size */
|
52
74
|
size: Vec3;
|
75
|
+
/** MD5 hash of the content for verification */
|
53
76
|
contentMD5: string;
|
54
77
|
}
|
55
78
|
|
79
|
+
/**
|
80
|
+
* Arguments provided to handlers when an object is dropped or added to the scene
|
81
|
+
*/
|
56
82
|
export declare type DropListenerOnDropArguments = {
|
83
|
+
/** The DropListener component that processed the drop event */
|
57
84
|
sender: DropListener,
|
58
|
-
/**
|
85
|
+
/** The root object added to the scene */
|
59
86
|
object: Object3D,
|
60
|
-
/** The
|
87
|
+
/** The complete model with all associated data */
|
61
88
|
model: Model,
|
89
|
+
/** MD5 hash of the content for verification */
|
62
90
|
contentMD5: string;
|
91
|
+
/** The original dropped URL or File object */
|
63
92
|
dropped: URL | File | undefined;
|
64
93
|
}
|
65
94
|
|
66
|
-
/**
|
95
|
+
/**
|
96
|
+
* CustomEvent dispatched when an object is added to the scene via the DropListener
|
97
|
+
*/
|
67
98
|
class DropListenerAddedEvent<T extends DropListenerOnDropArguments> extends CustomEvent<T> {
|
99
|
+
/**
|
100
|
+
* Creates a new added event with the provided details
|
101
|
+
* @param detail Information about the added object
|
102
|
+
*/
|
68
103
|
constructor(detail: T) {
|
69
104
|
super(DropListenerEvents.ObjectAdded, { detail });
|
70
105
|
}
|
71
106
|
}
|
72
107
|
|
108
|
+
/**
|
109
|
+
* Key name used for blob storage parameters
|
110
|
+
*/
|
73
111
|
const blobKeyName = "blob";
|
74
112
|
|
75
113
|
/** The DropListener component is used to listen for drag and drop events in the browser and add the dropped files to the scene
|
76
114
|
* It can be used to allow users to drag and drop glTF files into the scene to add new objects.
|
77
115
|
*
|
78
|
-
*
|
116
|
+
* If {@link useNetworking} is enabled, the DropListener will automatically synchronize dropped files to other connected clients.
|
117
|
+
* Enable {@link fitIntoVolume} to automatically scale dropped objects to fit within the volume defined by {@link fitVolumeSize}.
|
118
|
+
*
|
119
|
+
* The following events are dispatched by the DropListener:
|
79
120
|
* - **object-added** - dispatched when a new object is added to the scene
|
80
121
|
* - **file-dropped** - dispatched when a file is dropped into the scene
|
81
122
|
*
|
@@ -103,42 +144,46 @@
|
|
103
144
|
export class DropListener extends Behaviour {
|
104
145
|
|
105
146
|
/**
|
106
|
-
* When enabled the DropListener will automatically
|
147
|
+
* When enabled, the DropListener will automatically synchronize dropped files to other connected clients.
|
148
|
+
* When a file is dropped locally, it will be uploaded to blob storage and the URL will be shared with other clients.
|
107
149
|
*/
|
108
150
|
@serializable()
|
109
151
|
useNetworking: boolean = true;
|
110
152
|
|
111
153
|
/**
|
112
|
-
* When assigned the
|
154
|
+
* When assigned, the DropListener will only accept files that are dropped on this specific object.
|
155
|
+
* This allows creating designated drop zones in your scene.
|
113
156
|
*/
|
114
157
|
@serializable(Object3D)
|
115
158
|
dropArea?: Object3D;
|
116
159
|
|
117
160
|
/**
|
118
|
-
* When enabled
|
161
|
+
* When enabled, dropped objects will be automatically scaled to fit within the volume defined by fitVolumeSize.
|
162
|
+
* Useful for ensuring dropped models appear at an appropriate scale.
|
119
163
|
* @default false
|
120
164
|
*/
|
121
165
|
@serializable()
|
122
166
|
fitIntoVolume: boolean = false;
|
123
167
|
|
124
168
|
/**
|
125
|
-
*
|
169
|
+
* Defines the dimensions of the volume that dropped objects will be scaled to fit within.
|
170
|
+
* Only used when fitIntoVolume is enabled.
|
126
171
|
*/
|
127
172
|
@serializable(Vector3)
|
128
173
|
fitVolumeSize = new Vector3(1, 1, 1);
|
129
174
|
|
130
|
-
/**
|
175
|
+
/**
|
176
|
+
* When enabled, dropped objects will be positioned at the point where the cursor hit the scene.
|
177
|
+
* When disabled, objects will be placed at the origin of the DropListener.
|
131
178
|
* @default true
|
132
179
|
*/
|
133
180
|
@serializable()
|
134
181
|
placeAtHitPosition: boolean = true;
|
135
182
|
|
136
|
-
|
137
183
|
/**
|
138
|
-
*
|
139
|
-
*
|
184
|
+
* Event list that gets invoked after a file has been successfully added to the scene.
|
185
|
+
* Receives {@link DropListenerOnDropArguments} containing the added object and related information.
|
140
186
|
* @event object-added
|
141
|
-
* @param {DropListenerOnDropArguments} evt
|
142
187
|
* @example
|
143
188
|
* ```typescript
|
144
189
|
* dropListener.onDropped.addEventListener((evt) => {
|
@@ -178,6 +223,10 @@
|
|
178
223
|
this.removePreviouslyAddedObjects(false);
|
179
224
|
}
|
180
225
|
|
226
|
+
/**
|
227
|
+
* Handles network events received from other clients containing information about dropped objects
|
228
|
+
* @param evt Network event data containing object information, position, and content URL
|
229
|
+
*/
|
181
230
|
private onNetworkEvent = (evt: DropListenerNetworkEventArguments) => {
|
182
231
|
if (!this.useNetworking) {
|
183
232
|
if (debug) console.debug("[DropListener] Ignoring networked event because networking is disabled", evt);
|
@@ -199,6 +248,11 @@
|
|
199
248
|
}
|
200
249
|
}
|
201
250
|
|
251
|
+
/**
|
252
|
+
* Handles clipboard paste events and processes them as potential URL drops
|
253
|
+
* Only URLs are processed by this handler, and only when editing is allowed
|
254
|
+
* @param evt The paste event
|
255
|
+
*/
|
202
256
|
private handlePaste = (evt: Event) => {
|
203
257
|
if (this.context.connection.allowEditing === false) return;
|
204
258
|
if (evt.defaultPrevented) return;
|
@@ -217,12 +271,22 @@
|
|
217
271
|
.catch(console.warn);
|
218
272
|
}
|
219
273
|
|
274
|
+
/**
|
275
|
+
* Handles drag events over the renderer's canvas
|
276
|
+
* Prevents default behavior to enable drop events
|
277
|
+
* @param evt The drag event
|
278
|
+
*/
|
220
279
|
private onDrag = (evt: DragEvent) => {
|
221
280
|
if (this.context.connection.allowEditing === false) return;
|
222
281
|
// necessary to get drop event
|
223
282
|
evt.preventDefault();
|
224
283
|
}
|
225
284
|
|
285
|
+
/**
|
286
|
+
* Processes drop events to add files to the scene
|
287
|
+
* Handles both file drops and text/URL drops
|
288
|
+
* @param evt The drop event
|
289
|
+
*/
|
226
290
|
private onDrop = async (evt: DragEvent) => {
|
227
291
|
if (this.context.connection.allowEditing === false) return;
|
228
292
|
|
@@ -266,6 +330,14 @@
|
|
266
330
|
}
|
267
331
|
}
|
268
332
|
|
333
|
+
/**
|
334
|
+
* Processes a dropped or pasted URL and tries to load it as a 3D model
|
335
|
+
* Handles special cases like GitHub URLs and Polyhaven asset URLs
|
336
|
+
* @param url The URL to process
|
337
|
+
* @param ctx Context information about where the drop occurred
|
338
|
+
* @param isRemote Whether this URL was shared from a remote client
|
339
|
+
* @returns The added object or null if loading failed
|
340
|
+
*/
|
269
341
|
private async addFromUrl(url: string, ctx: DropContext, isRemote: boolean) {
|
270
342
|
if (debug) console.log("dropped url", url);
|
271
343
|
|
@@ -315,6 +387,12 @@
|
|
315
387
|
|
316
388
|
private _abort: AbortController | null = null;
|
317
389
|
|
390
|
+
/**
|
391
|
+
* Processes dropped files, loads them as 3D models, and handles networking if enabled
|
392
|
+
* Creates an abort controller to cancel previous uploads if new files are dropped
|
393
|
+
* @param fileList Array of dropped files
|
394
|
+
* @param ctx Context information about where the drop occurred
|
395
|
+
*/
|
318
396
|
private async addDroppedFiles(fileList: Array<File>, ctx: DropContext) {
|
319
397
|
if (debug) console.log("Add files", fileList)
|
320
398
|
if (!Array.isArray(fileList)) return;
|
@@ -361,7 +439,10 @@
|
|
361
439
|
private readonly _addedObjects = new Array<Object3D>();
|
362
440
|
private readonly _addedModels = new Array<Model>();
|
363
441
|
|
364
|
-
/**
|
442
|
+
/**
|
443
|
+
* Removes all previously added objects from the scene
|
444
|
+
* @param doDestroy When true, destroys the objects; when false, just clears the references
|
445
|
+
*/
|
365
446
|
private removePreviouslyAddedObjects(doDestroy: boolean = true) {
|
366
447
|
if (doDestroy) {
|
367
448
|
for (const prev of this._addedObjects) {
|
@@ -375,7 +456,13 @@
|
|
375
456
|
}
|
376
457
|
|
377
458
|
/**
|
378
|
-
* Adds
|
459
|
+
* Adds a loaded model to the scene with proper positioning and scaling.
|
460
|
+
* Handles placement based on component settings and raycasting.
|
461
|
+
* If {@link fitIntoVolume} is enabled, the object will be scaled to fit within the volume defined by {@link fitVolumeSize}.
|
462
|
+
* @param data The loaded model data and content hash
|
463
|
+
* @param ctx Context information about where the drop occurred
|
464
|
+
* @param isRemote Whether this object was shared from a remote client
|
465
|
+
* @returns The added object or null if adding failed
|
379
466
|
*/
|
380
467
|
private addObject(data: { model: Model, contentMD5: string }, ctx: DropContext, isRemote: boolean): Object3D | null {
|
381
468
|
|
@@ -444,6 +531,13 @@
|
|
444
531
|
return obj;
|
445
532
|
}
|
446
533
|
|
534
|
+
/**
|
535
|
+
* Sends a network event to other clients about a dropped object
|
536
|
+
* Only triggered when networking is enabled and the connection is established
|
537
|
+
* @param url The URL to the content that was dropped
|
538
|
+
* @param obj The object that was added to the scene
|
539
|
+
* @param contentmd5 The content hash for verification
|
540
|
+
*/
|
447
541
|
private async sendDropEvent(url: string, obj: Object3D, contentmd5: string) {
|
448
542
|
if (!this.useNetworking) {
|
449
543
|
if (debug) console.debug("[DropListener] Ignoring networked event because networking is disabled", url);
|
@@ -463,12 +557,20 @@
|
|
463
557
|
this.context.connection.send("droplistener", evt);
|
464
558
|
}
|
465
559
|
}
|
560
|
+
|
561
|
+
/**
|
562
|
+
* Deletes remote state for this DropListener's objects
|
563
|
+
* Called when new files are dropped to clean up previous state
|
564
|
+
*/
|
466
565
|
private deleteDropEvent() {
|
467
566
|
this.context.connection.sendDeleteRemoteState(this.guid);
|
468
567
|
}
|
469
568
|
|
470
|
-
|
471
|
-
|
569
|
+
/**
|
570
|
+
* Tests if a drop event occurred within the designated drop area if one is specified
|
571
|
+
* @param ctx The drop context containing screen position information
|
572
|
+
* @returns True if the drop is valid (either no drop area is set or the drop occurred inside it)
|
573
|
+
*/
|
472
574
|
private testIfIsInDropArea(ctx: DropContext): boolean {
|
473
575
|
if (this.dropArea) {
|
474
576
|
const screenPoint = this.context.input.convertScreenspaceToRaycastSpace(ctx.screenposition.clone());
|
@@ -492,7 +594,11 @@
|
|
492
594
|
|
493
595
|
}
|
494
596
|
|
495
|
-
|
597
|
+
/**
|
598
|
+
* Attempts to convert a Polyhaven website URL to a direct glTF model download URL
|
599
|
+
* @param urlStr The original Polyhaven URL
|
600
|
+
* @returns The direct download URL for the glTF model if it's a valid Polyhaven asset URL, otherwise returns the original URL
|
601
|
+
*/
|
496
602
|
function tryResolvePolyhavenAssetUrl(urlStr: string) {
|
497
603
|
if (!urlStr.startsWith("https://polyhaven.com/")) return urlStr;
|
498
604
|
// Handle dropping polyhaven image url
|
@@ -506,10 +612,18 @@
|
|
506
612
|
return assetUrl;
|
507
613
|
}
|
508
614
|
|
509
|
-
|
510
|
-
|
615
|
+
/**
|
616
|
+
* Helper namespace for loading files and models from various sources
|
617
|
+
*/
|
511
618
|
namespace FileHelper {
|
512
619
|
|
620
|
+
/**
|
621
|
+
* Loads and processes a File object into a 3D model
|
622
|
+
* @param file The file to load (supported formats: gltf, glb, fbx, obj, usdz, vrm)
|
623
|
+
* @param context The application context
|
624
|
+
* @param args Additional arguments including a unique guid for instantiation
|
625
|
+
* @returns Promise containing the loaded model and its content hash, or null if loading failed
|
626
|
+
*/
|
513
627
|
export async function loadFile(file: File, context: Context, args: { guid: string }): Promise<{ model: Model, contentMD5: string } | null> {
|
514
628
|
const name = file.name.toLowerCase();
|
515
629
|
if (name.endsWith(".gltf") ||
|
@@ -544,6 +658,12 @@
|
|
544
658
|
return null;
|
545
659
|
}
|
546
660
|
|
661
|
+
/**
|
662
|
+
* Loads a 3D model from a URL with progress visualization
|
663
|
+
* @param url The URL to load the model from
|
664
|
+
* @param args Arguments including context, parent object, and optional placement information
|
665
|
+
* @returns Promise containing the loaded model and its content hash, or null if loading failed
|
666
|
+
*/
|
547
667
|
export async function loadFileFromURL(url: URL, args: { guid: string, context: Context, parent: Object3D, point?: Vec3, size?: Vec3 }): Promise<{ model: Model, contentMD5: string } | null> {
|
548
668
|
return new Promise(async (resolve, _reject) => {
|
549
669
|
|
@@ -1,6 +1,7 @@
|
|
1
1
|
import { Object3D, Quaternion, Vector3 } from "three";
|
2
2
|
|
3
3
|
import { isDevEnvironment } from "../engine/debug/index.js";
|
4
|
+
import { WaitForSeconds } from "../engine/engine_coroutine.js";
|
4
5
|
import { InstantiateOptions } from "../engine/engine_gameobject.js";
|
5
6
|
import { InstantiateIdProvider } from "../engine/engine_networking_instantiate.js";
|
6
7
|
import { serializable } from "../engine/engine_serialization_decorator.js";
|
@@ -30,17 +31,10 @@
|
|
30
31
|
|
31
32
|
/**
|
32
33
|
* The maximum number of objects that can be duplicated in the interval.
|
33
|
-
* @default 10
|
34
|
-
*/
|
35
|
-
@serializable()
|
36
|
-
limitCount = 10;
|
37
|
-
|
38
|
-
/**
|
39
|
-
* The interval in seconds in which the limitCount is reset.
|
40
34
|
* @default 60
|
41
35
|
*/
|
42
36
|
@serializable()
|
43
|
-
|
37
|
+
limitCount = 60;
|
44
38
|
|
45
39
|
private _currentCount = 0;
|
46
40
|
private _startPosition: Vector3 | null = null;
|
@@ -94,9 +88,10 @@
|
|
94
88
|
|
95
89
|
if (!this.gameObject.getComponentInParent(ObjectRaycaster))
|
96
90
|
this.gameObject.addComponent(ObjectRaycaster);
|
97
|
-
|
98
|
-
this.cloneLimitIntervalFn();
|
99
91
|
}
|
92
|
+
onEnable(): void {
|
93
|
+
this.startCoroutine(this.cloneLimitIntervalFn());
|
94
|
+
}
|
100
95
|
|
101
96
|
private _forwardPointerEvents: Map<Object3D, DragControls> = new Map();
|
102
97
|
|
@@ -131,7 +126,12 @@
|
|
131
126
|
}
|
132
127
|
}
|
133
128
|
else {
|
134
|
-
|
129
|
+
if (this._currentCount >= this.limitCount) {
|
130
|
+
console.warn(`[Duplicatable] Limit of ${this.limitCount} objects created within a few seconds reached. Please wait a moment before creating more objects.`);
|
131
|
+
}
|
132
|
+
else {
|
133
|
+
console.warn(`[Duplicatable] Could not duplicate object.`);
|
134
|
+
}
|
135
135
|
}
|
136
136
|
}
|
137
137
|
|
@@ -145,19 +145,21 @@
|
|
145
145
|
}
|
146
146
|
}
|
147
147
|
|
148
|
-
private cloneLimitIntervalFn() {
|
149
|
-
|
150
|
-
|
151
|
-
|
148
|
+
private *cloneLimitIntervalFn() {
|
149
|
+
while (this.activeAndEnabled && !this.destroyed) {
|
150
|
+
if (this._currentCount > 0) {
|
151
|
+
this._currentCount -= 1;
|
152
|
+
}
|
153
|
+
else if (this._currentCount < 0) {
|
154
|
+
this._currentCount = 0;
|
155
|
+
}
|
156
|
+
yield WaitForSeconds(1);
|
152
157
|
}
|
153
|
-
setTimeout(() => {
|
154
|
-
this.cloneLimitIntervalFn();
|
155
|
-
}, (this.limitInterval / this.limitCount) * 1000);
|
156
158
|
}
|
157
159
|
|
158
160
|
private handleDuplication(): Object3D | null {
|
159
161
|
if (!this.object) return null;
|
160
|
-
if (this._currentCount >= this.limitCount) return null;
|
162
|
+
if (this.limitCount > 0 && this._currentCount >= this.limitCount) return null;
|
161
163
|
if (this.object === this.gameObject) return null;
|
162
164
|
if (GameObject.isDestroyed(this.object)) {
|
163
165
|
this.object = null;
|
@@ -164,6 +164,11 @@
|
|
164
164
|
return this._url;
|
165
165
|
}
|
166
166
|
|
167
|
+
/** The name of the assigned url. This name is deduced from the url and might not reflect the actual name of the asset */
|
168
|
+
get urlName(): string {
|
169
|
+
return this._urlName;
|
170
|
+
};
|
171
|
+
|
167
172
|
/**
|
168
173
|
* @returns true if the uri is a valid URL (http, https, blob)
|
169
174
|
*/
|
@@ -180,6 +185,7 @@
|
|
180
185
|
private _asset: any;
|
181
186
|
private _glbRoot?: Object3D | null;
|
182
187
|
private _url: string;
|
188
|
+
private _urlName: string;
|
183
189
|
private _progressListeners: ProgressCallback[] = [];
|
184
190
|
|
185
191
|
private _hash?: string;
|
@@ -191,12 +197,27 @@
|
|
191
197
|
/** @internal */
|
192
198
|
constructor(uri: string, hash?: string, asset: any = null) {
|
193
199
|
this._url = uri;
|
200
|
+
|
201
|
+
const lastUriPart = uri.lastIndexOf("/");
|
202
|
+
if (lastUriPart >= 0) {
|
203
|
+
this._urlName = uri.substring(lastUriPart + 1);
|
204
|
+
// remove file extension
|
205
|
+
const lastDot = this._urlName.lastIndexOf(".");
|
206
|
+
if (lastDot >= 0) {
|
207
|
+
this._urlName = this._urlName.substring(0, lastDot);
|
208
|
+
}
|
209
|
+
}
|
210
|
+
else {
|
211
|
+
this._urlName = uri;
|
212
|
+
}
|
213
|
+
|
194
214
|
this._hash = hash;
|
195
215
|
if (uri.includes("?v="))
|
196
216
|
this._hashedUri = uri;
|
197
217
|
else
|
198
218
|
this._hashedUri = hash ? uri + "?v=" + hash : uri;
|
199
219
|
if (asset !== null) this.asset = asset;
|
220
|
+
|
200
221
|
registerPrefabProvider(this._url, this.onResolvePrefab.bind(this));
|
201
222
|
}
|
202
223
|
|
@@ -527,10 +548,20 @@
|
|
527
548
|
|
528
549
|
private loader: TextureLoader | null = null;
|
529
550
|
createTexture(): Promise<Texture | null> {
|
530
|
-
if (!this.url)
|
551
|
+
if (!this.url) {
|
552
|
+
console.error("Can not load texture without url");
|
553
|
+
return failedTexturePromise;
|
554
|
+
}
|
555
|
+
|
531
556
|
if (!this.loader) this.loader = new TextureLoader();
|
532
557
|
this.loader.setCrossOrigin("anonymous");
|
533
|
-
return this.loader.loadAsync(this.url)
|
558
|
+
return this.loader.loadAsync(this.url).then(res => {
|
559
|
+
if (res && !res.name?.length) {
|
560
|
+
// default name if no name is defined
|
561
|
+
res.name = this.url.split("/").pop() ?? this.url;
|
562
|
+
}
|
563
|
+
return res;
|
564
|
+
})
|
534
565
|
// return this.getBitmap().then((bitmap) => {
|
535
566
|
// if (bitmap) {
|
536
567
|
// const texture = new Texture(bitmap);
|
@@ -864,7 +864,7 @@
|
|
864
864
|
if (name.length > 3) offendingComponentName = name;
|
865
865
|
}
|
866
866
|
}
|
867
|
-
console.error(`Needle Engine dependencies failed to load:\n\n# Make sure you don't have circular imports in your scripts!\n
|
867
|
+
console.error(`Needle Engine dependencies failed to load:\n\n# Make sure you don't have circular imports in your scripts!\n\nPossible solutions: \n→ Replace @serializable(${offendingComponentName}) in your script with @serializable(Behaviour)\n→ If you only need type information try importing the type only, e.g: import { type ${offendingComponentName} }\n\n---`, err)
|
868
868
|
return;
|
869
869
|
}
|
870
870
|
if (!printedError) {
|
@@ -255,7 +255,26 @@
|
|
255
255
|
/** Adds an event listener for the specified event type. The callback will be called when the event is triggered.
|
256
256
|
* @param type The event type to listen for
|
257
257
|
* @param callback The callback to call when the event is triggered
|
258
|
-
* @param options The options for adding the event listener
|
258
|
+
* @param options The options for adding the event listener.
|
259
|
+
* @example Basic usage:
|
260
|
+
* ```ts
|
261
|
+
* input.addEventListener("pointerdown", (evt) => {
|
262
|
+
* console.log("Pointer down", evt.pointerId, evt.pointerType);
|
263
|
+
* });
|
264
|
+
* ```
|
265
|
+
* @example Adding a listener that is called after all other listeners
|
266
|
+
* By using a higher value for the queue the listener will be called after other listeners (default queue is 0).
|
267
|
+
* ```ts
|
268
|
+
* input.addEventListener("pointerdown", (evt) => {
|
269
|
+
* console.log("Pointer down", evt.pointerId, evt.pointerType);
|
270
|
+
* }, { queue: 10 });
|
271
|
+
* ```
|
272
|
+
* @example Adding a listener that is only called once
|
273
|
+
* ```ts
|
274
|
+
* input.addEventListener("pointerdown", (evt) => {
|
275
|
+
* console.log("Pointer down", evt.pointerId, evt.pointerType);
|
276
|
+
* }, { once: true });
|
277
|
+
* ```
|
259
278
|
*/
|
260
279
|
addEventListener(type: PointerEventNames, callback: PointerEventListener, options?: EventListenerOptions);
|
261
280
|
addEventListener(type: KeyboardEventNames, callback: KeyboardEventListener, options?: EventListenerOptions);
|
@@ -326,8 +326,7 @@
|
|
326
326
|
if (comp.enabled) {
|
327
327
|
safeInvoke(comp.__internalAwake.bind(comp));
|
328
328
|
if (comp.enabled) {
|
329
|
-
comp
|
330
|
-
comp.onEnable();
|
329
|
+
comp.__internalEnable();
|
331
330
|
}
|
332
331
|
}
|
333
332
|
}
|
@@ -343,9 +342,8 @@
|
|
343
342
|
|
344
343
|
let success = true;
|
345
344
|
if (go.children) {
|
346
|
-
const nextLevel = level + 1;
|
347
345
|
for (const ch of go.children) {
|
348
|
-
const res = updateIsActiveInHierarchyRecursiveRuntime(ch, activeInHierarchy, allowEventCall,
|
346
|
+
const res = updateIsActiveInHierarchyRecursiveRuntime(ch, activeInHierarchy, allowEventCall, level + 1);
|
349
347
|
if (res === false) success = false;
|
350
348
|
}
|
351
349
|
}
|
@@ -43,16 +43,30 @@
|
|
43
43
|
return this.clamp(value, 0, 1);
|
44
44
|
}
|
45
45
|
|
46
|
+
/**
|
47
|
+
* Linear interpolate
|
48
|
+
*/
|
46
49
|
lerp(value1: number, value2: number, t: number) {
|
47
50
|
t = t < 0 ? 0 : t;
|
48
51
|
t = t > 1 ? 1 : t;
|
49
52
|
return value1 + (value2 - value1) * t;
|
50
53
|
}
|
51
54
|
|
55
|
+
/**
|
56
|
+
*
|
57
|
+
*/
|
52
58
|
inverseLerp(value1: number, value2: number, t: number) {
|
53
59
|
return (t - value1) / (value2 - value1);
|
54
60
|
}
|
55
61
|
|
62
|
+
/**
|
63
|
+
* Remaps a value from one range to another.
|
64
|
+
* @param value The value to remap.
|
65
|
+
* @param min1 The minimum value of the current range.
|
66
|
+
* @param max1 The maximum value of the current range.
|
67
|
+
* @param min2 The minimum value of the target range.
|
68
|
+
* @param max2 The maximum value of the target range.
|
69
|
+
*/
|
56
70
|
remap(value: number, min1: number, max1: number, min2: number, max2: number) {
|
57
71
|
return min2 + (max2 - min2) * (value - min1) / (max1 - min1);
|
58
72
|
}
|
@@ -64,20 +78,24 @@
|
|
64
78
|
return value1;
|
65
79
|
}
|
66
80
|
|
81
|
+
|
82
|
+
|
83
|
+
readonly Rad2Deg = 180 / Math.PI;
|
84
|
+
readonly Deg2Rad = Math.PI / 180;
|
85
|
+
readonly Epsilon = 0.00001;
|
86
|
+
/**
|
87
|
+
* Converts radians to degrees
|
88
|
+
*/
|
67
89
|
toDegrees(radians: number) {
|
68
90
|
return radians * 180 / Math.PI;
|
69
91
|
}
|
70
|
-
|
71
|
-
|
72
|
-
|
92
|
+
/**
|
93
|
+
* Converts degrees to radians
|
94
|
+
*/
|
73
95
|
toRadians(degrees: number) {
|
74
96
|
return degrees * Math.PI / 180;
|
75
97
|
}
|
76
98
|
|
77
|
-
readonly Deg2Rad = Math.PI / 180;
|
78
|
-
|
79
|
-
readonly Epsilon = 0.00001;
|
80
|
-
|
81
99
|
tan(radians: number) {
|
82
100
|
return Math.tan(radians);
|
83
101
|
}
|
@@ -406,7 +406,7 @@
|
|
406
406
|
}
|
407
407
|
const hasOtherKeys = value !== undefined && Object.keys(value).length > 1;
|
408
408
|
if (!hasOtherKeys) {
|
409
|
-
addLog(LogType.Warn, `<strong>Missing serialization for object reference!</strong>\n\nPlease change to: \n@serializable(Object3D)\n${key}? : Object3D;\n\nin
|
409
|
+
addLog(LogType.Warn, `<strong>Missing serialization for object reference!</strong>\n\nPlease change to: \n@serializable(Object3D)\n${key}? : Object3D;\n\nin ${typeName}.ts\n<a href="https://docs.needle.tools/serializable" target="_blank">See documentation</a>`);
|
410
410
|
console.warn(typeName, key, obj[key], obj);
|
411
411
|
continue;
|
412
412
|
}
|
@@ -398,7 +398,7 @@
|
|
398
398
|
gl_FragColor = texture2D( map, uv);
|
399
399
|
// gl_FragColor = vec4(uv.xy, 0, 1);
|
400
400
|
}`;
|
401
|
-
private static
|
401
|
+
private static blitMaterial: ShaderMaterial | undefined = undefined;
|
402
402
|
|
403
403
|
/**
|
404
404
|
* Create a blit material for copying textures
|
@@ -419,27 +419,28 @@
|
|
419
419
|
* @returns the newly created, copied texture
|
420
420
|
*/
|
421
421
|
static copyTexture(texture: Texture, blitMaterial?: ShaderMaterial): Texture {
|
422
|
-
const material = blitMaterial ?? this.blipMaterial!;
|
423
|
-
|
424
|
-
material.uniforms.map.value = texture;
|
425
|
-
material.needsUpdate = true;
|
426
|
-
material.uniformsNeedUpdate = true;
|
427
|
-
|
428
422
|
// ensure that a blit material exists
|
429
|
-
if (!this.
|
430
|
-
this.
|
423
|
+
if (!this.blitMaterial) {
|
424
|
+
this.blitMaterial = new ShaderMaterial({
|
431
425
|
uniforms: { map: new Uniform(null) },
|
432
426
|
vertexShader: this.vertex,
|
433
427
|
fragmentShader: this.fragment
|
434
428
|
});
|
435
429
|
}
|
436
430
|
|
431
|
+
const material = blitMaterial || this.blitMaterial;
|
432
|
+
|
433
|
+
material.uniforms.map.value = texture;
|
434
|
+
material.needsUpdate = true;
|
435
|
+
material.uniformsNeedUpdate = true;
|
436
|
+
|
437
|
+
|
437
438
|
// ensure that the blit material has the correct vertex shader
|
438
439
|
const origVertexShader = material.vertexShader;
|
439
440
|
material.vertexShader = this.vertex;
|
440
441
|
|
441
442
|
if (!this.mesh)
|
442
|
-
this.mesh = new Mesh(this.planeGeometry, this.
|
443
|
+
this.mesh = new Mesh(this.planeGeometry, this.blitMaterial);
|
443
444
|
const mesh = this.mesh;
|
444
445
|
mesh.material = material;
|
445
446
|
mesh.frustumCulled = false;
|
@@ -474,15 +475,22 @@
|
|
474
475
|
// return new Texture(this.renderer.domElement);
|
475
476
|
// }
|
476
477
|
|
477
|
-
|
478
|
-
|
478
|
+
/**
|
479
|
+
* Copy a texture to a HTMLCanvasElement
|
480
|
+
* @param texture the texture convert
|
481
|
+
* @param force if true the texture will be copied to a new texture before converting
|
482
|
+
* @returns the HTMLCanvasElement with the texture or null if the texture could not be copied
|
483
|
+
*/
|
484
|
+
static textureToCanvas(texture: Texture, force: boolean = false): HTMLCanvasElement | null {
|
485
|
+
if (!texture) {
|
486
|
+
return null;
|
487
|
+
}
|
479
488
|
|
480
489
|
if (force === true || texture["isCompressedTexture"] === true) {
|
481
490
|
texture = copyTexture(texture);
|
482
491
|
}
|
483
492
|
const image = texture.image;
|
484
493
|
if (isImageBitmap(image)) {
|
485
|
-
|
486
494
|
const canvas = document.createElement('canvas');
|
487
495
|
canvas.width = image.width;
|
488
496
|
canvas.height = image.height;
|
@@ -494,8 +502,8 @@
|
|
494
502
|
}
|
495
503
|
context.drawImage(image, 0, 0, image.width, image.height, 0, 0, canvas.width, canvas.height);
|
496
504
|
return canvas;
|
505
|
+
}
|
497
506
|
|
498
|
-
}
|
499
507
|
return null;
|
500
508
|
}
|
501
509
|
}
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import type { QueryFilterFlags } from "@dimforge/rapier3d-compat";
|
1
|
+
import type { QueryFilterFlags, World } from "@dimforge/rapier3d-compat";
|
2
2
|
import { AnimationClip, Color, Material, Mesh, Object3D, Quaternion } from "three";
|
3
3
|
import { Vector3 } from "three";
|
4
4
|
import { type GLTF as THREE_GLTF } from "three/examples/jsm/loaders/GLTFLoader.js";
|
@@ -442,80 +442,241 @@
|
|
442
442
|
|
443
443
|
|
444
444
|
export interface IPhysicsEngine {
|
445
|
+
/** Initializes the physics engine */
|
445
446
|
initialize(): Promise<boolean>;
|
447
|
+
/** Indicates whether the physics engine has been initialized */
|
446
448
|
get isInitialized(): boolean;
|
449
|
+
/** Advances physics simulation by the given time step */
|
447
450
|
step(dt: number): void;
|
448
451
|
postStep();
|
452
|
+
/** Indicates whether the physics engine is currently updating */
|
449
453
|
get isUpdating(): boolean;
|
450
|
-
/**
|
454
|
+
/** Clears all cached data (e.g., mesh data when creating scaled mesh colliders) */
|
451
455
|
clearCaches();
|
452
456
|
|
457
|
+
/** Enables or disables the physics engine */
|
453
458
|
enabled: boolean;
|
454
|
-
|
459
|
+
/** Returns the underlying physics world object */
|
460
|
+
get world(): World | undefined;
|
455
461
|
|
462
|
+
/** Sets the gravity vector for the physics simulation */
|
456
463
|
set gravity(vec3: Vec3);
|
464
|
+
/** Gets the current gravity vector */
|
457
465
|
get gravity(): Vec3;
|
458
466
|
|
459
|
-
/**
|
467
|
+
/**
|
468
|
+
* Gets the rapier physics body for a Needle component
|
469
|
+
* @param obj The collider or rigidbody component
|
470
|
+
* @returns The underlying physics body or null if not found
|
471
|
+
*/
|
460
472
|
getBody(obj: ICollider | IRigidbody): null | any;
|
461
|
-
/**
|
473
|
+
/**
|
474
|
+
* Gets the Needle Engine component for a rapier physics object
|
475
|
+
* @param rapierObject The rapier physics object
|
476
|
+
* @returns The associated component or null if not found
|
477
|
+
*/
|
462
478
|
getComponent(rapierObject: object): IComponent | null;
|
463
479
|
|
464
|
-
/
|
465
|
-
|
466
|
-
* @param origin
|
467
|
-
* @param direction
|
468
|
-
* @param options
|
480
|
+
/**
|
481
|
+
* Performs a fast raycast against physics colliders
|
482
|
+
* @param origin Ray origin in screen or worldspace
|
483
|
+
* @param direction Ray direction in worldspace
|
484
|
+
* @param options Additional raycast configuration options
|
485
|
+
* @returns Raycast result containing hit point and collider, or null if no hit
|
469
486
|
*/
|
470
487
|
raycast(origin?: Vec2 | Vec3, direction?: Vec3, options?: {
|
471
488
|
maxDistance?: number,
|
472
489
|
/** True if you want to also hit objects when the raycast starts from inside a collider */
|
473
490
|
solid?: boolean,
|
474
491
|
queryFilterFlags?: QueryFilterFlags,
|
475
|
-
/**
|
476
|
-
*
|
492
|
+
/**
|
493
|
+
* Raycast filter groups. Groups are used to apply the collision group rules for the scene query.
|
494
|
+
* The scene query will only consider hits with colliders with collision groups compatible with
|
495
|
+
* this collision group (using the bitwise test described in the collision groups section).
|
496
|
+
* For example membership 0x0001 and filter 0x0002 should be 0x00010002
|
497
|
+
* @see https://rapier.rs/docs/user_guides/javascript/colliders#collision-groups-and-solver-groups
|
498
|
+
*/
|
477
499
|
filterGroups?: number,
|
478
|
-
/**
|
500
|
+
/**
|
501
|
+
* Predicate to filter colliders in raycast results
|
502
|
+
* @param collider The collider being tested
|
503
|
+
* @returns False to ignore this collider, true to include it
|
504
|
+
*/
|
479
505
|
filterPredicate?: (collider: ICollider) => boolean
|
480
506
|
}): RaycastResult;
|
481
|
-
|
507
|
+
|
508
|
+
/**
|
509
|
+
* Performs a raycast that also returns the normal vector at the hit point
|
510
|
+
* @param origin Ray origin in screen or worldspace
|
511
|
+
* @param direction Ray direction in worldspace
|
512
|
+
* @param options Additional raycast configuration options
|
513
|
+
* @returns Raycast result containing hit point, normal, and collider, or null if no hit
|
514
|
+
*/
|
482
515
|
raycastAndGetNormal(origin?: Vec2 | Vec3, direction?: Vec3, options?: {
|
483
516
|
maxDistance?: number,
|
484
517
|
/** True if you want to also hit objects when the raycast starts from inside a collider */
|
485
518
|
solid?: boolean,
|
486
519
|
queryFilterFlags?: QueryFilterFlags,
|
487
|
-
/**
|
488
|
-
*
|
520
|
+
/**
|
521
|
+
* Raycast filter groups. Groups are used to apply the collision group rules for the scene query.
|
522
|
+
* The scene query will only consider hits with colliders with collision groups compatible with
|
523
|
+
* this collision group (using the bitwise test described in the collision groups section).
|
524
|
+
* For example membership 0x0001 and filter 0x0002 should be 0x00010002
|
525
|
+
* @see https://rapier.rs/docs/user_guides/javascript/colliders#collision-groups-and-solver-groups
|
526
|
+
*/
|
489
527
|
filterGroups?: number,
|
490
|
-
/**
|
528
|
+
/**
|
529
|
+
* Predicate to filter colliders in raycast results
|
530
|
+
* @param collider The collider being tested
|
531
|
+
* @returns False to ignore this collider, true to include it
|
532
|
+
*/
|
491
533
|
filterPredicate?: (collider: ICollider) => boolean
|
492
534
|
}): RaycastResult;
|
535
|
+
|
536
|
+
/**
|
537
|
+
* Finds all colliders within a sphere
|
538
|
+
* @param point The center point of the sphere
|
539
|
+
* @param radius The radius of the sphere
|
540
|
+
* @returns Array of objects that overlap with the sphere
|
541
|
+
*/
|
493
542
|
sphereOverlap(point: Vector3, radius: number): Array<SphereOverlapResult>;
|
494
543
|
|
495
544
|
// Collider methods
|
545
|
+
/**
|
546
|
+
* Adds a sphere collider to the physics world
|
547
|
+
* @param collider The collider component to add
|
548
|
+
*/
|
496
549
|
addSphereCollider(collider: ICollider);
|
550
|
+
|
551
|
+
/**
|
552
|
+
* Adds a box collider to the physics world
|
553
|
+
* @param collider The collider component to add
|
554
|
+
* @param size The size of the box
|
555
|
+
*/
|
497
556
|
addBoxCollider(collider: ICollider, size: Vector3);
|
557
|
+
|
558
|
+
/**
|
559
|
+
* Adds a capsule collider to the physics world
|
560
|
+
* @param collider The collider component to add
|
561
|
+
* @param radius The radius of the capsule
|
562
|
+
* @param height The height of the capsule
|
563
|
+
*/
|
498
564
|
addCapsuleCollider(collider: ICollider, radius: number, height: number);
|
565
|
+
|
566
|
+
/**
|
567
|
+
* Adds a mesh collider to the physics world
|
568
|
+
* @param collider The collider component to add
|
569
|
+
* @param mesh The mesh to use for collision
|
570
|
+
* @param convex Whether the collision mesh should be treated as convex
|
571
|
+
* @param scale Optional scale to apply to the mesh
|
572
|
+
*/
|
499
573
|
addMeshCollider(collider: ICollider, mesh: Mesh, convex: boolean, scale?: Vector3 | undefined);
|
500
574
|
|
575
|
+
/**
|
576
|
+
* Updates the physics material properties of a collider
|
577
|
+
* @param collider The collider to update
|
578
|
+
*/
|
501
579
|
updatePhysicsMaterial(collider: ICollider);
|
502
580
|
|
503
581
|
// Rigidbody methods
|
582
|
+
/**
|
583
|
+
* Wakes up a sleeping rigidbody
|
584
|
+
* @param rb The rigidbody to wake up
|
585
|
+
*/
|
504
586
|
wakeup(rb: IRigidbody);
|
587
|
+
|
588
|
+
/**
|
589
|
+
* Checks if a rigidbody is currently sleeping
|
590
|
+
* @param rb The rigidbody to check
|
591
|
+
* @returns Whether the rigidbody is sleeping or undefined if cannot be determined
|
592
|
+
*/
|
505
593
|
isSleeping(rb: IRigidbody): boolean | undefined;
|
594
|
+
|
595
|
+
/**
|
596
|
+
* Updates the physical properties of a rigidbody or collider
|
597
|
+
* @param rb The rigidbody or collider to update
|
598
|
+
*/
|
506
599
|
updateProperties(rb: IRigidbody | ICollider);
|
600
|
+
|
601
|
+
/**
|
602
|
+
* Resets all forces acting on a rigidbody
|
603
|
+
* @param rb The rigidbody to reset forces on
|
604
|
+
* @param wakeup Whether to wake up the rigidbody
|
605
|
+
*/
|
507
606
|
resetForces(rb: IRigidbody, wakeup: boolean);
|
607
|
+
|
608
|
+
/**
|
609
|
+
* Resets all torques acting on a rigidbody
|
610
|
+
* @param rb The rigidbody to reset torques on
|
611
|
+
* @param wakeup Whether to wake up the rigidbody
|
612
|
+
*/
|
508
613
|
resetTorques(rb: IRigidbody, wakeup: boolean);
|
614
|
+
|
615
|
+
/**
|
616
|
+
* Adds a continuous force to a rigidbody
|
617
|
+
* @param rb The rigidbody to add force to
|
618
|
+
* @param vec The force vector to add
|
619
|
+
* @param wakeup Whether to wake up the rigidbody
|
620
|
+
*/
|
509
621
|
addForce(rb: IRigidbody, vec: Vec3, wakeup: boolean);
|
622
|
+
|
623
|
+
/**
|
624
|
+
* Applies an instantaneous impulse to a rigidbody
|
625
|
+
* @param rb The rigidbody to apply impulse to
|
626
|
+
* @param vec The impulse vector to apply
|
627
|
+
* @param wakeup Whether to wake up the rigidbody
|
628
|
+
*/
|
510
629
|
applyImpulse(rb: IRigidbody, vec: Vec3, wakeup: boolean);
|
511
|
-
|
630
|
+
|
631
|
+
/**
|
632
|
+
* Gets the linear velocity of a rigidbody or the rigidbody attached to a collider
|
633
|
+
* @param rb The rigidbody or collider to get velocity from
|
634
|
+
* @returns The linear velocity vector or null if not available
|
635
|
+
*/
|
512
636
|
getLinearVelocity(rb: IRigidbody | ICollider): Vec3 | null;
|
637
|
+
|
638
|
+
/**
|
639
|
+
* Gets the angular velocity of a rigidbody
|
640
|
+
* @param rb The rigidbody to get angular velocity from
|
641
|
+
* @returns The angular velocity vector or null if not available
|
642
|
+
*/
|
513
643
|
getAngularVelocity(rb: IRigidbody): Vec3 | null;
|
644
|
+
|
645
|
+
/**
|
646
|
+
* Sets the angular velocity of a rigidbody
|
647
|
+
* @param rb The rigidbody to set angular velocity for
|
648
|
+
* @param vec The angular velocity vector to set
|
649
|
+
* @param wakeup Whether to wake up the rigidbody
|
650
|
+
*/
|
514
651
|
setAngularVelocity(rb: IRigidbody, vec: Vec3, wakeup: boolean);
|
652
|
+
|
653
|
+
/**
|
654
|
+
* Sets the linear velocity of a rigidbody
|
655
|
+
* @param rb The rigidbody to set linear velocity for
|
656
|
+
* @param vec The linear velocity vector to set
|
657
|
+
* @param wakeup Whether to wake up the rigidbody
|
658
|
+
*/
|
515
659
|
setLinearVelocity(rb: IRigidbody, vec: Vec3, wakeup: boolean);
|
516
660
|
|
661
|
+
/**
|
662
|
+
* Updates the position and/or rotation of a physics body
|
663
|
+
* @param comp The collider or rigidbody component to update
|
664
|
+
* @param translation Whether to update the position
|
665
|
+
* @param rotation Whether to update the rotation
|
666
|
+
*/
|
517
667
|
updateBody(comp: ICollider | IRigidbody, translation: boolean, rotation: boolean);
|
668
|
+
|
669
|
+
/**
|
670
|
+
* Removes a physics body from the simulation
|
671
|
+
* @param body The component whose physics body should be removed
|
672
|
+
*/
|
518
673
|
removeBody(body: IComponent);
|
674
|
+
|
675
|
+
/**
|
676
|
+
* Gets the physics body for a component
|
677
|
+
* @param obj The collider or rigidbody component
|
678
|
+
* @returns The underlying physics body or null if not found
|
679
|
+
*/
|
519
680
|
getBody(obj: ICollider | IRigidbody): null | any;
|
520
681
|
|
521
682
|
// Joints
|
@@ -10,6 +10,7 @@
|
|
10
10
|
import { registerFrameEventCallback } from "./engine_lifecycle_functions_internal.js";
|
11
11
|
import { Context, FrameEvent } from "./engine_setup.js";
|
12
12
|
import { ICamera } from "./engine_types.js";
|
13
|
+
import { DeviceUtilities } from "./engine_utils.js";
|
13
14
|
import { updateTextureFromXRFrame } from "./engine_utils_screenshot.xr.js";
|
14
15
|
import { RGBAColor } from "./js-extensions/index.js";
|
15
16
|
import { setCustomVisibility } from "./js-extensions/Layers.js";
|
@@ -395,8 +396,22 @@
|
|
395
396
|
|
396
397
|
if ("download_filename" in opts && opts.download_filename) {
|
397
398
|
let download_name = opts.download_filename;
|
398
|
-
|
399
|
-
|
399
|
+
// On mobile we don't want to see the dialogue for every screenshot
|
400
|
+
if (DeviceUtilities.isMobileDevice() && typeof window !== "undefined") {
|
401
|
+
const key = download_name + "_screenshots";
|
402
|
+
const parts = download_name.split(".");
|
403
|
+
const ext = parts.pop()?.toLowerCase();
|
404
|
+
let count = 0;
|
405
|
+
if (localStorage.getItem(key)) {
|
406
|
+
count = parseInt(sessionStorage.getItem(key) || "0");
|
407
|
+
}
|
408
|
+
if (count > 0) {
|
409
|
+
// const timestamp = new Date().toLocaleString();
|
410
|
+
download_name = `${parts.join()}-${count}.${ext}`;
|
411
|
+
}
|
412
|
+
count += 1;
|
413
|
+
sessionStorage.setItem(key, count.toString());
|
414
|
+
}
|
400
415
|
saveImage(dataUrl, download_name);
|
401
416
|
}
|
402
417
|
return dataUrl;
|
@@ -424,7 +439,6 @@
|
|
424
439
|
|
425
440
|
|
426
441
|
|
427
|
-
|
428
442
|
// trim to transparent pixels
|
429
443
|
function trimCanvas(originalCanvas: HTMLCanvasElement): HTMLCanvasElement | null {
|
430
444
|
if (!("document" in globalThis)) return null;
|
@@ -59,6 +59,7 @@
|
|
59
59
|
return ctx.scene.getComponent(EventSystem);
|
60
60
|
}
|
61
61
|
|
62
|
+
/** Get the currently active event system */
|
62
63
|
static get instance(): EventSystem | null {
|
63
64
|
return this.get(Context.Current);
|
64
65
|
}
|
@@ -75,7 +76,10 @@
|
|
75
76
|
}
|
76
77
|
}
|
77
78
|
|
78
|
-
get hasActiveUI() {
|
79
|
+
get hasActiveUI() {
|
80
|
+
return this.currentActiveMeshUIComponents.length > 0;
|
81
|
+
}
|
82
|
+
|
79
83
|
get isHoveringObjects() { return this.hoveredByID.size > 0; }
|
80
84
|
|
81
85
|
awake(): void {
|
@@ -424,7 +428,6 @@
|
|
424
428
|
const res = UIRaycastUtils.isInteractable(actualGo, this.out);
|
425
429
|
if (!res) return false;
|
426
430
|
canvasGroup = this.out.canvasGroup ?? null;
|
427
|
-
|
428
431
|
const handled = this.handleMeshUIIntersection(object, pressedOrClicked);
|
429
432
|
if (!clicked && handled) {
|
430
433
|
// return true;
|
@@ -753,7 +756,6 @@
|
|
753
756
|
}
|
754
757
|
|
755
758
|
private resetMeshUIStates() {
|
756
|
-
|
757
759
|
if (this.context.input.getPointerPressedCount() > 0) {
|
758
760
|
MeshUIHelper.resetLastSelected();
|
759
761
|
}
|
@@ -816,7 +818,7 @@
|
|
816
818
|
let foundBlock: Object3D | null = null;
|
817
819
|
|
818
820
|
if (intersect) {
|
819
|
-
foundBlock = this.
|
821
|
+
foundBlock = this.findBlockOrTextInParent(intersect);
|
820
822
|
// console.log(intersect, "-- found block:", foundBlock)
|
821
823
|
if (foundBlock && foundBlock !== this.lastSelected) {
|
822
824
|
const interactable = foundBlock["interactable"];
|
@@ -840,13 +842,13 @@
|
|
840
842
|
this.needsUpdate = true;
|
841
843
|
}
|
842
844
|
|
843
|
-
static
|
845
|
+
static findBlockOrTextInParent(elem: any): Object3D | null {
|
844
846
|
if (!elem) return null;
|
845
|
-
if (elem.isBlock) {
|
847
|
+
if (elem.isBlock || (elem.isText)) {
|
846
848
|
// @TODO : Replace states managements
|
847
849
|
// if (Object.keys(elem.states).length > 0)
|
848
850
|
return elem;
|
849
851
|
}
|
850
|
-
return this.
|
852
|
+
return this.findBlockOrTextInParent(elem.parent);
|
851
853
|
}
|
852
854
|
}
|
@@ -24,82 +24,86 @@
|
|
24
24
|
const debug = getParam("debuglights");
|
25
25
|
|
26
26
|
|
27
|
-
/
|
28
|
-
|
29
|
-
|
27
|
+
/**
|
28
|
+
* Defines the type of light in a scene.
|
29
|
+
*/
|
30
30
|
export enum LightType {
|
31
|
-
/
|
32
|
-
/// <para>The light is a spot light.</para>
|
33
|
-
/// </summary>
|
31
|
+
/** Spot light that emits light in a cone shape */
|
34
32
|
Spot = 0,
|
35
|
-
/
|
36
|
-
/// <para>The light is a directional light.</para>
|
37
|
-
/// </summary>
|
33
|
+
/** Directional light that emits parallel light rays in a specific direction */
|
38
34
|
Directional = 1,
|
39
|
-
/
|
40
|
-
/// <para>The light is a point light.</para>
|
41
|
-
/// </summary>
|
35
|
+
/** Point light that emits light in all directions from a single point */
|
42
36
|
Point = 2,
|
37
|
+
/** Area light */
|
43
38
|
Area = 3,
|
44
|
-
/
|
45
|
-
/// <para>The light is a rectangle shaped area light. It affects only baked lightmaps and lightprobes.</para>
|
46
|
-
/// </summary>
|
39
|
+
/** Rectangle shaped area light that only affects baked lightmaps and light probes */
|
47
40
|
Rectangle = 3,
|
48
|
-
/
|
49
|
-
/// <para>The light is a disc shaped area light. It affects only baked lightmaps and lightprobes.</para>
|
50
|
-
/// </summary>
|
41
|
+
/** Disc shaped area light that only affects baked lightmaps and light probes */
|
51
42
|
Disc = 4,
|
52
43
|
}
|
44
|
+
|
45
|
+
/**
|
46
|
+
* Defines how a light contributes to the scene lighting.
|
47
|
+
*/
|
53
48
|
export enum LightmapBakeType {
|
54
|
-
/
|
55
|
-
/// <para>Realtime lights cast run time light and shadows. They can change position, orientation, color, brightness, and many other properties at run time. No lighting gets baked into lightmaps or light probes..</para>
|
56
|
-
/// </summary>
|
49
|
+
/** Light affects the scene in real-time with no baking */
|
57
50
|
Realtime = 4,
|
58
|
-
/
|
59
|
-
/// <para>Baked lights cannot move or change in any way during run time. All lighting for static objects gets baked into lightmaps. Lighting and shadows for dynamic objects gets baked into Light Probes.</para>
|
60
|
-
/// </summary>
|
51
|
+
/** Light is completely baked into lightmaps and light probes */
|
61
52
|
Baked = 2,
|
62
|
-
/
|
63
|
-
/// <para>Mixed lights allow a mix of realtime and baked lighting, based on the Mixed Lighting Mode used. These lights cannot move, but can change color and intensity at run time. Changes to color and intensity only affect direct lighting as indirect lighting gets baked. If using Subtractive mode, changes to color or intensity are not calculated at run time on static objects.</para>
|
64
|
-
/// </summary>
|
53
|
+
/** Combines aspects of realtime and baked lighting */
|
65
54
|
Mixed = 1,
|
66
55
|
}
|
67
56
|
|
68
|
-
/
|
69
|
-
|
70
|
-
|
57
|
+
/**
|
58
|
+
* Defines the shadow casting options for a Light.
|
59
|
+
* @enum {number}
|
60
|
+
*/
|
71
61
|
enum LightShadows {
|
72
|
-
/
|
73
|
-
/// <para>Do not cast shadows (default).</para>
|
74
|
-
/// </summary>
|
62
|
+
/** No shadows are cast */
|
75
63
|
None = 0,
|
76
|
-
/
|
77
|
-
/// <para>Cast "hard" shadows (with no shadow filtering).</para>
|
78
|
-
/// </summary>
|
64
|
+
/** Hard-edged shadows without filtering */
|
79
65
|
Hard = 1,
|
80
|
-
/
|
81
|
-
/// <para>Cast "soft" shadows (with 4x PCF filtering).</para>
|
82
|
-
/// </summary>
|
66
|
+
/** Soft shadows with PCF filtering */
|
83
67
|
Soft = 2,
|
84
68
|
}
|
85
69
|
|
86
|
-
/**
|
87
|
-
*
|
88
|
-
*
|
89
|
-
*
|
90
|
-
*
|
70
|
+
/**
|
71
|
+
* The Light component creates a light source in the scene.
|
72
|
+
* Supports directional, spot, and point light types with various customization options.
|
73
|
+
* Lights can cast shadows with configurable settings and can be set to baked or realtime rendering.
|
74
|
+
*
|
75
|
+
* Debug mode can be enabled with the URL parameter `?debuglights`, which shows
|
76
|
+
* additional console output and visual helpers for lights.
|
77
|
+
*
|
91
78
|
* @category Rendering
|
92
79
|
* @group Components
|
93
80
|
*/
|
94
81
|
export class Light extends Behaviour implements ILight {
|
95
82
|
|
83
|
+
/**
|
84
|
+
* The type of light (spot, directional, point, etc.)
|
85
|
+
*/
|
96
86
|
@serializable()
|
97
87
|
private type: LightType = 0;
|
98
88
|
|
89
|
+
/**
|
90
|
+
* The maximum distance the light affects
|
91
|
+
*/
|
99
92
|
public range: number = 1;
|
93
|
+
|
94
|
+
/**
|
95
|
+
* The full outer angle of the spotlight cone in degrees
|
96
|
+
*/
|
100
97
|
public spotAngle: number = 1;
|
98
|
+
|
99
|
+
/**
|
100
|
+
* The angle of the inner cone in degrees for soft-edge spotlights
|
101
|
+
*/
|
101
102
|
public innerSpotAngle: number = 1;
|
102
103
|
|
104
|
+
/**
|
105
|
+
* The color of the light
|
106
|
+
*/
|
103
107
|
@serializable(Color)
|
104
108
|
set color(val: Color) {
|
105
109
|
this._color = val;
|
@@ -113,6 +117,9 @@
|
|
113
117
|
}
|
114
118
|
public _color: Color = new Color(0xffffff);
|
115
119
|
|
120
|
+
/**
|
121
|
+
* The near plane distance for shadow projection
|
122
|
+
*/
|
116
123
|
@serializable()
|
117
124
|
set shadowNearPlane(val: number) {
|
118
125
|
if (val === this._shadowNearPlane) return;
|
@@ -125,6 +132,9 @@
|
|
125
132
|
get shadowNearPlane(): number { return this._shadowNearPlane; }
|
126
133
|
private _shadowNearPlane: number = .1;
|
127
134
|
|
135
|
+
/**
|
136
|
+
* Shadow bias value to reduce shadow acne and peter-panning
|
137
|
+
*/
|
128
138
|
@serializable()
|
129
139
|
set shadowBias(val: number) {
|
130
140
|
if (val === this._shadowBias) return;
|
@@ -137,6 +147,9 @@
|
|
137
147
|
get shadowBias(): number { return this._shadowBias; }
|
138
148
|
private _shadowBias: number = 0;
|
139
149
|
|
150
|
+
/**
|
151
|
+
* Shadow normal bias to reduce shadow acne on sloped surfaces
|
152
|
+
*/
|
140
153
|
@serializable()
|
141
154
|
set shadowNormalBias(val: number) {
|
142
155
|
if (val === this._shadowNormalBias) return;
|
@@ -152,6 +165,9 @@
|
|
152
165
|
/** when enabled this will remove the multiplication when setting the shadow bias settings initially */
|
153
166
|
private _overrideShadowBiasSettings: boolean = false;
|
154
167
|
|
168
|
+
/**
|
169
|
+
* Shadow casting mode (None, Hard, or Soft)
|
170
|
+
*/
|
155
171
|
@serializable()
|
156
172
|
set shadows(val: LightShadows) {
|
157
173
|
this._shadows = val;
|
@@ -163,9 +179,16 @@
|
|
163
179
|
get shadows(): LightShadows { return this._shadows; }
|
164
180
|
private _shadows: LightShadows = 1;
|
165
181
|
|
182
|
+
/**
|
183
|
+
* Determines if the light contributes to realtime lighting, baked lighting, or a mix
|
184
|
+
*/
|
166
185
|
@serializable()
|
167
186
|
private lightmapBakeType: LightmapBakeType = LightmapBakeType.Realtime;
|
168
187
|
|
188
|
+
/**
|
189
|
+
* Brightness of the light. In WebXR experiences, the intensity is automatically
|
190
|
+
* adjusted based on the AR session scale to maintain consistent lighting.
|
191
|
+
*/
|
169
192
|
@serializable()
|
170
193
|
set intensity(val: number) {
|
171
194
|
this._intensity = val;
|
@@ -184,6 +207,9 @@
|
|
184
207
|
get intensity(): number { return this._intensity; }
|
185
208
|
private _intensity: number = -1;
|
186
209
|
|
210
|
+
/**
|
211
|
+
* Maximum distance the shadow is projected
|
212
|
+
*/
|
187
213
|
@serializable()
|
188
214
|
get shadowDistance(): number {
|
189
215
|
const light = this.light;
|
@@ -207,6 +233,9 @@
|
|
207
233
|
private shadowWidth?: number;
|
208
234
|
private shadowHeight?: number;
|
209
235
|
|
236
|
+
/**
|
237
|
+
* Resolution of the shadow map in pixels (width and height)
|
238
|
+
*/
|
210
239
|
@serializable()
|
211
240
|
get shadowResolution(): number {
|
212
241
|
const light = this.light;
|
@@ -226,10 +255,16 @@
|
|
226
255
|
}
|
227
256
|
private _shadowResolution?: number = undefined;
|
228
257
|
|
258
|
+
/**
|
259
|
+
* Whether this light's illumination is entirely baked into lightmaps
|
260
|
+
*/
|
229
261
|
get isBaked() {
|
230
262
|
return this.lightmapBakeType === LightmapBakeType.Baked;
|
231
263
|
}
|
232
264
|
|
265
|
+
/**
|
266
|
+
* Checks if the GameObject itself is a {@link ThreeLight} object
|
267
|
+
*/
|
233
268
|
private get selfIsLight(): boolean {
|
234
269
|
if (this.gameObject["isLight"] === true) return true;
|
235
270
|
switch (this.gameObject.type) {
|
@@ -241,8 +276,16 @@
|
|
241
276
|
return false;
|
242
277
|
}
|
243
278
|
|
279
|
+
/**
|
280
|
+
* The underlying three.js {@link ThreeLight} instance
|
281
|
+
*/
|
244
282
|
private light: ThreeLight | undefined = undefined;
|
245
283
|
|
284
|
+
/**
|
285
|
+
* Gets the world position of the light
|
286
|
+
* @param vec Vector3 to store the result
|
287
|
+
* @returns The world position as a Vector3
|
288
|
+
*/
|
246
289
|
public getWorldPosition(vec: Vector3): Vector3 {
|
247
290
|
if (this.light) {
|
248
291
|
if (this.type === LightType.Directional) {
|
@@ -311,6 +354,10 @@
|
|
311
354
|
// this.updateIntensity();
|
312
355
|
}
|
313
356
|
|
357
|
+
/**
|
358
|
+
* Creates the appropriate three.js light based on the configured light type
|
359
|
+
* and applies all settings like shadows, intensity, and color.
|
360
|
+
*/
|
314
361
|
createLight() {
|
315
362
|
const lightAlreadyCreated = this.selfIsLight;
|
316
363
|
|
@@ -447,6 +494,10 @@
|
|
447
494
|
|
448
495
|
}
|
449
496
|
|
497
|
+
/**
|
498
|
+
* Coroutine that updates the main light reference in the context
|
499
|
+
* if this directional light should be the main light
|
500
|
+
*/
|
450
501
|
*updateMainLightRoutine() {
|
451
502
|
while (true) {
|
452
503
|
if (this.type === LightType.Directional) {
|
@@ -459,8 +510,14 @@
|
|
459
510
|
}
|
460
511
|
}
|
461
512
|
|
513
|
+
/**
|
514
|
+
* Controls whether the renderer's shadow map type can be changed when soft shadows are used
|
515
|
+
*/
|
462
516
|
static allowChangingRendererShadowMapType: boolean = true;
|
463
517
|
|
518
|
+
/**
|
519
|
+
* Updates shadow settings based on whether the shadows are set to hard or soft
|
520
|
+
*/
|
464
521
|
private updateShadowSoftHard() {
|
465
522
|
if (!this.light) return;
|
466
523
|
if (!this.light.shadow) return;
|
@@ -486,6 +543,10 @@
|
|
486
543
|
}
|
487
544
|
}
|
488
545
|
|
546
|
+
/**
|
547
|
+
* Configures a directional light by adding and positioning its target
|
548
|
+
* @param dirLight The directional light to set up
|
549
|
+
*/
|
489
550
|
private setDirectionalLight(dirLight: DirectionalLight) {
|
490
551
|
dirLight.add(dirLight.target);
|
491
552
|
dirLight.target.position.set(0, 0, -1);
|
@@ -7,7 +7,7 @@
|
|
7
7
|
export * from "./engine-schemes/api.js";
|
8
8
|
|
9
9
|
// make accessible for external javascript
|
10
|
-
import { Context, loadSync, NeedleXRSession, onBeforeRender, onStart, onUpdate, VERSION } from "./engine/api.js";
|
10
|
+
import { Context, loadSync, NeedleXRSession, onAfterRender, onBeforeRender, onClear, onDestroy, onInitialized, onStart, onUpdate, VERSION } from "./engine/api.js";
|
11
11
|
const Needle = {
|
12
12
|
VERSION: VERSION,
|
13
13
|
Context: Context,
|
@@ -15,11 +15,13 @@
|
|
15
15
|
gltf: {
|
16
16
|
loadFromURL: loadSync,
|
17
17
|
},
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
18
|
+
onStart: onStart,
|
19
|
+
onUpdate: onUpdate,
|
20
|
+
onBeforeRender: onBeforeRender,
|
21
|
+
onAfterRender: onAfterRender,
|
22
|
+
onInitializedContext: onInitialized,
|
23
|
+
onDestroyContext: onDestroy,
|
24
|
+
onClearContext: onClear,
|
23
25
|
};
|
24
26
|
if (globalThis["Needle"]?.VERSION !== undefined) {
|
25
27
|
console.warn(`Needle Engine is already imported: ${globalThis["Needle"].VERSION}`);
|
@@ -4,50 +4,68 @@
|
|
4
4
|
import { Behaviour } from './Component.js';
|
5
5
|
|
6
6
|
/**
|
7
|
-
*
|
7
|
+
* Provides configuration options for the built-in Needle Menu.
|
8
|
+
* Needle Menu uses HTML in 2D mode, and automatically switches to a 3D menu in VR/AR mode.
|
9
|
+
*
|
10
|
+
* Controls display options, button visibility, and menu positioning.
|
8
11
|
* From code, you can access the menu via {@link Context.menu}.
|
9
12
|
* @category User Interface
|
10
13
|
* @group Components
|
11
14
|
**/
|
12
15
|
export class NeedleMenu extends Behaviour {
|
13
16
|
|
17
|
+
/**
|
18
|
+
* Determines the vertical positioning of the menu on the screen
|
19
|
+
*/
|
14
20
|
@serializable()
|
15
21
|
position: "top" | "bottom" = "bottom";
|
16
22
|
|
17
|
-
/**
|
23
|
+
/**
|
24
|
+
* Controls the visibility of the Needle logo in the menu (requires PRO license)
|
25
|
+
*/
|
18
26
|
@serializable()
|
19
27
|
showNeedleLogo: boolean = true;
|
20
28
|
|
21
|
-
/**
|
29
|
+
/**
|
30
|
+
* When enabled, displays the menu in VR/AR mode when the user looks up
|
22
31
|
* @default undefined
|
23
|
-
|
32
|
+
*/
|
24
33
|
@serializable()
|
25
34
|
showSpatialMenu?: boolean;
|
26
35
|
|
27
|
-
/**
|
36
|
+
/**
|
37
|
+
* When enabled, adds a fullscreen toggle button to the menu
|
28
38
|
* @default undefined
|
29
|
-
|
39
|
+
*/
|
30
40
|
@serializable()
|
31
41
|
createFullscreenButton?: boolean;
|
32
|
-
|
42
|
+
|
43
|
+
/**
|
44
|
+
* When enabled, adds an audio mute/unmute button to the menu
|
33
45
|
* @default undefined
|
34
|
-
|
46
|
+
*/
|
35
47
|
@serializable()
|
36
48
|
createMuteButton?: boolean;
|
37
49
|
|
38
50
|
/**
|
39
|
-
* When enabled a button to
|
51
|
+
* When enabled, adds a button to display a QR code for sharing the application.
|
52
|
+
* The QR code is only displayed on desktop devices.
|
40
53
|
* @default undefined
|
41
54
|
*/
|
42
55
|
@serializable()
|
43
56
|
createQRCodeButton?: boolean;
|
44
57
|
|
45
|
-
/**
|
58
|
+
/**
|
59
|
+
* Applies the configured menu options when the component is enabled
|
60
|
+
* @hidden
|
61
|
+
*/
|
46
62
|
onEnable() {
|
47
63
|
this.applyOptions();
|
48
64
|
}
|
49
65
|
|
50
|
-
/**
|
66
|
+
/**
|
67
|
+
* Applies all configured options to the active {@link Context.menu}.
|
68
|
+
*/
|
51
69
|
applyOptions() {
|
52
70
|
this.context.menu.setPosition(this.position);
|
53
71
|
this.context.menu.showNeedleLogo(this.showNeedleLogo);
|
@@ -693,7 +693,13 @@
|
|
693
693
|
}
|
694
694
|
private _rigScale: number = 1;
|
695
695
|
private _lastRigScaleUpdate: number = -1;
|
696
|
-
|
696
|
+
|
697
|
+
/** Get the XR Rig worldscale.
|
698
|
+
*
|
699
|
+
* **For AR**
|
700
|
+
* If you want to modify the scale in AR at runtime get the WebARSessionRoot component via `findObjectOfType(WebARSessionRoot)` and then set the `arScale` value.
|
701
|
+
*
|
702
|
+
*/
|
697
703
|
get rigScale() {
|
698
704
|
if (!this._rigs[0]) return 1;
|
699
705
|
if (this._lastRigScaleUpdate !== this.context.time.frame) {
|
@@ -7,21 +7,33 @@
|
|
7
7
|
const debug = getParam("debugnet");
|
8
8
|
|
9
9
|
/**
|
10
|
-
*
|
10
|
+
* Provides configuration to the built-in networking system.
|
11
|
+
* This component supplies websocket URLs for establishing connections.
|
12
|
+
* It implements the {@link INetworkingWebsocketUrlProvider} interface.
|
13
|
+
*
|
11
14
|
* @category Networking
|
12
15
|
* @group Components
|
13
16
|
*/
|
14
17
|
export class Networking extends Behaviour implements INetworkingWebsocketUrlProvider {
|
15
18
|
|
16
|
-
/**
|
19
|
+
/**
|
20
|
+
* The websocket URL to connect to for networking functionality.
|
21
|
+
* Can be a complete URL or a relative path that will be resolved against the current origin.
|
22
|
+
*/
|
17
23
|
@serializable()
|
18
24
|
url: string | null = null;
|
19
25
|
|
20
|
-
/**
|
26
|
+
/**
|
27
|
+
* Name of the URL parameter that can override the websocket connection URL.
|
28
|
+
* When set, the URL will be overridden by the parameter value from the browser URL.
|
29
|
+
* For example, with `urlParameterName="ws"`, adding `?ws=ws://localhost:8080` to the browser URL will override the connection URL.
|
30
|
+
*/
|
21
31
|
@serializable()
|
22
32
|
urlParameterName: string | null = null;
|
23
33
|
|
24
|
-
/**
|
34
|
+
/**
|
35
|
+
* Alternative URL to use when running on a local network.
|
36
|
+
* This is particularly useful for development, when the server is running on the same machine as the client.
|
25
37
|
*/
|
26
38
|
@serializable()
|
27
39
|
localhost: string | null = null;
|
@@ -33,7 +45,13 @@
|
|
33
45
|
this.context.connection.registerProvider(this);
|
34
46
|
}
|
35
47
|
|
36
|
-
/**
|
48
|
+
/**
|
49
|
+
* Determines the websocket URL to use for networking connections.
|
50
|
+
* Processes the configured URL, applying localhost fallbacks when appropriate and
|
51
|
+
* handling URL parameter overrides if specified.
|
52
|
+
* @returns The formatted websocket URL string or null if no valid URL could be determined
|
53
|
+
* @internal
|
54
|
+
*/
|
37
55
|
getWebsocketUrl(): string | null {
|
38
56
|
|
39
57
|
let socketurl = this.url ? Networking.GetUrl(this.url, this.localhost) : null;
|
@@ -58,7 +76,13 @@
|
|
58
76
|
return "wss://" + match?.groups["url"];
|
59
77
|
}
|
60
78
|
|
61
|
-
|
79
|
+
/**
|
80
|
+
* Processes a URL string applying various transformations based on network environment.
|
81
|
+
* Handles relative paths and localhost fallbacks for local network environments.
|
82
|
+
* @param url The original URL to process
|
83
|
+
* @param localhostFallback Alternative URL to use when on a local network
|
84
|
+
* @returns The processed URL string or null/undefined if input was invalid
|
85
|
+
*/
|
62
86
|
public static GetUrl(url: string | null | undefined, localhostFallback?: string | null): string | null | undefined {
|
63
87
|
|
64
88
|
let result = url;
|
@@ -78,6 +102,13 @@
|
|
78
102
|
return result;
|
79
103
|
}
|
80
104
|
|
105
|
+
/**
|
106
|
+
* Determines if the current connection is on a local network.
|
107
|
+
* Useful for applying different networking configurations in local development environments.
|
108
|
+
* This is the same as calling {@link isLocalNetwork}.
|
109
|
+
* @param hostname Optional hostname to check instead of the current window location
|
110
|
+
* @returns True if the connection is on a local network, false otherwise
|
111
|
+
*/
|
81
112
|
public static IsLocalNetwork(hostname = window.location.hostname) {
|
82
113
|
return isLocalNetwork(hostname);
|
83
114
|
}
|
@@ -332,6 +332,7 @@
|
|
332
332
|
|
333
333
|
this._activePointerEvents = [];
|
334
334
|
this.context.input.addEventListener("pointerdown", this._onPointerDown, { queue: InputEventQueue.Early });
|
335
|
+
this.context.input.addEventListener("pointerdown", this._onPointerDownLate, { queue: InputEventQueue.Late });
|
335
336
|
this.context.input.addEventListener("pointerup", this._onPointerUp, { queue: InputEventQueue.Early });
|
336
337
|
this.context.input.addEventListener("pointerup", this._onPointerUpLate, { queue: InputEventQueue.Late });
|
337
338
|
}
|
@@ -352,6 +353,7 @@
|
|
352
353
|
}
|
353
354
|
this._activePointerEvents.length = 0;
|
354
355
|
this.context.input.removeEventListener("pointerdown", this._onPointerDown);
|
356
|
+
this.context.input.removeEventListener("pointerdown", this._onPointerDownLate);
|
355
357
|
this.context.input.removeEventListener("pointerup", this._onPointerUp);
|
356
358
|
this.context.input.removeEventListener("pointerup", this._onPointerUpLate);
|
357
359
|
}
|
@@ -363,6 +365,12 @@
|
|
363
365
|
private _onPointerDown = (_evt: NEPointerEvent) => {
|
364
366
|
this._activePointerEvents.push(_evt);
|
365
367
|
}
|
368
|
+
private _onPointerDownLate = (evt: NEPointerEvent) => {
|
369
|
+
if(evt.used && this._controls) {
|
370
|
+
// Disabling orbit controls here because otherwise we get a slight movement when e.g. using DragControls
|
371
|
+
this._controls.enabled = false;
|
372
|
+
}
|
373
|
+
}
|
366
374
|
|
367
375
|
private _onPointerUp = (evt: NEPointerEvent) => {
|
368
376
|
// make sure we cleanup the active pointer events
|
@@ -94,7 +94,7 @@
|
|
94
94
|
else this.particleMaterial = newMaterial;
|
95
95
|
}
|
96
96
|
|
97
|
-
|
97
|
+
|
98
98
|
if (material.map) {
|
99
99
|
material.map.colorSpace = LinearSRGBColorSpace;
|
100
100
|
material.map.premultiplyAlpha = false;
|
@@ -916,7 +916,7 @@
|
|
916
916
|
if (particleSystemBehaviour instanceof ParticleSystemBaseBehaviour) {
|
917
917
|
particleSystemBehaviour.system = this;
|
918
918
|
}
|
919
|
-
if (
|
919
|
+
if (debug) console.debug("Add custom ParticleSystem Behaviour", particleSystemBehaviour);
|
920
920
|
this._particleSystem.addBehavior(particleSystemBehaviour);
|
921
921
|
return true;
|
922
922
|
}
|
@@ -12,6 +12,7 @@
|
|
12
12
|
|
13
13
|
const debug = getParam("debugplayersync");
|
14
14
|
|
15
|
+
/** Type definition for a PlayerSync instance with a guaranteed asset property */
|
15
16
|
declare type PlayerSyncWithAsset = PlayerSync & Required<Pick<PlayerSync, "asset">>;
|
16
17
|
|
17
18
|
/**
|
@@ -23,7 +24,10 @@
|
|
23
24
|
|
24
25
|
/**
|
25
26
|
* This API is experimental and may change or be removed in the future.
|
26
|
-
*
|
27
|
+
* Creates a PlayerSync instance at runtime from a given URL and sets it up for networking
|
28
|
+
* @param url Path to the asset that should be instantiated for each player
|
29
|
+
* @param init Optional initialization parameters for the PlayerSync component
|
30
|
+
* @returns Promise resolving to a PlayerSync instance with a guaranteed asset property
|
27
31
|
* @example
|
28
32
|
* ```typescript
|
29
33
|
* const res = await PlayerSync.setupFrom("/assets/demo.glb");
|
@@ -47,15 +51,22 @@
|
|
47
51
|
return ps as PlayerSyncWithAsset;
|
48
52
|
}
|
49
53
|
|
50
|
-
/**
|
54
|
+
/**
|
55
|
+
* When enabled, PlayerSync will automatically load and instantiate the assigned asset when joining a networked room
|
56
|
+
*/
|
51
57
|
@serializable()
|
52
58
|
autoSync: boolean = true;
|
53
59
|
|
54
|
-
/**
|
60
|
+
/**
|
61
|
+
* The asset that will be loaded and instantiated when PlayerSync becomes active and joins a networked room
|
62
|
+
*/
|
55
63
|
@serializable(AssetReference)
|
56
64
|
asset?: AssetReference;
|
57
65
|
|
58
|
-
/**
|
66
|
+
/**
|
67
|
+
* Event invoked when a player instance is spawned with the spawned {@link Object3D} as parameter
|
68
|
+
* @serializable
|
69
|
+
*/
|
59
70
|
@serializable(EventList)
|
60
71
|
onPlayerSpawned?: EventList<Object3D>;
|
61
72
|
|
@@ -86,6 +97,10 @@
|
|
86
97
|
if (this.autoSync) this.getInstance();
|
87
98
|
}
|
88
99
|
|
100
|
+
/**
|
101
|
+
* Gets or creates an instance of the assigned asset for the local player
|
102
|
+
* @returns Promise resolving to the instantiated player object or null if creation failed
|
103
|
+
*/
|
89
104
|
async getInstance() {
|
90
105
|
if (this._localInstance) return this._localInstance;
|
91
106
|
|
@@ -123,6 +138,9 @@
|
|
123
138
|
return this._localInstance;
|
124
139
|
}
|
125
140
|
|
141
|
+
/**
|
142
|
+
* Destroys the current player instance and cleans up networking state
|
143
|
+
*/
|
126
144
|
destroyInstance = () => {
|
127
145
|
this._localInstance?.then(go => {
|
128
146
|
if (debug) console.log("PlayerSync.destroyInstance", go);
|
@@ -131,8 +149,9 @@
|
|
131
149
|
this._localInstance = undefined;
|
132
150
|
}
|
133
151
|
|
134
|
-
|
135
|
-
|
152
|
+
/**
|
153
|
+
* Sets up visibility change listeners to handle player cleanup when browser tab visibility changes
|
154
|
+
*/
|
136
155
|
private watchTabVisible() {
|
137
156
|
window.addEventListener("visibilitychange", _ => {
|
138
157
|
if (document.visibilityState === "visible") {
|
@@ -147,32 +166,50 @@
|
|
147
166
|
}
|
148
167
|
}
|
149
168
|
|
169
|
+
/**
|
170
|
+
* Enum defining events that can be triggered by PlayerState
|
171
|
+
*/
|
150
172
|
export enum PlayerStateEvent {
|
173
|
+
/** Triggered when a PlayerState's owner property changes */
|
151
174
|
OwnerChanged = "ownerChanged",
|
152
175
|
}
|
153
176
|
|
177
|
+
/** Arguments passed when a PlayerState's owner changes */
|
154
178
|
export declare interface PlayerStateOwnerChangedArgs {
|
179
|
+
/** The PlayerState instance that changed */
|
155
180
|
playerState: PlayerState;
|
181
|
+
/** Previous owner's connection ID */
|
156
182
|
oldValue: string;
|
183
|
+
/** New owner's connection ID */
|
157
184
|
newValue: string;
|
158
185
|
}
|
159
186
|
|
187
|
+
/** Callback type for PlayerState events */
|
160
188
|
export declare type PlayerStateEventCallback = (args: CustomEvent<PlayerStateOwnerChangedArgs>) => void;
|
161
189
|
|
190
|
+
/**
|
191
|
+
* Represents a player instance in the networked environment.
|
192
|
+
* Handles ownership, synchronization, and lifecycle management of player objects.
|
193
|
+
*/
|
162
194
|
export class PlayerState extends Behaviour {
|
163
195
|
|
164
196
|
private static _all: PlayerState[] = [];
|
165
|
-
/**
|
197
|
+
/** All PlayerState instances for all players in the scene */
|
166
198
|
static get all(): PlayerState[] {
|
167
199
|
return PlayerState._all;
|
168
200
|
};
|
169
201
|
|
170
202
|
private static _local: PlayerState[] = [];
|
171
|
-
/**
|
203
|
+
/** All PlayerState instances that belong to the local player */
|
172
204
|
static get local(): PlayerState[] {
|
173
205
|
return PlayerState._local;
|
174
206
|
}
|
175
207
|
|
208
|
+
/**
|
209
|
+
* Gets the PlayerState component for a given object or component
|
210
|
+
* @param obj Object3D or Component to find the PlayerState for
|
211
|
+
* @returns The PlayerState component if found, undefined otherwise
|
212
|
+
*/
|
176
213
|
static getFor(obj: Object3D | Component) {
|
177
214
|
if (obj instanceof Object3D) {
|
178
215
|
return GameObject.getComponentInParent(obj, PlayerState);
|
@@ -183,22 +220,34 @@
|
|
183
220
|
return undefined;
|
184
221
|
}
|
185
222
|
|
186
|
-
/
|
223
|
+
/**
|
224
|
+
* Checks if a given object or component belongs to the local player
|
225
|
+
* @param obj Object3D or Component to check
|
226
|
+
* @returns True if the object belongs to the local player, false otherwise
|
227
|
+
*/
|
187
228
|
static isLocalPlayer(obj: Object3D | Component): boolean {
|
188
229
|
const state = PlayerState.getFor(obj);
|
189
230
|
return state?.isLocalPlayer ?? false;
|
190
231
|
}
|
191
232
|
|
192
|
-
// static Callback
|
193
233
|
private static _callbacks: { [key: string]: PlayerStateEventCallback[] } = {};
|
194
234
|
/**
|
195
|
-
*
|
235
|
+
* Registers a callback for a specific PlayerState event
|
236
|
+
* @param event The event to listen for
|
237
|
+
* @param cb Callback function that will be invoked when the event occurs
|
238
|
+
* @returns The provided callback function for chaining
|
196
239
|
*/
|
197
240
|
static addEventListener(event: PlayerStateEvent, cb: PlayerStateEventCallback) {
|
198
241
|
if (!this._callbacks[event]) this._callbacks[event] = [];
|
199
242
|
this._callbacks[event].push(cb);
|
200
243
|
return cb;
|
201
244
|
}
|
245
|
+
|
246
|
+
/**
|
247
|
+
* Removes a previously registered event callback
|
248
|
+
* @param event The event type to remove the callback from
|
249
|
+
* @param cb The callback function to remove
|
250
|
+
*/
|
202
251
|
static removeEventListener(event: PlayerStateEvent, cb: PlayerStateEventCallback) {
|
203
252
|
if (!this._callbacks[event]) return;
|
204
253
|
const index = this._callbacks[event].indexOf(cb);
|
@@ -212,20 +261,39 @@
|
|
212
261
|
}
|
213
262
|
|
214
263
|
|
264
|
+
/** Event triggered when the owner of this PlayerState changes */
|
215
265
|
public onOwnerChangeEvent = new EventList();
|
266
|
+
|
267
|
+
/** Event triggered the first time an owner is assigned to this PlayerState */
|
216
268
|
public onFirstOwnerChangeEvent = new EventList();
|
269
|
+
|
270
|
+
/** Indicates if this PlayerState has an owner assigned */
|
217
271
|
public hasOwner = false;
|
218
272
|
|
273
|
+
/**
|
274
|
+
* The connection ID of the player who owns this PlayerState instance
|
275
|
+
* @syncField Synchronized across the network
|
276
|
+
*/
|
219
277
|
@syncField(PlayerState.prototype.onOwnerChange)
|
220
278
|
owner?: string;
|
221
279
|
|
222
|
-
/**
|
280
|
+
/**
|
281
|
+
* When enabled, PlayerState will not destroy itself when the owner is not connected anymore
|
282
|
+
*/
|
223
283
|
dontDestroy: boolean = false;
|
224
284
|
|
285
|
+
/**
|
286
|
+
* Indicates if this PlayerState belongs to the local player
|
287
|
+
*/
|
225
288
|
get isLocalPlayer(): boolean {
|
226
289
|
return this.owner === this.context.connection.connectionId;
|
227
290
|
}
|
228
291
|
|
292
|
+
/**
|
293
|
+
* Handles owner change events and updates relevant state
|
294
|
+
* @param newOwner The new owner's connection ID
|
295
|
+
* @param oldOwner The previous owner's connection ID
|
296
|
+
*/
|
229
297
|
private onOwnerChange(newOwner: string, oldOwner: string) {
|
230
298
|
if (debug) console.log(`PlayerSync.onOwnerChange: ${oldOwner} → ${newOwner} (me: ${this.context.connection.connectionId})`);
|
231
299
|
|
@@ -300,7 +368,7 @@
|
|
300
368
|
}
|
301
369
|
}
|
302
370
|
|
303
|
-
/**
|
371
|
+
/** Tells the server that this client has been destroyed, and the networking message for the instantiate will be removed */
|
304
372
|
doDestroy() {
|
305
373
|
if (debug) console.log("PlayerSync.doDestroy → syncDestroy", this.name);
|
306
374
|
syncDestroy(this.gameObject, this.context.connection, true, { saveInRoom: false });
|
@@ -319,6 +387,10 @@
|
|
319
387
|
}
|
320
388
|
}
|
321
389
|
|
390
|
+
/**
|
391
|
+
* Handler for when a user leaves the networked room
|
392
|
+
* @param model Object containing the ID of the user who left
|
393
|
+
*/
|
322
394
|
private onUserLeftRoom = (model: { userId: string }) => {
|
323
395
|
if (model.userId === this.owner) {
|
324
396
|
if (debug)
|
@@ -75,6 +75,10 @@
|
|
75
75
|
|
76
76
|
abstract get typeName(): string;
|
77
77
|
|
78
|
+
/**
|
79
|
+
* Whether the effect is active or not. Prefer using `enabled` instead.
|
80
|
+
* @deprecated
|
81
|
+
*/
|
78
82
|
@serializable()
|
79
83
|
active: boolean = true;
|
80
84
|
|
@@ -82,14 +86,16 @@
|
|
82
86
|
|
83
87
|
onEnable(): void {
|
84
88
|
super.onEnable();
|
85
|
-
|
89
|
+
if (debug) console.warn("onEnable effect", this, this.__internalDidAwakeAndStart)
|
86
90
|
// Dont override the serialized value by enabling (we could also just disable this component / map enabled to active)
|
87
91
|
if (this.__internalDidAwakeAndStart)
|
88
92
|
this.active = true;
|
93
|
+
this.onEffectEnabled();
|
89
94
|
}
|
90
95
|
|
91
96
|
onDisable(): void {
|
92
97
|
super.onDisable();
|
98
|
+
if (debug) console.warn("onDisable effect", this)
|
93
99
|
this._manager?.removeEffect(this);
|
94
100
|
this.active = false;
|
95
101
|
}
|
@@ -219,12 +219,12 @@
|
|
219
219
|
if (ef instanceof MODULES.POSTPROCESSING.MODULE.Effect)
|
220
220
|
effects.push(ef as Effect);
|
221
221
|
else if (ef instanceof MODULES.POSTPROCESSING.MODULE.Pass) {
|
222
|
-
const pass = new MODULES.POSTPROCESSING.MODULE.EffectPass(cam, ...effects);
|
223
|
-
pass.mainScene = scene;
|
224
|
-
pass.name = effects.map(e => e.constructor.name).join(", ");
|
225
|
-
pass.enabled = true;
|
222
|
+
// const pass = new MODULES.POSTPROCESSING.MODULE.EffectPass(cam, ...effects);
|
223
|
+
// pass.mainScene = scene;
|
224
|
+
// pass.name = effects.map(e => e.constructor.name).join(", ");
|
225
|
+
// pass.enabled = true;
|
226
226
|
// composer.addPass(pass);
|
227
|
-
effects.length = 0;
|
227
|
+
// effects.length = 0;
|
228
228
|
composer.addPass(ef as Pass);
|
229
229
|
}
|
230
230
|
else {
|
@@ -262,7 +262,7 @@
|
|
262
262
|
}
|
263
263
|
|
264
264
|
if (debug)
|
265
|
-
console.log("PostProcessing Passes
|
265
|
+
console.log("[PostProcessing] Passes →", composer.passes);
|
266
266
|
}
|
267
267
|
|
268
268
|
private orderEffects() {
|
@@ -847,11 +847,18 @@
|
|
847
847
|
for (const mesh of this.sharedMeshes) {
|
848
848
|
if (mesh instanceof SkinnedMesh) {
|
849
849
|
this._needUpdateBoundingSphere = false;
|
850
|
-
|
851
|
-
|
852
|
-
|
853
|
-
|
854
|
-
|
850
|
+
try {
|
851
|
+
const geometry = mesh.geometry;
|
852
|
+
const raycastmesh = getRaycastMesh(mesh);
|
853
|
+
if (raycastmesh) {
|
854
|
+
mesh.geometry = raycastmesh;
|
855
|
+
}
|
856
|
+
mesh.computeBoundingSphere();
|
857
|
+
mesh.geometry = geometry;
|
858
|
+
}
|
859
|
+
catch(err) {
|
860
|
+
console.error(`Error updating bounding sphere for ${mesh.name}`, err);
|
861
|
+
}
|
855
862
|
}
|
856
863
|
}
|
857
864
|
}
|
@@ -1,6 +1,7 @@
|
|
1
1
|
import { BatchedMesh, BufferGeometry, Color, Material, Matrix4, Mesh, MeshStandardMaterial, Object3D, RawShaderMaterial } from "three";
|
2
2
|
|
3
3
|
import { isDevEnvironment, showBalloonError } from "../engine/debug/index.js";
|
4
|
+
import { Gizmos } from "../engine/engine_gizmos.js";
|
4
5
|
import { $instancingAutoUpdateBounds, $instancingRenderer, NEED_UPDATE_INSTANCE_KEY } from "../engine/engine_instancing.js";
|
5
6
|
import { Context } from "../engine/engine_setup.js";
|
6
7
|
import { getParam, makeIdFromRandomWords } from "../engine/engine_utils.js";
|
@@ -337,6 +338,12 @@
|
|
337
338
|
this._batchedMesh.computeBoundingBox();
|
338
339
|
if (sphere)
|
339
340
|
this._batchedMesh.computeBoundingSphere();
|
341
|
+
if (debugInstancing && this._batchedMesh.boundingSphere) {
|
342
|
+
const sphere = this._batchedMesh.boundingSphere;
|
343
|
+
// const worldPos = this._batchedMesh.worldPosition.add(sphere.center);
|
344
|
+
// const worldRadius = sphere!.radius;
|
345
|
+
Gizmos.DrawWireSphere(sphere.center, sphere.radius, 0x00ff00);
|
346
|
+
}
|
340
347
|
}
|
341
348
|
|
342
349
|
private _context: Context;
|
@@ -401,7 +408,7 @@
|
|
401
408
|
// break;
|
402
409
|
// }
|
403
410
|
// }
|
404
|
-
if(!canMergeMaterial) {
|
411
|
+
if (!canMergeMaterial) {
|
405
412
|
return false;
|
406
413
|
}
|
407
414
|
}
|
@@ -588,7 +595,7 @@
|
|
588
595
|
this._batchedMesh.layers.enableAll();
|
589
596
|
|
590
597
|
if (this._needUpdateBounds && this._batchedMesh[$instancingAutoUpdateBounds] === true) {
|
591
|
-
if (debugInstancing) console.log("Update instancing bounds", this.name, this._batchedMesh.matrixWorldNeedsUpdate);
|
598
|
+
if (debugInstancing === "verbose") console.log("Update instancing bounds", this.name, this._batchedMesh.matrixWorldNeedsUpdate);
|
592
599
|
this.updateBounds();
|
593
600
|
}
|
594
601
|
}
|
@@ -623,7 +630,9 @@
|
|
623
630
|
}
|
624
631
|
|
625
632
|
private markNeedsUpdate() {
|
626
|
-
if (debugInstancing
|
633
|
+
if (debugInstancing === "verbose") {
|
634
|
+
console.warn("Marking instanced mesh dirty", this.name);
|
635
|
+
}
|
627
636
|
this._needUpdateBounds = true;
|
628
637
|
// this.inst.instanceMatrix.needsUpdate = true;
|
629
638
|
}
|
@@ -641,19 +650,23 @@
|
|
641
650
|
}
|
642
651
|
|
643
652
|
private grow(geometry: BufferGeometry) {
|
644
|
-
const
|
653
|
+
const growFactor = 2;
|
654
|
+
const newSize = Math.ceil(this._maxInstanceCount * growFactor);
|
645
655
|
|
646
656
|
// create a new BatchedMesh instance
|
647
657
|
const estimatedSpace = this.tryEstimateVertexCountSize(newSize, [geometry]);// geometry.attributes.position.count;
|
648
658
|
// const indices = geometry.index ? geometry.index.count : 0;
|
649
659
|
const newMaxVertexCount = Math.max(this._maxVertexCount, estimatedSpace.vertexCount);
|
650
|
-
const newMaxIndexCount = Math.max(this._maxIndexCount, estimatedSpace.indexCount, this._maxVertexCount *
|
660
|
+
const newMaxIndexCount = Math.max(this._maxIndexCount, estimatedSpace.indexCount, Math.ceil(this._maxVertexCount * growFactor));
|
651
661
|
|
652
662
|
if (debugInstancing) {
|
653
663
|
const geometryInfo = getMeshInformation(geometry);
|
654
|
-
console.warn(`Growing
|
664
|
+
console.warn(`[Instancing] Growing Buffer\nMesh: \"${this.name}${geometry.name?.length ? "/" + geometry.name : ""}\"\n${geometryInfo.vertexCount} vertices, ${geometryInfo.indexCount} indices\nMax count ${this._maxInstanceCount} → ${newSize}\nMax vertex count ${this._maxVertexCount} -> ${newMaxVertexCount}\nMax index count ${this._maxIndexCount} -> ${newMaxIndexCount}`);
|
655
665
|
this._debugMaterial = createDebugMaterial();
|
656
666
|
}
|
667
|
+
else if (isDevEnvironment()) {
|
668
|
+
console.debug(`[Instancing] Growing Buffer\nMesh: \"${this.name}${geometry.name?.length ? "/" + geometry.name : ""}\"\nMax count ${this._maxInstanceCount} → ${newSize}\nMax vertex count ${this._maxVertexCount} -> ${newMaxVertexCount}\nMax index count ${this._maxIndexCount} -> ${newMaxIndexCount}`);
|
669
|
+
}
|
657
670
|
|
658
671
|
this._maxVertexCount = newMaxVertexCount;
|
659
672
|
this._maxIndexCount = newMaxIndexCount;
|
@@ -750,7 +763,8 @@
|
|
750
763
|
|
751
764
|
private addGeometry(handle: InstanceHandle) {
|
752
765
|
|
753
|
-
const
|
766
|
+
const obj = handle.object;
|
767
|
+
const geo = obj.geometry as BufferGeometry;
|
754
768
|
if (!geo) {
|
755
769
|
// if the geometry is null we cannot add it
|
756
770
|
return;
|
@@ -776,7 +790,7 @@
|
|
776
790
|
if (smallestBucket != null) {
|
777
791
|
const bucket = smallestBucket;
|
778
792
|
if (debugInstancing)
|
779
|
-
console.debug(`
|
793
|
+
console.debug(`[Instancing] SET GEOMETRY \"${handle.name}\"\nGEOMETRY_ID=${bucket.geometryIndex}\n${this._currentInstanceCount} instances\n${handle.maxVertexCount} vertices, ${handle.maxIndexCount} indices, `);
|
780
794
|
try {
|
781
795
|
this._batchedMesh.setGeometryAt(bucket.geometryIndex, handle.object.geometry as BufferGeometry);
|
782
796
|
const newIndex = this._batchedMesh.addInstance(bucket.geometryIndex);
|
@@ -800,15 +814,13 @@
|
|
800
814
|
let geometryId = this._geometryIds.get(geo);
|
801
815
|
if (geometryId === undefined || geometryId === null) {
|
802
816
|
if (debugInstancing)
|
803
|
-
console.debug(
|
817
|
+
console.debug(`[Instancing] > ADD GEOMETRY \"${handle.name}\"\n${this._currentInstanceCount} instances, ${handle.maxVertexCount} max vertices, ${handle.maxIndexCount} max indices`);
|
804
818
|
|
805
|
-
|
806
|
-
|
807
819
|
geometryId = this._batchedMesh.addGeometry(geo, handle.maxVertexCount, handle.maxIndexCount);
|
808
820
|
this._geometryIds.set(geo, geometryId);
|
809
821
|
}
|
810
822
|
else {
|
811
|
-
if (debugInstancing) console.log(
|
823
|
+
if (debugInstancing === "verbose") console.log(`[Instancing] > ADD INSTANCE \"${handle.name}\"\nGEOMETRY_ID=${geometryId}\n${this._currentInstanceCount} instances`);
|
812
824
|
}
|
813
825
|
this._currentVertexCount += handle.maxVertexCount;
|
814
826
|
this._currentIndexCount += handle.maxIndexCount;
|
@@ -820,7 +832,7 @@
|
|
820
832
|
this._usedBuckets[i] = { geometryIndex: geometryId, vertexCount: handle.maxVertexCount, indexCount: handle.maxIndexCount };
|
821
833
|
this._batchedMesh.setMatrixAt(i, handle.object.matrixWorld);
|
822
834
|
if (debugInstancing)
|
823
|
-
console.debug(`
|
835
|
+
console.debug(`[Instancing] > ADDED INSTANCE \"${handle.name}\"\nGEOMETRY_ID=${geometryId}\n${this._currentInstanceCount} instances\nIndex: ${handle.__instanceIndex}`);
|
824
836
|
|
825
837
|
}
|
826
838
|
|
@@ -837,6 +849,9 @@
|
|
837
849
|
// this.inst.deleteGeometry(handle.__instanceIndex);
|
838
850
|
// else
|
839
851
|
// this._batchedMesh.setVisibleAt(handle.__instanceIndex, false);
|
852
|
+
if(debugInstancing) {
|
853
|
+
console.debug(`[Instancing] < REMOVE INSTANCE \"${handle.name}\" at [${handle.__instanceIndex}]\nGEOMETRY_ID=${handle.__geometryIndex}\n${this._currentInstanceCount} instances\nIndex: ${handle.__instanceIndex}`);
|
854
|
+
}
|
840
855
|
this._batchedMesh.deleteInstance(handle.__instanceIndex);
|
841
856
|
this._availableBuckets.push({
|
842
857
|
geometryIndex: handle.__geometryIndex,
|
@@ -498,7 +498,9 @@
|
|
498
498
|
else this.setAngularVelocity(x);
|
499
499
|
}
|
500
500
|
|
501
|
-
/**
|
501
|
+
/**
|
502
|
+
* Returns the rigidbody velocity smoothed over ~ 10 frames
|
503
|
+
*/
|
502
504
|
public get smoothedVelocity(): Vector3 {
|
503
505
|
this._smoothedVelocityGetter.copy(this._smoothedVelocity);
|
504
506
|
return this._smoothedVelocityGetter.multiplyScalar(1 / this.context.time.deltaTime);
|
@@ -512,9 +514,9 @@
|
|
512
514
|
|
513
515
|
|
514
516
|
private captureVelocity() {
|
515
|
-
const
|
516
|
-
|
517
|
-
const vel =
|
517
|
+
const matrixWorld = this.gameObject.matrixWorld;
|
518
|
+
Rigidbody.tempPosition.setFromMatrixPosition(matrixWorld);
|
519
|
+
const vel = Rigidbody.tempPosition.sub(this._lastPosition);
|
518
520
|
this._lastPosition.copy(Rigidbody.tempPosition);
|
519
521
|
this._smoothedVelocity.lerp(vel, this.context.time.deltaTime / .1);
|
520
522
|
}
|
@@ -46,6 +46,10 @@
|
|
46
46
|
index: number;
|
47
47
|
}
|
48
48
|
|
49
|
+
export type LoadSceneProgressEvent = LoadSceneEvent & {
|
50
|
+
progress: number,
|
51
|
+
}
|
52
|
+
|
49
53
|
/**
|
50
54
|
* The ISceneEventListener is called by the {@link SceneSwitcher} when a scene is loaded or unloaded.
|
51
55
|
* It must be added to the root object of your scene (that is being loaded) or on the same object as the SceneSwitcher
|
@@ -226,6 +230,15 @@
|
|
226
230
|
get currentlyLoadedScene() { return this._currentScene; }
|
227
231
|
|
228
232
|
/**
|
233
|
+
* Called when a scene starts loading
|
234
|
+
*/
|
235
|
+
@serializable(EventList)
|
236
|
+
sceneLoadingStart: EventList<LoadSceneEvent> = new EventList();
|
237
|
+
|
238
|
+
@serializable(EventList)
|
239
|
+
sceneLoadingProgress: EventList<ProgressEvent> = new EventList();
|
240
|
+
|
241
|
+
/**
|
229
242
|
* The sceneLoaded event is called when a scene/glTF is loaded and added to the scene
|
230
243
|
*/
|
231
244
|
@serializable(EventList)
|
@@ -281,7 +294,7 @@
|
|
281
294
|
this._preloadScheduler.maxLoadAhead = this.preloadNext;
|
282
295
|
this._preloadScheduler.maxLoadBehind = this.preloadPrevious;
|
283
296
|
this._preloadScheduler.maxConcurrent = this.preloadConcurrent;
|
284
|
-
this._preloadScheduler.begin();
|
297
|
+
this._preloadScheduler.begin(2000);
|
285
298
|
|
286
299
|
// Begin loading the loading scene
|
287
300
|
if (this.autoLoadFirstScene && this._currentIndex === -1 && !await this.tryLoadFromQueryParam()) {
|
@@ -602,11 +615,13 @@
|
|
602
615
|
|
603
616
|
const loadStartEvt = new CustomEvent<LoadSceneEvent>("loadscene-start", { detail: { scene: scene, switcher: this, index: index } })
|
604
617
|
this.dispatchEvent(loadStartEvt);
|
618
|
+
this.sceneLoadingStart?.invoke(loadStartEvt.detail);
|
605
619
|
await this.onStartLoading();
|
606
620
|
// start loading and wait for the scene to be loaded
|
607
621
|
await scene.loadAssetAsync((_, prog) => {
|
608
622
|
this._currentLoadingProgress = prog;
|
609
623
|
this.dispatchEvent(prog);
|
624
|
+
this.sceneLoadingProgress?.invoke(prog);
|
610
625
|
}).catch(console.error);
|
611
626
|
await this.onEndLoading();
|
612
627
|
const finishedEvt = new CustomEvent<LoadSceneEvent>("loadscene-finished", { detail: { scene: scene, switcher: this, index: index } });
|
@@ -842,9 +857,18 @@
|
|
842
857
|
|
843
858
|
|
844
859
|
|
860
|
+
/**
|
861
|
+
* PreLoadScheduler is responsible for managing preloading of scenes.
|
862
|
+
* It handles scheduling and limiting concurrent downloads of scenes based on specified parameters.
|
863
|
+
*/
|
845
864
|
class PreLoadScheduler {
|
865
|
+
/** Maximum number of scenes to preload ahead of the current scene */
|
846
866
|
maxLoadAhead: number;
|
867
|
+
|
868
|
+
/** Maximum number of scenes to preload behind the current scene */
|
847
869
|
maxLoadBehind: number;
|
870
|
+
|
871
|
+
/** Maximum number of scenes that can be preloaded concurrently */
|
848
872
|
maxConcurrent: number;
|
849
873
|
|
850
874
|
private _isRunning: boolean = false;
|
@@ -852,6 +876,13 @@
|
|
852
876
|
private _loadTasks: LoadTask[] = [];
|
853
877
|
private _maxConcurrentLoads: number = 1;
|
854
878
|
|
879
|
+
/**
|
880
|
+
* Creates a new PreLoadScheduler instance
|
881
|
+
* @param rooms The SceneSwitcher that this scheduler belongs to
|
882
|
+
* @param ahead Number of scenes to preload ahead of current scene
|
883
|
+
* @param behind Number of scenes to preload behind current scene
|
884
|
+
* @param maxConcurrent Maximum number of concurrent preloads allowed
|
885
|
+
*/
|
855
886
|
constructor(rooms: SceneSwitcher, ahead: number = 1, behind: number = 1, maxConcurrent: number = 2) {
|
856
887
|
this._switcher = rooms;
|
857
888
|
this.maxLoadAhead = ahead;
|
@@ -859,26 +890,34 @@
|
|
859
890
|
this.maxConcurrent = maxConcurrent;
|
860
891
|
}
|
861
892
|
|
862
|
-
|
893
|
+
/**
|
894
|
+
* Starts the preloading process after a specified delay
|
895
|
+
* @param delay Time in milliseconds to wait before starting preload
|
896
|
+
*/
|
897
|
+
begin(delay: number) {
|
863
898
|
if (this._isRunning) return;
|
864
|
-
if (debug) console.log("Preload begin")
|
899
|
+
if (debug) console.log("Preload begin", { delay })
|
865
900
|
this._isRunning = true;
|
866
|
-
let lastRoom: number = -
|
901
|
+
let lastRoom: number = -10;
|
867
902
|
let searchDistance: number;
|
868
903
|
let searchCall: number;
|
869
904
|
const array = this._switcher.scenes;
|
905
|
+
const startTime = Date.now() + delay;
|
870
906
|
const interval = setInterval(() => {
|
871
907
|
if (this.allLoaded()) {
|
872
908
|
if (debug)
|
873
|
-
console.log("All scenes loaded");
|
909
|
+
console.log("All scenes (pre-)loaded");
|
874
910
|
this.stop();
|
875
911
|
}
|
876
912
|
if (!this._isRunning) {
|
877
913
|
clearInterval(interval);
|
878
914
|
return;
|
879
915
|
}
|
916
|
+
|
917
|
+
if (Date.now() < startTime) return;
|
918
|
+
|
880
919
|
if (this.canLoadNewScene() === false) return;
|
881
|
-
if (lastRoom !== this._switcher.currentIndex) {
|
920
|
+
if (lastRoom === -10 || lastRoom !== this._switcher.currentIndex) {
|
882
921
|
lastRoom = this._switcher.currentIndex;
|
883
922
|
searchCall = 0;
|
884
923
|
searchDistance = 0;
|
@@ -892,19 +931,33 @@
|
|
892
931
|
if (roomIndex < 0) return;
|
893
932
|
// if (roomIndex < 0) roomIndex = array.length + roomIndex;
|
894
933
|
if (roomIndex < 0 || roomIndex >= array.length) return;
|
895
|
-
|
896
|
-
|
934
|
+
if (!this._loadTasks.some(t => t.index === roomIndex)) {
|
935
|
+
const scene = array[roomIndex];
|
936
|
+
if (debug) console.log("Preload scene", { roomIndex, searchForward, lastRoom, currentIndex: this._switcher.currentIndex, tasks: this._loadTasks.length }, scene?.url);
|
937
|
+
new LoadTask(roomIndex, scene, this._loadTasks);
|
938
|
+
}
|
897
939
|
}, 200);
|
898
940
|
}
|
899
941
|
|
942
|
+
/**
|
943
|
+
* Stops the preloading process
|
944
|
+
*/
|
900
945
|
stop() {
|
901
946
|
this._isRunning = false;
|
902
947
|
}
|
903
948
|
|
949
|
+
/**
|
950
|
+
* Checks if a new scene can be loaded based on current load limits
|
951
|
+
* @returns True if a new scene can be loaded, false otherwise
|
952
|
+
*/
|
904
953
|
canLoadNewScene(): boolean {
|
905
954
|
return this._loadTasks.length < this._maxConcurrentLoads;
|
906
955
|
}
|
907
956
|
|
957
|
+
/**
|
958
|
+
* Checks if all scenes in the SceneSwitcher have been loaded
|
959
|
+
* @returns True if all scenes are loaded, false otherwise
|
960
|
+
*/
|
908
961
|
allLoaded(): boolean {
|
909
962
|
if (this._switcher.scenes) {
|
910
963
|
for (const scene of this._switcher.scenes) {
|
@@ -915,12 +968,25 @@
|
|
915
968
|
}
|
916
969
|
}
|
917
970
|
|
971
|
+
/**
|
972
|
+
* Represents a single preloading task for a scene
|
973
|
+
*/
|
918
974
|
class LoadTask {
|
919
|
-
|
975
|
+
/** The index of the scene in the scenes array */
|
920
976
|
index: number;
|
977
|
+
|
978
|
+
/** The AssetReference to be loaded */
|
921
979
|
asset: AssetReference;
|
980
|
+
|
981
|
+
/** The collection of active load tasks this task belongs to */
|
922
982
|
tasks: LoadTask[];
|
923
983
|
|
984
|
+
/**
|
985
|
+
* Creates a new LoadTask and begins loading immediately
|
986
|
+
* @param index The index of the scene in the scenes array
|
987
|
+
* @param asset The AssetReference to preload
|
988
|
+
* @param tasks The collection of active load tasks
|
989
|
+
*/
|
924
990
|
constructor(index: number, asset: AssetReference, tasks: LoadTask[]) {
|
925
991
|
this.index = index;
|
926
992
|
this.asset = asset;
|
@@ -929,6 +995,9 @@
|
|
929
995
|
this.awaitLoading();
|
930
996
|
}
|
931
997
|
|
998
|
+
/**
|
999
|
+
* Asynchronously loads the asset and removes this task from the active tasks list when complete
|
1000
|
+
*/
|
932
1001
|
private async awaitLoading() {
|
933
1002
|
if (this.asset && !this.asset.isLoaded()) {
|
934
1003
|
if (debug)
|
@@ -1,13 +1,16 @@
|
|
1
1
|
import type { N8AOPostPass } from "n8ao";
|
2
|
-
import { Color, PerspectiveCamera } from "three";
|
2
|
+
import { Color, DepthFormat, DepthStencilFormat, DepthTexture, PerspectiveCamera, UnsignedInt248Type, UnsignedIntType, WebGLRenderTarget } from "three";
|
3
3
|
|
4
4
|
import { MODULES } from "../../../engine/engine_modules.js";
|
5
5
|
import { serializable } from "../../../engine/engine_serialization.js";
|
6
6
|
import { validate } from "../../../engine/engine_util_decorator.js";
|
7
|
+
import { getParam } from "../../../engine/engine_utils.js";
|
7
8
|
import { type EffectProviderResult, PostProcessingEffect } from "../PostProcessingEffect.js";
|
8
9
|
import { VolumeParameter } from "../VolumeParameter.js";
|
9
10
|
import { registerCustomEffectType } from "../VolumeProfile.js";
|
10
11
|
|
12
|
+
const debugN8AO = getParam("debugN8AO");
|
13
|
+
|
11
14
|
// https://github.com/N8python/n8ao
|
12
15
|
|
13
16
|
/**See (N8AO documentation)[https://github.com/N8python/n8ao] */
|
@@ -30,6 +33,10 @@
|
|
30
33
|
return "ScreenSpaceAmbientOcclusionN8";
|
31
34
|
}
|
32
35
|
|
36
|
+
get pass(): N8AOPostPass {
|
37
|
+
return this._ssao;
|
38
|
+
}
|
39
|
+
|
33
40
|
@validate()
|
34
41
|
@serializable()
|
35
42
|
gammaCorrection: boolean = true;
|
@@ -109,19 +116,51 @@
|
|
109
116
|
|
110
117
|
const cam = this.context.mainCamera! as PerspectiveCamera;
|
111
118
|
|
119
|
+
const width = this.context.domWidth;
|
120
|
+
const height = this.context.domHeight;
|
121
|
+
|
112
122
|
const ssao = this._ssao = new MODULES.POSTPROCESSING_AO.MODULE.N8AOPostPass(
|
113
123
|
this.context.scene,
|
114
124
|
cam,
|
115
|
-
|
116
|
-
|
117
|
-
|
125
|
+
width, height
|
126
|
+
) as N8AOPostPass;
|
127
|
+
|
118
128
|
const mode = ScreenSpaceAmbientOcclusionN8QualityMode[this.quality];
|
119
129
|
ssao.setQualityMode(mode);
|
120
130
|
|
131
|
+
ssao.configuration.transparencyAware = false;
|
132
|
+
// ssao.needsSwap = false;
|
133
|
+
|
134
|
+
|
135
|
+
const renderTarget = new WebGLRenderTarget(width, height);
|
136
|
+
ssao.configuration.beautyRenderTarget = renderTarget;
|
137
|
+
ssao.configuration.autoRenderBeauty = false;
|
138
|
+
// // If you just want a depth buffer
|
139
|
+
// renderTarget.depthTexture = new DepthTexture(width, height, UnsignedIntType);
|
140
|
+
// renderTarget.depthTexture.format = DepthFormat;
|
141
|
+
// // If you want a depth buffer and a stencil buffer
|
142
|
+
// renderTarget.depthTexture = new DepthTexture(width, height, UnsignedInt248Type);
|
143
|
+
// renderTarget.depthTexture.format = DepthStencilFormat;
|
144
|
+
|
145
|
+
|
121
146
|
ssao.configuration.gammaCorrection = this.gammaCorrection;
|
122
|
-
|
123
147
|
ssao.configuration.screenSpaceRadius = this.screenspaceRadius;
|
124
148
|
|
149
|
+
|
150
|
+
if (debugN8AO) {
|
151
|
+
// ssao.debug = debugN8AO;
|
152
|
+
ssao.enableDebugMode();
|
153
|
+
console.log(ssao);
|
154
|
+
setInterval(() => {
|
155
|
+
console.log("SSAO", ssao.lastTime);
|
156
|
+
}, 1000);
|
157
|
+
setInterval(() => {
|
158
|
+
// ssao.enabled = !ssao.enabled;
|
159
|
+
console.log("SSAO", ssao.enabled, { ssao, autoRenderBeauty: ssao.configuration.autoRenderBeauty });
|
160
|
+
}, 4000)
|
161
|
+
}
|
162
|
+
|
163
|
+
|
125
164
|
this.intensity.onValueChanged = newValue => {
|
126
165
|
ssao.configuration.intensity = newValue;
|
127
166
|
}
|
@@ -135,10 +174,18 @@
|
|
135
174
|
if (!ssao.color) ssao.color = new Color();
|
136
175
|
ssao.configuration.color.copy(newValue);
|
137
176
|
}
|
177
|
+
|
138
178
|
|
139
|
-
const
|
140
|
-
|
141
|
-
|
179
|
+
// const normalPass = new MODULES.POSTPROCESSING.MODULE.NormalPass(this.context.scene, cam);
|
180
|
+
// const depthDownsamplingPass = new MODULES.POSTPROCESSING.MODULE.DepthDownsamplingPass({
|
181
|
+
// normalBuffer: normalPass.texture,
|
182
|
+
// resolutionScale: .1
|
183
|
+
// });
|
184
|
+
// const arr = new Array();
|
185
|
+
// arr.push(normalPass);
|
186
|
+
// arr.push(depthDownsamplingPass);
|
187
|
+
// arr.push(ssao);
|
188
|
+
return ssao;
|
142
189
|
}
|
143
190
|
|
144
191
|
}
|
@@ -8,8 +8,15 @@
|
|
8
8
|
|
9
9
|
const debug = getParam("debugspatialtrigger");
|
10
10
|
|
11
|
+
/** Layer instances used for mask comparison */
|
11
12
|
const layer1 = new Layers();
|
12
13
|
const layer2 = new Layers();
|
14
|
+
/**
|
15
|
+
* Tests if two layer masks intersect
|
16
|
+
* @param mask1 First layer mask
|
17
|
+
* @param mask2 Second layer mask
|
18
|
+
* @returns True if the layers intersect
|
19
|
+
*/
|
13
20
|
function testMask(mask1, mask2) {
|
14
21
|
layer1.mask = mask1;
|
15
22
|
layer2.mask = mask2;
|
@@ -17,25 +24,45 @@
|
|
17
24
|
}
|
18
25
|
|
19
26
|
/**
|
27
|
+
* Component that receives and responds to spatial events, like entering or exiting a trigger zone.
|
28
|
+
* Used in conjunction with {@link SpatialTrigger} to create interactive spatial events.
|
20
29
|
* @category Interactivity
|
21
30
|
* @group Components
|
22
31
|
*/
|
23
32
|
export class SpatialTriggerReceiver extends Behaviour {
|
24
33
|
|
25
|
-
/
|
34
|
+
/**
|
35
|
+
* Bitmask determining which triggers this receiver responds to
|
36
|
+
* Only triggers with matching masks will interact with this receiver
|
37
|
+
*/
|
26
38
|
@serializable()
|
27
39
|
triggerMask: number = 0;
|
40
|
+
|
41
|
+
/** Event invoked when this object enters a trigger zone */
|
28
42
|
@serializable(EventList)
|
29
43
|
onEnter?: EventList<any>;
|
44
|
+
|
45
|
+
/** Event invoked continuously while this object is inside a trigger zone */
|
30
46
|
@serializable(EventList)
|
31
47
|
onStay?: EventList<any>;
|
48
|
+
|
49
|
+
/** Event invoked when this object exits a trigger zone */
|
32
50
|
@serializable(EventList)
|
33
51
|
onExit?: EventList<any>;
|
34
52
|
|
53
|
+
/**
|
54
|
+
* Initializes the receiver and logs debug info if enabled
|
55
|
+
* @internal
|
56
|
+
*/
|
35
57
|
start() {
|
36
58
|
if (debug) console.log(this.name, this.triggerMask, this);
|
37
59
|
}
|
38
60
|
|
61
|
+
/**
|
62
|
+
* Checks for intersections with spatial triggers and fires appropriate events
|
63
|
+
* Handles enter, stay, and exit events for all relevant triggers
|
64
|
+
* @internal
|
65
|
+
*/
|
39
66
|
update(): void {
|
40
67
|
this.currentIntersected.length = 0;
|
41
68
|
for (const trigger of SpatialTrigger.triggers) {
|
@@ -59,23 +86,38 @@
|
|
59
86
|
}
|
60
87
|
this.lastIntersected.length = 0;
|
61
88
|
this.lastIntersected.push(...this.currentIntersected);
|
62
|
-
|
63
89
|
}
|
64
90
|
|
91
|
+
/** Array of triggers currently intersecting with this receiver */
|
65
92
|
readonly currentIntersected: SpatialTrigger[] = [];
|
93
|
+
|
94
|
+
/** Array of triggers that intersected with this receiver in the previous frame */
|
66
95
|
readonly lastIntersected: SpatialTrigger[] = [];
|
67
96
|
|
97
|
+
/**
|
98
|
+
* Handles trigger enter events.
|
99
|
+
* @param trigger The spatial trigger that was entered
|
100
|
+
*/
|
68
101
|
onEnterTrigger(trigger: SpatialTrigger): void {
|
69
102
|
if(debug) console.log("ENTER TRIGGER", this.name, trigger.name, this, trigger);
|
70
103
|
trigger.raiseOnEnterEvent(this);
|
71
104
|
this.onEnter?.invoke();
|
72
105
|
}
|
106
|
+
|
107
|
+
/**
|
108
|
+
* Handles trigger exit events.
|
109
|
+
* @param trigger The spatial trigger that was exited
|
110
|
+
*/
|
73
111
|
onExitTrigger(trigger: SpatialTrigger): void {
|
74
112
|
if(debug) console.log("EXIT TRIGGER", this.name, trigger.name, );
|
75
113
|
trigger.raiseOnExitEvent(this);
|
76
114
|
this.onExit?.invoke();
|
77
115
|
}
|
78
116
|
|
117
|
+
/**
|
118
|
+
* Handles trigger stay events.
|
119
|
+
* @param trigger The spatial trigger that the receiver is staying in
|
120
|
+
*/
|
79
121
|
onStayTrigger(trigger: SpatialTrigger): void {
|
80
122
|
trigger.raiseOnStayEvent(this);
|
81
123
|
this.onStay?.invoke();
|
@@ -83,24 +125,38 @@
|
|
83
125
|
}
|
84
126
|
|
85
127
|
/**
|
86
|
-
* A trigger that
|
128
|
+
* A spatial trigger component that detects objects within a box-shaped area.
|
129
|
+
* Used to trigger events when objects enter, stay in, or exit the defined area
|
87
130
|
* @category Interactivity
|
88
131
|
* @group Components
|
89
132
|
*/
|
90
133
|
export class SpatialTrigger extends Behaviour {
|
91
134
|
|
135
|
+
/** Global registry of all active spatial triggers in the scene */
|
92
136
|
static triggers: SpatialTrigger[] = [];
|
93
137
|
|
138
|
+
/**
|
139
|
+
* Bitmask determining which receivers this trigger affects.
|
140
|
+
* Only receivers with matching masks will be triggered.
|
141
|
+
*/
|
142
|
+
// 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?
|
94
143
|
@serializable()
|
95
144
|
triggerMask?: number;
|
96
145
|
|
146
|
+
/** Box helper component used to visualize and calculate the trigger area */
|
97
147
|
private boxHelper?: BoxHelperComponent;
|
98
148
|
|
149
|
+
/**
|
150
|
+
* Initializes the trigger and logs debug info if enabled
|
151
|
+
*/
|
99
152
|
start() {
|
100
153
|
if (debug)
|
101
154
|
console.log(this.name, this.triggerMask, this);
|
102
155
|
}
|
103
156
|
|
157
|
+
/**
|
158
|
+
* Registers this trigger in the global registry and sets up debug visualization if enabled
|
159
|
+
*/
|
104
160
|
onEnable(): void {
|
105
161
|
SpatialTrigger.triggers.push(this);
|
106
162
|
if (!this.boxHelper) {
|
@@ -108,10 +164,19 @@
|
|
108
164
|
this.boxHelper?.showHelper(null, debug as boolean);
|
109
165
|
}
|
110
166
|
}
|
167
|
+
|
168
|
+
/**
|
169
|
+
* Removes this trigger from the global registry when disabled
|
170
|
+
*/
|
111
171
|
onDisable(): void {
|
112
172
|
SpatialTrigger.triggers.splice(SpatialTrigger.triggers.indexOf(this), 1);
|
113
173
|
}
|
114
174
|
|
175
|
+
/**
|
176
|
+
* Tests if an object is inside this trigger's box
|
177
|
+
* @param obj The object to test against this trigger
|
178
|
+
* @returns True if the object is inside the trigger box
|
179
|
+
*/
|
115
180
|
test(obj: Object3D): boolean {
|
116
181
|
if (!this.boxHelper) return false;
|
117
182
|
return this.boxHelper.isInBox(obj) ?? false;
|
@@ -119,6 +184,10 @@
|
|
119
184
|
|
120
185
|
// private args: SpatialTriggerEventArgs = new SpatialTriggerEventArgs();
|
121
186
|
|
187
|
+
/**
|
188
|
+
* Raises the onEnter event on any SpatialTriggerReceiver components attached to this trigger's GameObject
|
189
|
+
* @param rec The receiver that entered this trigger
|
190
|
+
*/
|
122
191
|
raiseOnEnterEvent(rec: SpatialTriggerReceiver) {
|
123
192
|
// this.args.trigger = this;
|
124
193
|
// this.args.source = rec;
|
@@ -130,6 +199,10 @@
|
|
130
199
|
}, false);
|
131
200
|
}
|
132
201
|
|
202
|
+
/**
|
203
|
+
* Raises the onStay event on any SpatialTriggerReceiver components attached to this trigger's GameObject
|
204
|
+
* @param rec The receiver that is staying in this trigger
|
205
|
+
*/
|
133
206
|
raiseOnStayEvent(rec: SpatialTriggerReceiver) {
|
134
207
|
// this.args.trigger = this;
|
135
208
|
// this.args.source = rec;
|
@@ -141,6 +214,10 @@
|
|
141
214
|
}, false);
|
142
215
|
}
|
143
216
|
|
217
|
+
/**
|
218
|
+
* Raises the onExit event on any SpatialTriggerReceiver components attached to this trigger's GameObject
|
219
|
+
* @param rec The receiver that exited this trigger
|
220
|
+
*/
|
144
221
|
raiseOnExitEvent(rec: SpatialTriggerReceiver) {
|
145
222
|
GameObject.foreachComponent(this.gameObject, c => {
|
146
223
|
if (c === rec) return;
|
@@ -17,50 +17,77 @@
|
|
17
17
|
import { XRStateFlag } from "./webxr/XRFlag.js";
|
18
18
|
|
19
19
|
|
20
|
+
/**
|
21
|
+
* Defines the viewing perspective in spectator mode
|
22
|
+
*/
|
20
23
|
export enum SpectatorMode {
|
24
|
+
/** View from the perspective of the followed player */
|
21
25
|
FirstPerson = 0,
|
26
|
+
/** Freely view from a third-person perspective */
|
22
27
|
ThirdPerson = 1,
|
23
28
|
}
|
24
29
|
|
25
30
|
const debug = getParam("debugspectator");
|
26
31
|
|
27
32
|
/**
|
33
|
+
* Provides functionality to follow and spectate other users in a networked environment.
|
34
|
+
* Handles camera switching, following behavior, and network synchronization for spectator mode.
|
35
|
+
*
|
36
|
+
* Debug mode can be enabled with the URL parameter `?debugspectator`, which provides additional console output.
|
37
|
+
*
|
28
38
|
* @category Networking
|
29
39
|
* @group Components
|
30
40
|
*/
|
31
41
|
export class SpectatorCamera extends Behaviour {
|
32
42
|
|
43
|
+
/** Reference to the Camera component on this GameObject */
|
33
44
|
cam: Camera | null = null;
|
34
45
|
|
35
|
-
/**
|
46
|
+
/**
|
47
|
+
* When enabled, pressing F will send a request to all connected users to follow the local player.
|
48
|
+
* Pressing ESC will stop spectating.
|
49
|
+
*/
|
36
50
|
@serializable()
|
37
51
|
useKeys: boolean = true;
|
38
52
|
|
39
53
|
private _mode: SpectatorMode = SpectatorMode.FirstPerson;
|
40
54
|
|
55
|
+
/** Gets the current spectator perspective mode */
|
41
56
|
get mode() { return this._mode; }
|
57
|
+
/** Sets the current spectator perspective mode */
|
42
58
|
set mode(val: SpectatorMode) {
|
43
59
|
this._mode = val;
|
44
60
|
}
|
45
61
|
|
46
|
-
/**
|
62
|
+
/** Returns whether this user is currently spectating another user */
|
47
63
|
get isSpectating(): boolean {
|
48
64
|
return this._handler?.currentTarget !== undefined;
|
49
65
|
}
|
50
66
|
|
67
|
+
/**
|
68
|
+
* Checks if this instance is spectating the user with the given ID
|
69
|
+
* @param userId The user ID to check
|
70
|
+
* @returns True if spectating the specified user, false otherwise
|
71
|
+
*/
|
51
72
|
isSpectatingUser(userId: string): boolean {
|
52
73
|
return this.target?.userId === userId;
|
53
74
|
}
|
54
75
|
|
76
|
+
/**
|
77
|
+
* Checks if the user with the specified ID is following this user
|
78
|
+
* @param userId The user ID to check
|
79
|
+
* @returns True if the specified user is following this user, false otherwise
|
80
|
+
*/
|
55
81
|
isFollowedBy(userId: string): boolean {
|
56
82
|
return this.followers?.includes(userId);
|
57
83
|
}
|
58
84
|
|
59
|
-
/**
|
85
|
+
/** List of user IDs that are currently following the user */
|
60
86
|
get followers(): string[] {
|
61
87
|
return this._networking.followers;
|
62
88
|
}
|
63
89
|
|
90
|
+
/** Stops the current spectating session */
|
64
91
|
stopSpectating() {
|
65
92
|
if (this.context.isInXR) {
|
66
93
|
this.followSelf();
|
@@ -69,11 +96,15 @@
|
|
69
96
|
this.target = undefined;
|
70
97
|
}
|
71
98
|
|
99
|
+
/** Gets the local player's connection ID */
|
72
100
|
private get localId() : string {
|
73
101
|
return this.context.connection.connectionId ?? "local";
|
74
102
|
}
|
75
103
|
|
76
|
-
/**
|
104
|
+
/**
|
105
|
+
* Sets the player view to follow
|
106
|
+
* @param target The PlayerView to follow, or undefined to stop spectating
|
107
|
+
*/
|
77
108
|
set target(target: PlayerView | undefined) {
|
78
109
|
if (this._handler) {
|
79
110
|
|
@@ -106,14 +137,17 @@
|
|
106
137
|
}
|
107
138
|
}
|
108
139
|
|
140
|
+
/** Gets the currently followed player view */
|
109
141
|
get target(): PlayerView | undefined {
|
110
142
|
return this._handler?.currentTarget;
|
111
143
|
}
|
112
144
|
|
145
|
+
/** Sends a network request for all users to follow this player */
|
113
146
|
requestAllFollowMe() {
|
114
147
|
this._networking.onRequestFollowMe();
|
115
148
|
}
|
116
149
|
|
150
|
+
/** Determines if the camera is spectating the local player */
|
117
151
|
private get isSpectatingSelf() {
|
118
152
|
return this.isSpectating && this.target?.currentObject === this.context.players.getPlayerView(this.localId)?.currentObject;
|
119
153
|
}
|
@@ -131,7 +165,6 @@
|
|
131
165
|
private _networking!: SpectatorCamNetworking;
|
132
166
|
|
133
167
|
awake(): void {
|
134
|
-
|
135
168
|
this._debug = new SpectatorSelectionController(this.context, this);
|
136
169
|
this._networking = new SpectatorCamNetworking(this.context, this);
|
137
170
|
this._networking.awake();
|
@@ -144,7 +177,6 @@
|
|
144
177
|
return;
|
145
178
|
}
|
146
179
|
|
147
|
-
|
148
180
|
if (!this._handler && this.cam)
|
149
181
|
this._handler = new SpectatorHandler(this.context, this.cam, this);
|
150
182
|
|
@@ -157,6 +189,10 @@
|
|
157
189
|
this._networking?.destroy();
|
158
190
|
}
|
159
191
|
|
192
|
+
/**
|
193
|
+
* Checks if the current platform supports spectator mode
|
194
|
+
* @returns True if the platform is supported, false otherwise
|
195
|
+
*/
|
160
196
|
private isSupportedPlatform() {
|
161
197
|
const ua = window.navigator.userAgent;
|
162
198
|
const standalone = /Windows|MacOS/.test(ua);
|
@@ -164,12 +200,19 @@
|
|
164
200
|
return standalone && !isHololens;
|
165
201
|
}
|
166
202
|
|
203
|
+
/**
|
204
|
+
* Called before entering WebXR mode
|
205
|
+
* @param _evt The WebXR event
|
206
|
+
*/
|
167
207
|
onBeforeXR(_evt) {
|
168
208
|
if (!this.isSupportedPlatform()) return;
|
169
209
|
GameObject.setActive(this.gameObject, true);
|
170
210
|
}
|
171
211
|
|
172
|
-
|
212
|
+
/**
|
213
|
+
* Called when entering WebXR mode
|
214
|
+
* @param _evt The WebXR event
|
215
|
+
*/
|
173
216
|
onEnterXR(_evt) {
|
174
217
|
if (!this.isSupportedPlatform()) return;
|
175
218
|
if (debug) console.log(this.context.mainCamera);
|
@@ -178,6 +221,10 @@
|
|
178
221
|
}
|
179
222
|
}
|
180
223
|
|
224
|
+
/**
|
225
|
+
* Called when exiting WebXR mode
|
226
|
+
* @param _evt The WebXR event
|
227
|
+
*/
|
181
228
|
onLeaveXR(_evt) {
|
182
229
|
this.context.removeCamera(this.cam as ICamera);
|
183
230
|
GameObject.setActive(this.gameObject, false);
|
@@ -188,7 +235,9 @@
|
|
188
235
|
this.stopSpectating();
|
189
236
|
}
|
190
237
|
|
191
|
-
|
238
|
+
/**
|
239
|
+
* Sets the target to follow the local player
|
240
|
+
*/
|
192
241
|
private followSelf() {
|
193
242
|
this.target = this.context.players.getPlayerView(this.context.connection.connectionId);
|
194
243
|
if (!this.target) {
|
@@ -201,6 +250,9 @@
|
|
201
250
|
// TODO: only show Spectator cam for DesktopVR;
|
202
251
|
// don't show for AR, don't show on Quest
|
203
252
|
// TODO: properly align cameras on enter/exit VR, seems currently spectator cam breaks alignment
|
253
|
+
/**
|
254
|
+
* Called after the main rendering pass to render the spectator view
|
255
|
+
*/
|
204
256
|
onAfterRender(): void {
|
205
257
|
if (!this.cam) return;
|
206
258
|
|
@@ -278,6 +330,9 @@
|
|
278
330
|
this.resetAvatarFlags();
|
279
331
|
}
|
280
332
|
|
333
|
+
/**
|
334
|
+
* Updates avatar visibility flags for rendering in spectator mode
|
335
|
+
*/
|
281
336
|
private setAvatarFlagsBeforeRender() {
|
282
337
|
const isFirstPersonMode = this._mode === SpectatorMode.FirstPerson;
|
283
338
|
|
@@ -295,6 +350,9 @@
|
|
295
350
|
}
|
296
351
|
}
|
297
352
|
|
353
|
+
/**
|
354
|
+
* Restores avatar visibility flags after spectator rendering
|
355
|
+
*/
|
298
356
|
private resetAvatarFlags() {
|
299
357
|
for (const av of AvatarMarker.instances) {
|
300
358
|
if (av.avatar && "flags" in av.avatar) {
|
@@ -313,6 +371,9 @@
|
|
313
371
|
}
|
314
372
|
}
|
315
373
|
|
374
|
+
/**
|
375
|
+
* Interface for handling spectator camera behavior
|
376
|
+
*/
|
316
377
|
interface ISpectatorHandler {
|
317
378
|
context: Context;
|
318
379
|
get currentTarget(): PlayerView | undefined;
|
@@ -322,6 +383,9 @@
|
|
322
383
|
destroy();
|
323
384
|
}
|
324
385
|
|
386
|
+
/**
|
387
|
+
* Handles the smooth following behavior for the spectator camera
|
388
|
+
*/
|
325
389
|
class SpectatorHandler implements ISpectatorHandler {
|
326
390
|
|
327
391
|
readonly context: Context;
|
@@ -333,6 +397,7 @@
|
|
333
397
|
private view?: PlayerView;
|
334
398
|
private currentObject: Object3D | undefined;
|
335
399
|
|
400
|
+
/** Gets the currently targeted player view */
|
336
401
|
get currentTarget(): PlayerView | undefined {
|
337
402
|
return this.view;
|
338
403
|
}
|
@@ -343,6 +408,10 @@
|
|
343
408
|
this.spectator = spectator;
|
344
409
|
}
|
345
410
|
|
411
|
+
/**
|
412
|
+
* Sets the target player view to follow
|
413
|
+
* @param view The PlayerView to follow
|
414
|
+
*/
|
346
415
|
set(view?: PlayerView): void {
|
347
416
|
const followObject = view?.currentObject;
|
348
417
|
if (!followObject) {
|
@@ -368,6 +437,7 @@
|
|
368
437
|
else this.context.removeCamera(this.cam as ICamera);
|
369
438
|
}
|
370
439
|
|
440
|
+
/** Disables the spectator following behavior */
|
371
441
|
disable() {
|
372
442
|
if (debug) console.log("STOP FOLLOW", this.currentObject);
|
373
443
|
this.view = undefined;
|
@@ -377,12 +447,17 @@
|
|
377
447
|
this.follow.enabled = false;
|
378
448
|
}
|
379
449
|
|
450
|
+
/** Cleans up resources used by the handler */
|
380
451
|
destroy() {
|
381
452
|
this.target?.removeFromParent();
|
382
453
|
if (this.follow)
|
383
454
|
GameObject.destroy(this.follow);
|
384
455
|
}
|
385
456
|
|
457
|
+
/**
|
458
|
+
* Updates the camera position and orientation based on the spectator mode
|
459
|
+
* @param mode The current spectator mode (first or third person)
|
460
|
+
*/
|
386
461
|
update(mode: SpectatorMode) {
|
387
462
|
if (this.currentTarget?.isConnected === false || this.currentTarget?.removed === true) {
|
388
463
|
if (debug) console.log("Target disconnected or timeout", this.currentTarget);
|
@@ -431,13 +506,13 @@
|
|
431
506
|
target.quaternion.copy(_inverseYQuat);
|
432
507
|
else target.quaternion.identity();
|
433
508
|
}
|
434
|
-
|
435
|
-
|
436
509
|
}
|
437
510
|
|
438
511
|
const _inverseYQuat = new Quaternion().setFromAxisAngle(new Vector3(0, 1, 0), Math.PI);
|
439
512
|
|
440
|
-
|
513
|
+
/**
|
514
|
+
* Handles user input for selecting targets to spectate
|
515
|
+
*/
|
441
516
|
class SpectatorSelectionController {
|
442
517
|
|
443
518
|
private readonly context: Context;
|
@@ -468,6 +543,9 @@
|
|
468
543
|
});
|
469
544
|
}
|
470
545
|
|
546
|
+
/**
|
547
|
+
* Attempts to select an avatar to spectate through raycasting
|
548
|
+
*/
|
471
549
|
private trySelectObject() {
|
472
550
|
const opts = new RaycastOptions();
|
473
551
|
opts.setMask(0xffffff);
|
@@ -491,17 +569,17 @@
|
|
491
569
|
}
|
492
570
|
}
|
493
571
|
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
572
|
+
/**
|
573
|
+
* Network model for communicating follower changes
|
574
|
+
*/
|
498
575
|
class SpectatorFollowerChangedEventModel implements IModel {
|
499
|
-
/**
|
576
|
+
/** The user ID that is following */
|
500
577
|
guid: string;
|
501
578
|
readonly dontSave: boolean = true;
|
502
579
|
|
503
|
-
/**
|
580
|
+
/** The user ID being followed */
|
504
581
|
targetUserId: string | undefined;
|
582
|
+
/** Indicates if the user stopped following */
|
505
583
|
stoppedFollowing: boolean;
|
506
584
|
|
507
585
|
constructor(connectionId: string, userId: string | undefined, stoppedFollowing: boolean) {
|
@@ -511,6 +589,9 @@
|
|
511
589
|
}
|
512
590
|
}
|
513
591
|
|
592
|
+
/**
|
593
|
+
* Network model for requesting users to follow a specific player
|
594
|
+
*/
|
514
595
|
class SpectatorFollowEventModel implements IModel {
|
515
596
|
guid: string;
|
516
597
|
userId: string | undefined;
|
@@ -521,11 +602,14 @@
|
|
521
602
|
}
|
522
603
|
}
|
523
604
|
|
605
|
+
/**
|
606
|
+
* Handles network communication for spectator functionality
|
607
|
+
*/
|
524
608
|
class SpectatorCamNetworking {
|
525
609
|
|
610
|
+
/** List of user IDs currently following this player */
|
526
611
|
readonly followers: string[] = [];
|
527
612
|
|
528
|
-
|
529
613
|
private readonly context: Context;
|
530
614
|
private readonly spectator: SpectatorCamera;
|
531
615
|
private _followerEventMethod: Function;
|
@@ -540,6 +624,9 @@
|
|
540
624
|
this._joinedRoomMethod = this.onUserJoinedRoom.bind(this);
|
541
625
|
}
|
542
626
|
|
627
|
+
/**
|
628
|
+
* Initializes network event listeners
|
629
|
+
*/
|
543
630
|
awake() {
|
544
631
|
this.context.connection.beginListen("spectator-follower-changed", this._followerEventMethod);
|
545
632
|
this.context.connection.beginListen("spectator-request-follow", this._requestFollowMethod);
|
@@ -555,12 +642,20 @@
|
|
555
642
|
});
|
556
643
|
}
|
557
644
|
|
645
|
+
/**
|
646
|
+
* Removes network event listeners
|
647
|
+
*/
|
558
648
|
destroy() {
|
559
649
|
this.context.connection.stopListen("spectator-follower-changed", this._followerEventMethod);
|
560
650
|
this.context.connection.stopListen("spectator-request-follow", this._requestFollowMethod);
|
561
651
|
this.context.connection.stopListen(RoomEvents.JoinedRoom, this._joinedRoomMethod);
|
562
652
|
}
|
563
653
|
|
654
|
+
/**
|
655
|
+
* Notifies other users about spectating target changes
|
656
|
+
* @param target The new target being spectated
|
657
|
+
* @param _prevId The previous target's user ID
|
658
|
+
*/
|
564
659
|
onSpectatedObjectChanged(target: PlayerView | undefined, _prevId?: string) {
|
565
660
|
if (debug)
|
566
661
|
console.log(this.context.connection.connectionId, "onSpectatedObjectChanged", target, _prevId);
|
@@ -572,6 +667,10 @@
|
|
572
667
|
}
|
573
668
|
}
|
574
669
|
|
670
|
+
/**
|
671
|
+
* Requests other users to follow this player or stop following
|
672
|
+
* @param stop Whether to request users to stop following
|
673
|
+
*/
|
575
674
|
onRequestFollowMe(stop: boolean = false) {
|
576
675
|
if (debug)
|
577
676
|
console.log("Request follow", this.context.connection.connectionId);
|
@@ -583,12 +682,19 @@
|
|
583
682
|
}
|
584
683
|
}
|
585
684
|
|
685
|
+
/**
|
686
|
+
* Handles room join events
|
687
|
+
*/
|
586
688
|
private onUserJoinedRoom() {
|
587
689
|
if (getParam("followme")) {
|
588
690
|
this.onRequestFollowMe();
|
589
691
|
}
|
590
692
|
}
|
591
693
|
|
694
|
+
/**
|
695
|
+
* Processes follower status change events from the network
|
696
|
+
* @param evt The follower change event data
|
697
|
+
*/
|
592
698
|
private onFollowerEvent(evt: SpectatorFollowerChangedEventModel) {
|
593
699
|
const userBeingFollowed = evt.targetUserId;
|
594
700
|
const userThatIsFollowing = evt.guid;
|
@@ -615,6 +721,9 @@
|
|
615
721
|
}
|
616
722
|
}
|
617
723
|
|
724
|
+
/**
|
725
|
+
* Removes followers that are no longer connected to the room
|
726
|
+
*/
|
618
727
|
private removeDisconnectedFollowers() {
|
619
728
|
for (let i = this.followers.length - 1; i >= 0; i--) {
|
620
729
|
const id = this.followers[i];
|
@@ -626,6 +735,11 @@
|
|
626
735
|
|
627
736
|
private _lastRequestFollowUser: SpectatorFollowEventModel | undefined;
|
628
737
|
|
738
|
+
/**
|
739
|
+
* Handles follow requests from other users
|
740
|
+
* @param evt The follow request event
|
741
|
+
* @returns True if the request was handled successfully
|
742
|
+
*/
|
629
743
|
private onRequestFollowEvent(evt: SpectatorFollowEventModel) {
|
630
744
|
this._lastRequestFollowUser = evt;
|
631
745
|
|
@@ -652,6 +766,10 @@
|
|
652
766
|
}
|
653
767
|
|
654
768
|
private _enforceFollowInterval: any;
|
769
|
+
|
770
|
+
/**
|
771
|
+
* Periodically retries following a user if the initial attempt failed
|
772
|
+
*/
|
655
773
|
private enforceFollow() {
|
656
774
|
if (this._enforceFollowInterval) return;
|
657
775
|
this._enforceFollowInterval = setInterval(() => {
|
@@ -155,7 +155,11 @@
|
|
155
155
|
export class SpriteSheet {
|
156
156
|
|
157
157
|
@serializable(Sprite)
|
158
|
-
sprites
|
158
|
+
sprites: Sprite[];
|
159
|
+
|
160
|
+
constructor() {
|
161
|
+
this.sprites = [];
|
162
|
+
}
|
159
163
|
}
|
160
164
|
|
161
165
|
export class SpriteData {
|
@@ -171,6 +175,13 @@
|
|
171
175
|
// hence the spriteSheet field is undefined by default
|
172
176
|
constructor() { }
|
173
177
|
|
178
|
+
clone() {
|
179
|
+
const i = new SpriteData();
|
180
|
+
i.index = this.index;
|
181
|
+
i.spriteSheet = this.spriteSheet;
|
182
|
+
return i;
|
183
|
+
}
|
184
|
+
|
174
185
|
/**
|
175
186
|
* Set the sprite to be rendered in the currently assigned sprite sheet at the currently active index {@link index}
|
176
187
|
*/
|
@@ -320,7 +331,6 @@
|
|
320
331
|
if (typeof value === "number") {
|
321
332
|
const index = Math.floor(value);
|
322
333
|
this.spriteIndex = index;
|
323
|
-
return;
|
324
334
|
}
|
325
335
|
else if (value instanceof Sprite) {
|
326
336
|
if (!this._spriteSheet) {
|
@@ -328,8 +338,8 @@
|
|
328
338
|
}
|
329
339
|
if (this._spriteSheet.sprite != value) {
|
330
340
|
this._spriteSheet.sprite = value;
|
331
|
-
this.updateSprite();
|
332
341
|
}
|
342
|
+
this.updateSprite();
|
333
343
|
}
|
334
344
|
else if (value != this._spriteSheet) {
|
335
345
|
this._spriteSheet = value;
|
@@ -342,7 +352,6 @@
|
|
342
352
|
*/
|
343
353
|
set spriteIndex(value: number) {
|
344
354
|
if (!this._spriteSheet) return;
|
345
|
-
if (value === this.spriteIndex) return;
|
346
355
|
this._spriteSheet.index = value;
|
347
356
|
this.updateSprite();
|
348
357
|
}
|
@@ -359,13 +368,19 @@
|
|
359
368
|
private _spriteSheet?: SpriteData;
|
360
369
|
private _currentSprite?: Mesh;
|
361
370
|
|
371
|
+
|
362
372
|
/** @internal */
|
363
373
|
awake(): void {
|
364
374
|
this._currentSprite = undefined;
|
375
|
+
|
365
376
|
if (!this._spriteSheet) {
|
366
|
-
this._spriteSheet =
|
367
|
-
this._spriteSheet.spriteSheet = new SpriteSheet();
|
377
|
+
this._spriteSheet = SpriteData.create();
|
368
378
|
}
|
379
|
+
else {
|
380
|
+
// Ensure each SpriteRenderer has a unique spritesheet instance for cases where sprite renderer are cloned at runtime and then different sprites are assigned to each instance
|
381
|
+
this._spriteSheet = this._spriteSheet.clone();
|
382
|
+
}
|
383
|
+
|
369
384
|
if (debug) {
|
370
385
|
console.log("Awake", this.name, this, this.sprite);
|
371
386
|
}
|
@@ -408,6 +423,7 @@
|
|
408
423
|
}
|
409
424
|
mat.transparent = true;
|
410
425
|
mat.toneMapped = this.toneMapped;
|
426
|
+
mat.depthWrite = false;
|
411
427
|
|
412
428
|
if (sprite.texture && !mat.wireframe) {
|
413
429
|
let tex = sprite.texture;
|
@@ -19,7 +19,12 @@
|
|
19
19
|
|
20
20
|
const builder = new flatbuffers.Builder();
|
21
21
|
|
22
|
-
/**
|
22
|
+
/**
|
23
|
+
* Creates a flatbuffer model containing the transform data of a game object. Used by {@link SyncedTransform}
|
24
|
+
* @param guid The unique identifier of the object to sync
|
25
|
+
* @param b The behavior component containing transform data
|
26
|
+
* @param fast Whether to use fast mode synchronization (syncs more frequently)
|
27
|
+
* @returns A Uint8Array containing the serialized transform data
|
23
28
|
*/
|
24
29
|
export function createTransformModel(guid: string, b: Behaviour, fast: boolean = true): Uint8Array {
|
25
30
|
builder.clear();
|
@@ -50,18 +55,28 @@
|
|
50
55
|
})
|
51
56
|
|
52
57
|
/**
|
53
|
-
* SyncedTransform
|
58
|
+
* SyncedTransform synchronizes the position and rotation of a game object over the network.
|
59
|
+
* It handles ownership transfer, interpolation, and network state updates automatically.
|
54
60
|
* @category Networking
|
55
61
|
* @group Components
|
56
62
|
*/
|
57
63
|
export class SyncedTransform extends Behaviour {
|
58
64
|
|
59
|
-
|
65
|
+
|
60
66
|
// public autoOwnership: boolean = true;
|
67
|
+
/** When true, overrides physics behavior when this object is owned by the local user */
|
61
68
|
public overridePhysics: boolean = true
|
69
|
+
|
70
|
+
/** Whether to smoothly interpolate position changes when receiving updates */
|
62
71
|
public interpolatePosition: boolean = true;
|
72
|
+
|
73
|
+
/** Whether to smoothly interpolate rotation changes when receiving updates */
|
63
74
|
public interpolateRotation: boolean = true;
|
75
|
+
|
76
|
+
/** When true, sends updates at a higher frequency, useful for fast-moving objects */
|
64
77
|
public fastMode: boolean = false;
|
78
|
+
|
79
|
+
/** When true, notifies other clients when this object is destroyed */
|
65
80
|
public syncDestroy: boolean = false;
|
66
81
|
|
67
82
|
// private _state!: SyncedTransformModel;
|
@@ -77,7 +92,10 @@
|
|
77
92
|
private _receivedFastUpdate: boolean = false;
|
78
93
|
private _shouldRequestOwnership: boolean = false;
|
79
94
|
|
80
|
-
/**
|
95
|
+
/**
|
96
|
+
* Requests ownership of this object on the network.
|
97
|
+
* You need to be connected to a room for this to work.
|
98
|
+
*/
|
81
99
|
public requestOwnership() {
|
82
100
|
if (debug)
|
83
101
|
console.log("Request ownership");
|
@@ -89,10 +107,18 @@
|
|
89
107
|
this._model.requestOwnership();
|
90
108
|
}
|
91
109
|
|
110
|
+
/**
|
111
|
+
* Checks if this client has ownership of the object
|
112
|
+
* @returns true if this client has ownership, false if not, undefined if ownership state is unknown
|
113
|
+
*/
|
92
114
|
public hasOwnership(): boolean | undefined {
|
93
115
|
return this._model?.hasOwnership ?? undefined;
|
94
116
|
}
|
95
117
|
|
118
|
+
/**
|
119
|
+
* Checks if the object is owned by any client
|
120
|
+
* @returns true if the object is owned, false if not, undefined if ownership state is unknown
|
121
|
+
*/
|
96
122
|
public isOwned(): boolean | undefined {
|
97
123
|
return this._model?.isOwned;
|
98
124
|
}
|
@@ -140,6 +166,9 @@
|
|
140
166
|
this.context.connection.stopListenBinary(SyncedTransformIdentifier, this.receivedDataCallback);
|
141
167
|
}
|
142
168
|
|
169
|
+
/**
|
170
|
+
* Attempts to retrieve and apply the last known network state for this transform
|
171
|
+
*/
|
143
172
|
private tryGetLastState() {
|
144
173
|
const model = this.context.connection.tryGetState(this.guid) as unknown as SyncedTransformModel;
|
145
174
|
if (model) this.onReceivedData(model);
|
@@ -147,6 +176,10 @@
|
|
147
176
|
|
148
177
|
private tempEuler: Euler = new Euler();
|
149
178
|
|
179
|
+
/**
|
180
|
+
* Handles incoming network data for this transform
|
181
|
+
* @param data The model containing transform information
|
182
|
+
*/
|
150
183
|
private onReceivedData(data: SyncedTransformModel) {
|
151
184
|
if (this.destroyed) return;
|
152
185
|
if (typeof data.guid === "function" && data.guid() === this.guid) {
|
@@ -183,7 +216,10 @@
|
|
183
216
|
}
|
184
217
|
}
|
185
218
|
|
186
|
-
/**
|
219
|
+
/**
|
220
|
+
* @internal
|
221
|
+
* Initializes tracking of position and rotation when component is enabled
|
222
|
+
*/
|
187
223
|
onEnable(): void {
|
188
224
|
this.lastWorldPos.copy(this.worldPosition);
|
189
225
|
this.lastWorldRotation.copy(this.worldQuaternion);
|
@@ -194,7 +230,10 @@
|
|
194
230
|
}
|
195
231
|
}
|
196
232
|
|
197
|
-
/**
|
233
|
+
/**
|
234
|
+
* @internal
|
235
|
+
* Releases ownership when component is disabled
|
236
|
+
*/
|
198
237
|
onDisable(): void {
|
199
238
|
if (this._model)
|
200
239
|
this._model.freeOwnership();
|
@@ -205,7 +244,11 @@
|
|
205
244
|
private lastWorldPos!: Vector3;
|
206
245
|
private lastWorldRotation!: Quaternion;
|
207
246
|
|
208
|
-
/**
|
247
|
+
/**
|
248
|
+
* @internal
|
249
|
+
* Handles transform synchronization before each render frame
|
250
|
+
* Sends updates when owner, receives and applies updates when not owner
|
251
|
+
*/
|
209
252
|
onBeforeRender() {
|
210
253
|
if (!this.activeAndEnabled || !this.context.connection.isConnected) return;
|
211
254
|
// console.log("BEFORE RENDER", this.destroyed, this.guid, this._model?.isOwned, this.name, this.gameObject);
|
@@ -8,27 +8,43 @@
|
|
8
8
|
import { SyncedTransform } from "./SyncedTransform.js";
|
9
9
|
|
10
10
|
/**
|
11
|
-
* TransformGizmo
|
11
|
+
* TransformGizmo displays manipulation controls for translating, rotating, and scaling objects in the scene.
|
12
|
+
* It wraps three.js {@link TransformControls} and provides keyboard shortcuts for changing modes and settings.
|
12
13
|
* @category Helpers
|
13
14
|
* @group Components
|
14
15
|
*/
|
15
16
|
export class TransformGizmo extends Behaviour {
|
16
17
|
|
18
|
+
/**
|
19
|
+
* When true, this is considered a helper gizmo and will only be shown if showGizmos is enabled in engine parameters.
|
20
|
+
*/
|
17
21
|
@serializable()
|
18
22
|
public isGizmo: boolean = false;
|
19
23
|
|
24
|
+
/**
|
25
|
+
* Specifies the translation grid snap value in world units.
|
26
|
+
* Applied when holding Shift while translating an object.
|
27
|
+
*/
|
20
28
|
@serializable()
|
21
29
|
public translationSnap: number = 1;
|
22
30
|
|
31
|
+
/**
|
32
|
+
* Specifies the rotation snap angle in degrees.
|
33
|
+
* Applied when holding Shift while rotating an object.
|
34
|
+
*/
|
23
35
|
@serializable()
|
24
36
|
public rotationSnapAngle: number = 15;
|
25
37
|
|
38
|
+
/**
|
39
|
+
* Specifies the scale snapping value.
|
40
|
+
* Applied when holding Shift while scaling an object.
|
41
|
+
*/
|
26
42
|
@serializable()
|
27
43
|
public scaleSnap: number = .25;
|
28
44
|
|
29
45
|
/**
|
30
|
-
*
|
31
|
-
* @returns The TransformControls instance.
|
46
|
+
* Gets the underlying three.js {@link TransformControls} instance.
|
47
|
+
* @returns The TransformControls instance or undefined if not initialized.
|
32
48
|
*/
|
33
49
|
get control() {
|
34
50
|
return this._control;
|
@@ -81,6 +97,10 @@
|
|
81
97
|
window.removeEventListener('keyup', this.windowKeyUpListener);
|
82
98
|
}
|
83
99
|
|
100
|
+
/**
|
101
|
+
* Enables grid snapping for transform operations according to set snap values.
|
102
|
+
* This applies the translationSnap, rotationSnapAngle, and scaleSnap properties to the controls.
|
103
|
+
*/
|
84
104
|
enableSnapping() {
|
85
105
|
if (this._control) {
|
86
106
|
this._control.setTranslationSnap(this.translationSnap);
|
@@ -89,6 +109,10 @@
|
|
89
109
|
}
|
90
110
|
}
|
91
111
|
|
112
|
+
/**
|
113
|
+
* Disables grid snapping for transform operations.
|
114
|
+
* Removes all snapping constraints from the transform controls.
|
115
|
+
*/
|
92
116
|
disableSnapping() {
|
93
117
|
if (this._control) {
|
94
118
|
this._control.setTranslationSnap(null);
|
@@ -97,6 +121,11 @@
|
|
97
121
|
}
|
98
122
|
}
|
99
123
|
|
124
|
+
/**
|
125
|
+
* Event handler for when dragging state changes.
|
126
|
+
* Disables orbit controls during dragging and requests ownership of the transform if it's synchronized.
|
127
|
+
* @param event The drag change event
|
128
|
+
*/
|
100
129
|
private onControlChangedEvent = (event) => {
|
101
130
|
const orbit = this.orbit;
|
102
131
|
if (orbit) orbit.enabled = !event.value;
|
@@ -109,7 +138,18 @@
|
|
109
138
|
}
|
110
139
|
}
|
111
140
|
|
112
|
-
|
141
|
+
/**
|
142
|
+
* Handles keyboard shortcuts for transform operations:
|
143
|
+
* - Q: Toggle local/world space
|
144
|
+
* - W: Translation mode
|
145
|
+
* - E: Rotation mode
|
146
|
+
* - R: Scale mode
|
147
|
+
* - Shift: Enable snapping (while held)
|
148
|
+
* - +/-: Adjust gizmo size
|
149
|
+
* - X/Y/Z: Toggle visibility of respective axis
|
150
|
+
* - Spacebar: Toggle controls enabled state
|
151
|
+
* @param event The keyboard event
|
152
|
+
*/
|
113
153
|
private windowKeyDownListener = (event) => {
|
114
154
|
if (!this.enabled) return;
|
115
155
|
if (!this._control) return;
|
@@ -162,6 +202,11 @@
|
|
162
202
|
}
|
163
203
|
}
|
164
204
|
|
205
|
+
/**
|
206
|
+
* Handles keyboard key release events.
|
207
|
+
* Currently only handles releasing Shift key to disable snapping.
|
208
|
+
* @param event The keyboard event
|
209
|
+
*/
|
165
210
|
private windowKeyUpListener = (event) => {
|
166
211
|
if (!this.enabled) return;
|
167
212
|
switch (event.keyCode) {
|
@@ -62,11 +62,11 @@
|
|
62
62
|
* Add a post processing effect to the stack and schedules the effect stack to be re-created.
|
63
63
|
*/
|
64
64
|
addEffect<T extends PostProcessingEffect | Effect>(effect: T): T {
|
65
|
-
|
66
65
|
let entry = effect as PostProcessingEffect;
|
67
66
|
if (!(entry instanceof PostProcessingEffect)) {
|
68
67
|
entry = new EffectWrapper(entry);
|
69
68
|
}
|
69
|
+
if(entry.gameObject === undefined) this.gameObject.addComponent(entry);
|
70
70
|
if (this._effects.includes(entry)) return effect;
|
71
71
|
this._effects.push(entry);
|
72
72
|
this._isDirty = true;
|
@@ -125,7 +125,7 @@
|
|
125
125
|
}
|
126
126
|
|
127
127
|
// ensure the profile is initialized
|
128
|
-
this.sharedProfile?.
|
128
|
+
this.sharedProfile?.__init(this);
|
129
129
|
}
|
130
130
|
|
131
131
|
onEnable(): void {
|
@@ -190,7 +190,7 @@
|
|
190
190
|
private _isDirty: boolean = false;
|
191
191
|
|
192
192
|
private apply() {
|
193
|
-
if (debug) console.log(
|
193
|
+
if (debug) console.log(`Apply PostProcessing "${this.name}"`);
|
194
194
|
|
195
195
|
if (isDevEnvironment()) {
|
196
196
|
if (this._lastApplyTime !== undefined && Date.now() - this._lastApplyTime < 100) {
|
@@ -209,18 +209,16 @@
|
|
209
209
|
if (this.sharedProfile?.components) {
|
210
210
|
const comps = this.sharedProfile.components;
|
211
211
|
for (const effect of comps) {
|
212
|
-
if (effect.active && !this._activeEffects.includes(effect))
|
212
|
+
if (effect.active && effect.enabled && !this._activeEffects.includes(effect))
|
213
213
|
this._activeEffects.push(effect);
|
214
214
|
}
|
215
215
|
}
|
216
216
|
// add effects registered via code
|
217
217
|
for (const effect of this._effects) {
|
218
|
-
if (effect.active && !this._activeEffects.includes(effect))
|
218
|
+
if (effect.active && effect.enabled && !this._activeEffects.includes(effect))
|
219
219
|
this._activeEffects.push(effect);
|
220
220
|
}
|
221
221
|
|
222
|
-
if (debug) console.log("Apply PostProcessing", this._activeEffects);
|
223
|
-
|
224
222
|
if (this._activeEffects.length > 0) {
|
225
223
|
if (!this._postprocessing)
|
226
224
|
this._postprocessing = new PostProcessingHandler(this.context);
|
@@ -1,6 +1,8 @@
|
|
1
1
|
import { serializeable } from "../../engine/engine_serialization_decorator.js";
|
2
2
|
import { getParam } from "../../engine/engine_utils.js";
|
3
|
+
import { Component } from "../Component.js";
|
3
4
|
import { PostProcessingEffect } from "./PostProcessingEffect.js";
|
5
|
+
import type { Volume } from "./Volume.js";
|
4
6
|
|
5
7
|
const debug = getParam("debugpost");
|
6
8
|
|
@@ -38,8 +40,14 @@
|
|
38
40
|
* call init on all components
|
39
41
|
* @hidden
|
40
42
|
**/
|
41
|
-
|
42
|
-
this.components?.forEach(c =>
|
43
|
+
__init(owner: Component) {
|
44
|
+
this.components?.forEach(c => {
|
45
|
+
// Make sure all components are added to the gameobject (this is not the case when e.g. effects are serialized from Unity)
|
46
|
+
if (c.gameObject === undefined) {
|
47
|
+
owner.gameObject.addComponent(c);
|
48
|
+
}
|
49
|
+
c.init()
|
50
|
+
});
|
43
51
|
}
|
44
52
|
|
45
53
|
addEffect(effect: PostProcessingEffect) {
|
@@ -49,18 +49,25 @@
|
|
49
49
|
}
|
50
50
|
}
|
51
51
|
|
52
|
+
private static _hasPlaced: boolean = false;
|
53
|
+
/**
|
54
|
+
* @returns true if the scene has been placed in AR by the user or automatic placement
|
55
|
+
*/
|
56
|
+
static get hasPlaced(): boolean {
|
57
|
+
return this._hasPlaced;
|
58
|
+
}
|
52
59
|
|
53
|
-
|
54
|
-
|
60
|
+
|
61
|
+
/** The scale of the user in AR.
|
62
|
+
* **NOTE**: a large value makes the scene appear smaller
|
55
63
|
* @default 1
|
56
64
|
*/
|
57
65
|
get arScale(): number {
|
58
66
|
return this._arScale;
|
59
67
|
}
|
60
68
|
set arScale(val: number) {
|
61
|
-
|
62
|
-
this.
|
63
|
-
this.onScaleChanged();
|
69
|
+
this._arScale = Math.max(0.000001, val);
|
70
|
+
this.onSetScale();
|
64
71
|
}
|
65
72
|
private _arScale: number = 1;
|
66
73
|
|
@@ -134,6 +141,7 @@
|
|
134
141
|
if (debug) console.log("ENTER WEBXR: SessionRoot start...");
|
135
142
|
|
136
143
|
this._anchor = null;
|
144
|
+
WebARSessionRoot._hasPlaced = false;
|
137
145
|
|
138
146
|
// if (_args.xr.session.enabledFeatures?.includes("image-tracking")) {
|
139
147
|
// console.warn("Image tracking is enabled - will not place scene");
|
@@ -193,6 +201,7 @@
|
|
193
201
|
this.onRevertSceneChanges();
|
194
202
|
// this._anchor?.delete();
|
195
203
|
this._anchor = null;
|
204
|
+
WebARSessionRoot._hasPlaced = false;
|
196
205
|
this._rigPlacementMatrix = undefined;
|
197
206
|
}
|
198
207
|
onUpdateXR(args: NeedleXREventArgs): void {
|
@@ -405,6 +414,7 @@
|
|
405
414
|
reticle.quaternion.copy(reticle["lastQuat"]);
|
406
415
|
|
407
416
|
this.onApplyPose(reticle);
|
417
|
+
WebARSessionRoot._hasPlaced = true;
|
408
418
|
|
409
419
|
if (this.useXRAnchor) {
|
410
420
|
this.onCreateAnchor(NeedleXRSession.active!, hit);
|
@@ -417,8 +427,16 @@
|
|
417
427
|
}
|
418
428
|
}
|
419
429
|
|
420
|
-
private
|
421
|
-
|
430
|
+
private onSetScale() {
|
431
|
+
if (!WebARSessionRoot._hasPlaced) return;
|
432
|
+
const rig = NeedleXRSession.active?.rig?.gameObject;
|
433
|
+
if (rig) {
|
434
|
+
const currentScale = NeedleXRSession.active?.rigScale || 1;
|
435
|
+
const newScale = (1 / this._arScale) * currentScale;
|
436
|
+
const scaleMatrix = new Matrix4().makeScale(newScale, newScale, newScale).invert();
|
437
|
+
rig.matrix.premultiply(scaleMatrix);
|
438
|
+
rig.matrix.decompose(rig.position, rig.quaternion, rig.scale);
|
439
|
+
}
|
422
440
|
}
|
423
441
|
|
424
442
|
private onRevertSceneChanges() {
|
@@ -516,7 +534,7 @@
|
|
516
534
|
console.warn("No rig object to place");
|
517
535
|
return;
|
518
536
|
}
|
519
|
-
const rigScale = NeedleXRSession.active?.rigScale || 1;
|
537
|
+
// const rigScale = NeedleXRSession.active?.rigScale || 1;
|
520
538
|
|
521
539
|
// save the previous rig parent
|
522
540
|
const previousParent = rigObject.parent || this.context.scene;
|
@@ -578,6 +596,10 @@
|
|
578
596
|
private _scale: number = 1;
|
579
597
|
private _hasChanged: boolean = false;
|
580
598
|
|
599
|
+
get scale() {
|
600
|
+
return this._scale;
|
601
|
+
}
|
602
|
+
|
581
603
|
// readonly translate: Vector3 = new Vector3();
|
582
604
|
// readonly rotation: Quaternion = new Quaternion();
|
583
605
|
// readonly scale: Vector3 = new Vector3(1, 1, 1);
|
@@ -594,6 +616,7 @@
|
|
594
616
|
reset() {
|
595
617
|
this._scale = 1;
|
596
618
|
this.offset.identity();
|
619
|
+
this._hasChanged = true;
|
597
620
|
}
|
598
621
|
get hasChanged() { return this._hasChanged; }
|
599
622
|
|
@@ -31,74 +31,127 @@
|
|
31
31
|
export class WebXR extends Behaviour {
|
32
32
|
|
33
33
|
// UI
|
34
|
-
/**
|
34
|
+
/**
|
35
|
+
* When enabled, a button will be automatically added to {@link NeedleMenu} that allows users to enter VR mode.
|
36
|
+
*/
|
35
37
|
@serializable()
|
36
38
|
createVRButton: boolean = true;
|
37
|
-
|
39
|
+
|
40
|
+
/**
|
41
|
+
* When enabled, a button will be automatically added to {@link NeedleMenu} that allows users to enter AR mode.
|
42
|
+
*/
|
38
43
|
@serializable()
|
39
44
|
createARButton: boolean = true;
|
40
|
-
|
45
|
+
|
46
|
+
/**
|
47
|
+
* When enabled, a button to send the experience to an Oculus Quest will be shown if the current device does not support VR.
|
48
|
+
* This helps direct users to compatible devices for optimal VR experiences.
|
49
|
+
*/
|
41
50
|
@serializable()
|
42
51
|
createSendToQuestButton: boolean = true;
|
43
|
-
|
52
|
+
|
53
|
+
/**
|
54
|
+
* When enabled, a QR code will be generated and displayed on desktop devices to allow easy opening of the experience on mobile devices.
|
55
|
+
*/
|
44
56
|
@serializable()
|
45
57
|
createQRCode: boolean = true;
|
46
58
|
|
47
59
|
// VR Settings
|
48
|
-
/**
|
60
|
+
/**
|
61
|
+
* When enabled, default movement controls will be automatically added to the scene when entering VR.
|
62
|
+
* This includes teleportation and smooth locomotion options for VR controllers.
|
63
|
+
*/
|
49
64
|
@serializable()
|
50
65
|
useDefaultControls: boolean = true;
|
51
|
-
|
66
|
+
|
67
|
+
/**
|
68
|
+
* When enabled, 3D models representing the user's VR controllers will be automatically created and rendered in the scene.
|
69
|
+
*/
|
52
70
|
@serializable()
|
53
71
|
showControllerModels: boolean = true;
|
54
|
-
|
72
|
+
|
73
|
+
/**
|
74
|
+
* When enabled, 3D models representing the user's hands will be automatically created and rendered when hand tracking is available.
|
75
|
+
*/
|
55
76
|
@serializable()
|
56
77
|
showHandModels: boolean = true;
|
57
78
|
|
58
79
|
// AR Settings
|
59
|
-
/**
|
80
|
+
/**
|
81
|
+
* When enabled, a reticle will be displayed to help place the scene in AR. The user must tap on a detected surface to position the scene.
|
82
|
+
*/
|
60
83
|
@serializable()
|
61
84
|
usePlacementReticle: boolean = true;
|
62
|
-
|
85
|
+
|
86
|
+
/**
|
87
|
+
* Optional custom 3D object to use as the AR placement reticle instead of the default one.
|
88
|
+
*/
|
63
89
|
@serializable(AssetReference)
|
64
90
|
customARPlacementReticle?: AssetReference;
|
65
|
-
|
91
|
+
|
92
|
+
/**
|
93
|
+
* When enabled, users can adjust the position, rotation, and scale of the AR scene with one or two fingers after initial placement.
|
94
|
+
*/
|
66
95
|
@serializable()
|
67
96
|
usePlacementAdjustment: boolean = true;
|
68
|
-
|
97
|
+
|
98
|
+
/**
|
99
|
+
* Determines the scale of the user relative to the scene in AR. Larger values make the 3D content appear smaller.
|
100
|
+
* Only applies when `usePlacementReticle` is enabled.
|
101
|
+
*/
|
69
102
|
@serializable()
|
70
103
|
arScale: number = 1;
|
71
|
-
|
104
|
+
|
105
|
+
/**
|
106
|
+
* When enabled, an XRAnchor will be created for the AR scene and its position will be regularly updated to match the anchor.
|
107
|
+
* This can help with spatial persistence in AR experiences.
|
108
|
+
* @experimental
|
109
|
+
*/
|
72
110
|
@serializable()
|
73
111
|
useXRAnchor: boolean = false;
|
112
|
+
|
74
113
|
/**
|
75
|
-
* When enabled the scene will be
|
114
|
+
* When enabled, the scene will be automatically placed as soon as a suitable surface is detected in AR,
|
115
|
+
* without requiring the user to tap to confirm placement.
|
76
116
|
*/
|
77
117
|
@serializable()
|
78
|
-
autoPlace: boolean =
|
79
|
-
|
118
|
+
autoPlace: boolean = false;
|
119
|
+
|
120
|
+
/**
|
121
|
+
* When enabled, the AR session root center will be automatically adjusted to place the center of the scene.
|
122
|
+
* This helps ensure the scene is properly aligned with detected surfaces.
|
123
|
+
*/
|
80
124
|
@serializable()
|
81
125
|
autoCenter: boolean = false;
|
82
126
|
|
83
|
-
/**
|
127
|
+
/**
|
128
|
+
* When enabled, a USDZExporter component will be automatically added to the scene if none is found.
|
129
|
+
* This allows iOS and visionOS devices to view 3D content using Apple's AR QuickLook.
|
130
|
+
*/
|
84
131
|
@serializable()
|
85
132
|
useQuicklookExport: boolean = false;
|
86
133
|
|
87
|
-
|
88
|
-
|
89
|
-
*
|
134
|
+
/**
|
135
|
+
* When enabled, the 'depth-sensing' WebXR feature will be requested to provide real-time depth occlusion.
|
136
|
+
* Currently only supported on Oculus Quest devices.
|
137
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/API/XRDepthInformation
|
138
|
+
* @experimental
|
90
139
|
*/
|
91
140
|
@serializable()
|
92
141
|
useDepthSensing: boolean = false;
|
93
142
|
|
94
143
|
/**
|
95
|
-
* When enabled
|
144
|
+
* When enabled, a {@link SpatialGrabRaycaster} will be added or enabled in the scene,
|
145
|
+
* allowing users to interact with objects at a distance in VR/AR.
|
96
146
|
* @default true
|
97
147
|
*/
|
98
148
|
@serializable()
|
99
149
|
useSpatialGrab: boolean = true;
|
100
150
|
|
101
|
-
/**
|
151
|
+
/**
|
152
|
+
* Specifies the avatar representation that will be created when entering a WebXR session.
|
153
|
+
* Can be a reference to a 3D model or a boolean to use the default avatar.
|
154
|
+
*/
|
102
155
|
@serializable(AssetReference)
|
103
156
|
defaultAvatar?: AssetReference | boolean;
|
104
157
|
|
@@ -111,10 +164,19 @@
|
|
111
164
|
|
112
165
|
static activeWebXRComponent: WebXR | null = null;
|
113
166
|
|
167
|
+
/**
|
168
|
+
* Initializes the WebXR component by obtaining the XR sync object for this context.
|
169
|
+
* @internal
|
170
|
+
*/
|
114
171
|
awake() {
|
115
172
|
NeedleXRSession.getXRSync(this.context);
|
116
173
|
}
|
117
174
|
|
175
|
+
/**
|
176
|
+
* Sets up the WebXR component when it's enabled. Checks for HTTPS connection,
|
177
|
+
* sets up USDZ export if enabled, creates UI buttons, and configures avatar settings.
|
178
|
+
* @internal
|
179
|
+
*/
|
118
180
|
onEnable(): void {
|
119
181
|
// check if we're on a secure connection:
|
120
182
|
if (window.location.protocol !== "https:") {
|
@@ -153,11 +215,21 @@
|
|
153
215
|
}
|
154
216
|
}
|
155
217
|
|
218
|
+
/**
|
219
|
+
* Cleans up resources when the component is disabled.
|
220
|
+
* Destroys the USDZ exporter if one was created and removes UI buttons.
|
221
|
+
* @internal
|
222
|
+
*/
|
156
223
|
onDisable(): void {
|
157
224
|
this._usdzExporter?.destroy();
|
158
225
|
this.removeButtons();
|
159
226
|
}
|
160
227
|
|
228
|
+
/**
|
229
|
+
* Checks if WebXR is supported and offers an appropriate session.
|
230
|
+
* This is used to show the WebXR session joining prompt in browsers that support it.
|
231
|
+
* @returns A Promise that resolves to true if a session was offered, false otherwise
|
232
|
+
*/
|
161
233
|
private async handleOfferSession() {
|
162
234
|
if (this.createVRButton) {
|
163
235
|
const hasVRSupport = await NeedleXRSession.isVRSupported();
|
@@ -182,16 +254,32 @@
|
|
182
254
|
get sessionMode(): XRSessionMode | null {
|
183
255
|
return NeedleXRSession.activeMode ?? null;;
|
184
256
|
}
|
257
|
+
/** While AR: this will return the currently active WebARSessionRoot component.
|
258
|
+
* You can also query this component in your scene with `findObjectOfType(WebARSessionRoot)`
|
259
|
+
*/
|
260
|
+
get arSessionRoot() {
|
261
|
+
return this._activeWebARSessionRoot;
|
262
|
+
}
|
185
263
|
|
186
|
-
/** Call to start an WebVR session
|
264
|
+
/** Call to start an WebVR session.
|
265
|
+
*
|
266
|
+
* This is a shorthand for `NeedleXRSession.start("immersive-vr", init, this.context)`
|
267
|
+
*/
|
187
268
|
async enterVR(init?: XRSessionInit): Promise<NeedleXRSession | null> {
|
188
269
|
return NeedleXRSession.start("immersive-vr", init, this.context);
|
189
270
|
}
|
190
|
-
/** Call to start an WebAR session
|
271
|
+
/** Call to start an WebAR session
|
272
|
+
*
|
273
|
+
* This is a shorthand for `NeedleXRSession.start("immersive-ar", init, this.context)`
|
274
|
+
*/
|
191
275
|
async enterAR(init?: XRSessionInit): Promise<NeedleXRSession | null> {
|
192
276
|
return NeedleXRSession.start("immersive-ar", init, this.context);
|
193
277
|
}
|
194
|
-
|
278
|
+
|
279
|
+
/** Call to end a WebXR (AR or VR) session.
|
280
|
+
*
|
281
|
+
* This is a shorthand for `NeedleXRSession.stop()`
|
282
|
+
*/
|
195
283
|
exitXR() {
|
196
284
|
NeedleXRSession.stop();
|
197
285
|
}
|
@@ -199,11 +287,18 @@
|
|
199
287
|
private _exitXRMenuButton?: HTMLElement;
|
200
288
|
private _previousXRState: number = 0;
|
201
289
|
private _spatialGrabRaycaster?: SpatialGrabRaycaster;
|
290
|
+
private _activeWebARSessionRoot: WebARSessionRoot | null = null;
|
202
291
|
|
203
292
|
private get isActiveWebXR() {
|
204
293
|
return !WebXR.activeWebXRComponent || WebXR.activeWebXRComponent === this;
|
205
294
|
}
|
206
295
|
|
296
|
+
/**
|
297
|
+
* Called before entering a WebXR session. Sets up optional features like depth sensing, if needed.
|
298
|
+
* @param _mode The XR session mode being requested (immersive-ar or immersive-vr)
|
299
|
+
* @param args The XRSessionInit object that will be passed to the WebXR API
|
300
|
+
* @internal
|
301
|
+
*/
|
207
302
|
onBeforeXR(_mode: XRSessionMode, args: XRSessionInit): void {
|
208
303
|
if (!this.isActiveWebXR) {
|
209
304
|
console.warn(`WebXR: another WebXR component is already active (${WebXR.activeWebXRComponent?.name}). This is ignored: ${this.name}`);
|
@@ -217,6 +312,12 @@
|
|
217
312
|
}
|
218
313
|
}
|
219
314
|
|
315
|
+
/**
|
316
|
+
* Called when a WebXR session begins. Sets up the scene for XR by configuring controllers,
|
317
|
+
* AR placement, and other features based on component settings.
|
318
|
+
* @param args Event arguments containing information about the started XR session
|
319
|
+
* @internal
|
320
|
+
*/
|
220
321
|
async onEnterXR(args: NeedleXREventArgs) {
|
221
322
|
if (!this.isActiveWebXR) return;
|
222
323
|
|
@@ -244,6 +345,7 @@
|
|
244
345
|
}
|
245
346
|
}
|
246
347
|
|
348
|
+
this._activeWebARSessionRoot = sessionroot;
|
247
349
|
if (sessionroot) {
|
248
350
|
// sessionroot.enabled = this.usePlacementReticle; // < not sure if we want to disable the session root when placement reticle if OFF...
|
249
351
|
sessionroot.customReticle = this.customARPlacementReticle;
|
@@ -288,6 +390,12 @@
|
|
288
390
|
}
|
289
391
|
}
|
290
392
|
|
393
|
+
/**
|
394
|
+
* Called every frame during an active WebXR session.
|
395
|
+
* Updates components that depend on the current XR state.
|
396
|
+
* @param _args Event arguments containing information about the current XR session frame
|
397
|
+
* @internal
|
398
|
+
*/
|
291
399
|
onUpdateXR(_args: NeedleXREventArgs): void {
|
292
400
|
if (!this.isActiveWebXR) return;
|
293
401
|
if (this._spatialGrabRaycaster) {
|
@@ -295,6 +403,12 @@
|
|
295
403
|
}
|
296
404
|
}
|
297
405
|
|
406
|
+
/**
|
407
|
+
* Called when a WebXR session ends. Restores pre-session state,
|
408
|
+
* removes temporary components, and cleans up resources.
|
409
|
+
* @param _ Event arguments containing information about the ended XR session
|
410
|
+
* @internal
|
411
|
+
*/
|
298
412
|
onLeaveXR(_: NeedleXREventArgs): void {
|
299
413
|
this._exitXRMenuButton?.remove();
|
300
414
|
|
@@ -310,6 +424,8 @@
|
|
310
424
|
}
|
311
425
|
this._createdComponentsInSession.length = 0;
|
312
426
|
|
427
|
+
this._activeWebARSessionRoot = null;
|
428
|
+
|
313
429
|
this.handleOfferSession();
|
314
430
|
|
315
431
|
delayForFrames(1).then(() => WebXR.activeWebXRComponent = null);
|
@@ -339,8 +455,10 @@
|
|
339
455
|
return models;
|
340
456
|
}
|
341
457
|
|
342
|
-
|
343
|
-
|
458
|
+
/**
|
459
|
+
* Creates and instantiates the user's avatar representation in the WebXR session.
|
460
|
+
* @param xr The active session
|
461
|
+
*/
|
344
462
|
protected async createLocalAvatar(xr: NeedleXRSession) {
|
345
463
|
if (this._playerSync && xr.running && typeof this.defaultAvatar != "boolean") {
|
346
464
|
this._playerSync.asset = this.defaultAvatar;
|
@@ -348,6 +466,11 @@
|
|
348
466
|
}
|
349
467
|
}
|
350
468
|
|
469
|
+
/**
|
470
|
+
* Event handler called when a player avatar is spawned.
|
471
|
+
* Ensures the avatar has the necessary Avatar component.
|
472
|
+
* @param instance The spawned avatar 3D object
|
473
|
+
*/
|
351
474
|
private onAvatarSpawned = (instance: Object3D) => {
|
352
475
|
// spawned webxr avatars must have a avatar component
|
353
476
|
if (debug) console.log("WebXR.onAvatarSpawned", instance);
|
@@ -360,13 +483,16 @@
|
|
360
483
|
|
361
484
|
// HTML UI
|
362
485
|
|
363
|
-
/** @deprecated use
|
486
|
+
/** @deprecated use {@link getButtonsFactory} or directly access {@link WebXRButtonFactory.getOrCreate} */
|
364
487
|
getButtonsContainer(): WebXRButtonFactory {
|
365
488
|
return this.getButtonsFactory();
|
366
489
|
}
|
367
490
|
|
368
|
-
/**
|
369
|
-
*
|
491
|
+
/**
|
492
|
+
* Returns the WebXR button factory, creating one if it doesn't exist.
|
493
|
+
* Use this to access and modify WebXR UI buttons.
|
494
|
+
* @returns The WebXRButtonFactory instance
|
495
|
+
*/
|
370
496
|
getButtonsFactory(): WebXRButtonFactory {
|
371
497
|
if (!this._buttonFactory) {
|
372
498
|
this._buttonFactory = WebXRButtonFactory.getOrCreate();
|
@@ -374,8 +500,15 @@
|
|
374
500
|
return this._buttonFactory;
|
375
501
|
}
|
376
502
|
|
503
|
+
/**
|
504
|
+
* Reference to the WebXR button factory used by this component.
|
505
|
+
*/
|
377
506
|
private _buttonFactory?: WebXRButtonFactory;
|
378
507
|
|
508
|
+
/**
|
509
|
+
* Creates and sets up UI elements for WebXR interaction based on component settings
|
510
|
+
* and device capabilities. Handles creating AR, VR, QuickLook buttons and utility buttons like QR codes.
|
511
|
+
*/
|
379
512
|
private handleCreatingHTML() {
|
380
513
|
const xrButtonsPriority = 50;
|
381
514
|
|
@@ -423,14 +556,25 @@
|
|
423
556
|
}
|
424
557
|
}
|
425
558
|
|
559
|
+
/**
|
560
|
+
* Storage for UI buttons created by this component.
|
561
|
+
*/
|
426
562
|
private readonly _buttons: HTMLElement[] = [];
|
427
563
|
|
564
|
+
/**
|
565
|
+
* Adds a button to the UI with the specified priority.
|
566
|
+
* @param button The HTML element to add
|
567
|
+
* @param priority The button's priority value (lower numbers appear first)
|
568
|
+
*/
|
428
569
|
private addButton(button: HTMLElement, priority: number) {
|
429
570
|
this._buttons.push(button);
|
430
571
|
button.setAttribute("priority", priority.toString());
|
431
572
|
this.context.menu.appendChild(button);
|
432
573
|
}
|
433
574
|
|
575
|
+
/**
|
576
|
+
* Removes all buttons created by this component from the UI.
|
577
|
+
*/
|
434
578
|
private removeButtons() {
|
435
579
|
for (const button of this._buttons) {
|
436
580
|
button.remove();
|