@@ -1,5 +1,5 @@
|
|
1
1
|
import { TypeStore } from "./../engine_typestore"
|
2
|
-
|
2
|
+
|
3
3
|
// Import types
|
4
4
|
import { __Ignore } from "../../engine-components/codegen/components";
|
5
5
|
import { ActionBuilder } from "../../engine-components/export/usdz/extensions/behavior/BehavioursBuilder";
|
@@ -14,6 +14,7 @@
|
|
14
14
|
import { AnimatorController } from "../../engine-components/AnimatorController";
|
15
15
|
import { Antialiasing } from "../../engine-components/postprocessing/Effects/Antialiasing";
|
16
16
|
import { AttachedObject } from "../../engine-components/webxr/WebXRController";
|
17
|
+
import { AudioExtension } from "../../engine-components/export/usdz/extensions/behavior/AudioExtension";
|
17
18
|
import { AudioListener } from "../../engine-components/AudioListener";
|
18
19
|
import { AudioSource } from "../../engine-components/AudioSource";
|
19
20
|
import { AudioTrackHandler } from "../../engine-components/timeline/TimelineTracks";
|
@@ -116,6 +117,7 @@
|
|
116
117
|
import { PixelationEffect } from "../../engine-components/postprocessing/Effects/Pixelation";
|
117
118
|
import { PlayableDirector } from "../../engine-components/timeline/PlayableDirector";
|
118
119
|
import { PlayAnimationOnClick } from "../../engine-components/export/usdz/extensions/behavior/BehaviourComponents";
|
120
|
+
import { PlayAudioOnClick } from "../../engine-components/export/usdz/extensions/behavior/BehaviourComponents";
|
119
121
|
import { PlayerColor } from "../../engine-components/PlayerColor";
|
120
122
|
import { PlayerState } from "../../engine-components-experimental/networking/PlayerSync";
|
121
123
|
import { PlayerSync } from "../../engine-components-experimental/networking/PlayerSync";
|
@@ -213,7 +215,7 @@
|
|
213
215
|
import { XRGrabRendering } from "../../engine-components/webxr/WebXRGrabRendering";
|
214
216
|
import { XRRig } from "../../engine-components/webxr/WebXRRig";
|
215
217
|
import { XRState } from "../../engine-components/XRFlag";
|
216
|
-
|
218
|
+
|
217
219
|
// Register types
|
218
220
|
TypeStore.add("__Ignore", __Ignore);
|
219
221
|
TypeStore.add("ActionBuilder", ActionBuilder);
|
@@ -228,6 +230,7 @@
|
|
228
230
|
TypeStore.add("AnimatorController", AnimatorController);
|
229
231
|
TypeStore.add("Antialiasing", Antialiasing);
|
230
232
|
TypeStore.add("AttachedObject", AttachedObject);
|
233
|
+
TypeStore.add("AudioExtension", AudioExtension);
|
231
234
|
TypeStore.add("AudioListener", AudioListener);
|
232
235
|
TypeStore.add("AudioSource", AudioSource);
|
233
236
|
TypeStore.add("AudioTrackHandler", AudioTrackHandler);
|
@@ -330,6 +333,7 @@
|
|
330
333
|
TypeStore.add("PixelationEffect", PixelationEffect);
|
331
334
|
TypeStore.add("PlayableDirector", PlayableDirector);
|
332
335
|
TypeStore.add("PlayAnimationOnClick", PlayAnimationOnClick);
|
336
|
+
TypeStore.add("PlayAudioOnClick", PlayAudioOnClick);
|
333
337
|
TypeStore.add("PlayerColor", PlayerColor);
|
334
338
|
TypeStore.add("PlayerState", PlayerState);
|
335
339
|
TypeStore.add("PlayerSync", PlayerSync);
|
@@ -26,6 +26,12 @@
|
|
26
26
|
|
27
27
|
isRoot() { return this.Root?.gameObject === this.gameObject; }
|
28
28
|
|
29
|
+
get canvas() {
|
30
|
+
const cv = this.Root as any as ICanvas;
|
31
|
+
if (cv?.isCanvas) return cv;
|
32
|
+
return null;
|
33
|
+
}
|
34
|
+
|
29
35
|
markDirty() {
|
30
36
|
EventSystem.markUIDirty(this.context);
|
31
37
|
}
|
@@ -50,10 +56,9 @@
|
|
50
56
|
return this._root;
|
51
57
|
}
|
52
58
|
|
59
|
+
// TODO: rename to canvas
|
53
60
|
protected get Canvas() {
|
54
|
-
|
55
|
-
if (cv?.isCanvas) return cv;
|
56
|
-
return null;
|
61
|
+
return this.canvas;
|
57
62
|
}
|
58
63
|
|
59
64
|
// private _intermediate?: Object3D;
|
@@ -8,6 +8,7 @@
|
|
8
8
|
createBehaviours?(ext: BehaviorExtension, model: USDObject, context: IContext): void;
|
9
9
|
beforeCreateDocument?(ext: BehaviorExtension, context: IContext): void;
|
10
10
|
afterCreateDocument?(ext: BehaviorExtension, context: IContext): void;
|
11
|
+
afterSerialize?(ext: BehaviorExtension, context: IContext): void;
|
11
12
|
}
|
12
13
|
|
13
14
|
export class BehaviorExtension implements IUSDExporterExtension {
|
@@ -23,6 +24,7 @@
|
|
23
24
|
}
|
24
25
|
|
25
26
|
behaviourComponents: Array<UsdzBehaviour> = [];
|
27
|
+
private behaviourComponentsCopy: Array<UsdzBehaviour> = [];
|
26
28
|
|
27
29
|
|
28
30
|
onBeforeBuildDocument(context) {
|
@@ -42,7 +44,6 @@
|
|
42
44
|
}
|
43
45
|
|
44
46
|
onExportObject(_object, model: USDObject, context) {
|
45
|
-
|
46
47
|
for (const beh of this.behaviourComponents) {
|
47
48
|
beh.createBehaviours?.call(beh, this, model, context);
|
48
49
|
}
|
@@ -53,6 +54,7 @@
|
|
53
54
|
if (typeof beh.afterCreateDocument === "function")
|
54
55
|
beh.afterCreateDocument(this, context);
|
55
56
|
}
|
57
|
+
this.behaviourComponentsCopy = this.behaviourComponents.slice();
|
56
58
|
this.behaviourComponents.length = 0;
|
57
59
|
}
|
58
60
|
|
@@ -70,6 +72,26 @@
|
|
70
72
|
}
|
71
73
|
}
|
72
74
|
|
75
|
+
async onAfterSerialize(context) {
|
76
|
+
console.log("onAfterSerialize", this.behaviourComponentsCopy)
|
77
|
+
for (const beh of this.behaviourComponentsCopy) {
|
78
|
+
|
79
|
+
console.log("behaviour", beh)
|
80
|
+
if (typeof beh.afterSerialize === "function") {
|
81
|
+
|
82
|
+
console.log("beh has afterSerialize", beh)
|
83
|
+
|
84
|
+
const isAsync = beh.afterSerialize.constructor.name === "AsyncFunction";
|
85
|
+
|
86
|
+
if ( isAsync ) {
|
87
|
+
await beh.afterSerialize(this, context);
|
88
|
+
} else {
|
89
|
+
beh.afterSerialize(this, context);
|
90
|
+
}
|
91
|
+
}
|
92
|
+
}
|
93
|
+
}
|
94
|
+
|
73
95
|
// combine behaviours that have tap triggers on the same object
|
74
96
|
// private combineBehavioursWithSameTapActions() {
|
75
97
|
// // TODO: if behaviours have different settings (e.g. one is exclusive and another one is not) this wont work - we need more logic for that
|
@@ -6,11 +6,12 @@
|
|
6
6
|
import { RegisteredAnimationInfo, UsdzAnimation } from "../Animation";
|
7
7
|
import { getWorldPosition, getWorldQuaternion, getWorldScale, setWorldPosition, setWorldQuaternion, setWorldScale } from "../../../../../engine/engine_three_utils";
|
8
8
|
|
9
|
-
import { Object3D, Material, Vector3, Quaternion
|
9
|
+
import { Object3D, Material, Vector3, Quaternion } from "three";
|
10
10
|
import { USDDocument, USDObject } from "../../ThreeUSDZExporter";
|
11
11
|
|
12
12
|
import { BehaviorExtension, UsdzBehaviour } from "./Behaviour";
|
13
|
-
import { ActionBuilder, ActionModel, BehaviorModel, IBehaviorElement, MotionType, Space, TriggerBuilder } from "./BehavioursBuilder";
|
13
|
+
import { ActionBuilder, ActionModel, AuralMode, BehaviorModel, IBehaviorElement, MotionType, PlayAction, Space, TriggerBuilder } from "./BehavioursBuilder";
|
14
|
+
import { AudioSource } from "../../../../AudioSource";
|
14
15
|
|
15
16
|
export class ChangeTransformOnClick extends Behaviour implements IPointerClickHandler, UsdzBehaviour {
|
16
17
|
|
@@ -407,6 +408,82 @@
|
|
407
408
|
afterCreateDocument(_ext, _context) { }
|
408
409
|
}
|
409
410
|
|
411
|
+
export class PlayAudioOnClick extends Behaviour implements IPointerClickHandler, UsdzBehaviour {
|
412
|
+
|
413
|
+
@serializable(AudioSource)
|
414
|
+
target?: AudioSource;
|
415
|
+
|
416
|
+
@serializable(URL)
|
417
|
+
clip: string = "";
|
418
|
+
|
419
|
+
@serializable()
|
420
|
+
toggleOnClick: boolean = false;
|
421
|
+
|
422
|
+
onPointerClick() {
|
423
|
+
if (!this.target && !this.clip) return;
|
424
|
+
|
425
|
+
if (!this.target) {
|
426
|
+
|
427
|
+
const newAudioSource = this.gameObject.addNewComponent(AudioSource);
|
428
|
+
if (newAudioSource) {
|
429
|
+
newAudioSource.spatialBlend = 1;
|
430
|
+
newAudioSource.volume = 1;
|
431
|
+
newAudioSource.loop = false;
|
432
|
+
|
433
|
+
this.target = newAudioSource;
|
434
|
+
}
|
435
|
+
}
|
436
|
+
|
437
|
+
if (this.target) {
|
438
|
+
|
439
|
+
if (this.target.isPlaying && this.toggleOnClick) {
|
440
|
+
this.target.stop();
|
441
|
+
}
|
442
|
+
else {
|
443
|
+
if (!this.toggleOnClick && this.target.isPlaying) {
|
444
|
+
this.target.stop();
|
445
|
+
}
|
446
|
+
if (this.clip) this.target.play(this.clip);
|
447
|
+
else this.target.play();
|
448
|
+
}
|
449
|
+
}
|
450
|
+
}
|
451
|
+
|
452
|
+
createBehaviours(ext, model, _context) {
|
453
|
+
if (!this.target && !this.clip) return;
|
454
|
+
if (model.uuid === this.gameObject.uuid) {
|
455
|
+
|
456
|
+
const clipUrl = this.clip ? this.clip : this.target ? this.target.clip : undefined;
|
457
|
+
if (!clipUrl) return;
|
458
|
+
|
459
|
+
const playbackTarget = this.target ? this.target.gameObject : this.gameObject;
|
460
|
+
const clipName = clipUrl.split("/").pop();
|
461
|
+
const volume = this.target ? this.target.volume : 1;
|
462
|
+
const auralMode = this.target && this.target.spatialBlend == 0 ? AuralMode.NonSpatial : AuralMode.Spatial;
|
463
|
+
const playClip = new BehaviorModel("playAudio " + this.name,
|
464
|
+
TriggerBuilder.tapTrigger(this.gameObject),
|
465
|
+
ActionBuilder.playAudioAction(playbackTarget, "audio/" + clipName, PlayAction.Play, volume, auralMode),
|
466
|
+
);
|
467
|
+
ext.addBehavior(playClip);
|
468
|
+
}
|
469
|
+
}
|
470
|
+
|
471
|
+
async afterSerialize(_ext, context) {
|
472
|
+
if (!this.target && !this.clip) return;
|
473
|
+
const clipUrl = this.clip ? this.clip : this.target ? this.target.clip : undefined;
|
474
|
+
if (!clipUrl) return;
|
475
|
+
const clipName = clipUrl.split("/").pop();
|
476
|
+
|
477
|
+
const audio = await fetch(this.clip);
|
478
|
+
const audioBlob = await audio.blob();
|
479
|
+
const arrayBuffer = await audioBlob.arrayBuffer();
|
480
|
+
|
481
|
+
const audioData: Uint8Array = new Uint8Array(arrayBuffer)
|
482
|
+
|
483
|
+
context.files["audio/" + clipName] = audioData;
|
484
|
+
}
|
485
|
+
}
|
486
|
+
|
410
487
|
export class PlayAnimationOnClick extends Behaviour implements IPointerClickHandler, UsdzBehaviour, UsdzAnimation {
|
411
488
|
|
412
489
|
@serializable(Object3D)
|
@@ -237,6 +237,13 @@
|
|
237
237
|
Absolute = "absolute"
|
238
238
|
};
|
239
239
|
|
240
|
+
// https://developer.apple.com/documentation/arkit/usdz_schemas_for_ar/actions_and_triggers/preliminary_action/multipleperformoperation
|
241
|
+
export enum MultiplePerformOperation {
|
242
|
+
Allow = "allow",
|
243
|
+
Ignore = "ignore",
|
244
|
+
Stop = "stop",
|
245
|
+
}
|
246
|
+
|
240
247
|
export class ActionModel implements IBehaviorElement {
|
241
248
|
|
242
249
|
private static global_id: number = 0;
|
@@ -257,6 +264,10 @@
|
|
257
264
|
reversed?: boolean;
|
258
265
|
pingPong?: boolean;
|
259
266
|
xFormTarget?: Target | string;
|
267
|
+
audio?: string;
|
268
|
+
gain?: number;
|
269
|
+
auralMode?: string;
|
270
|
+
multiplePerformOperation?: string;
|
260
271
|
|
261
272
|
clone(): ActionModel {
|
262
273
|
const copy = new ActionModel();
|
@@ -319,6 +330,18 @@
|
|
319
330
|
this.xFormTarget = resolve(this.xFormTarget, document);
|
320
331
|
writer.appendLine(`rel xformTarget = ${this.xFormTarget}`)
|
321
332
|
}
|
333
|
+
if (typeof this.audio === "string") {
|
334
|
+
writer.appendLine(`asset audio = @${this.audio}@`);
|
335
|
+
}
|
336
|
+
if (typeof this.gain ==="number") {
|
337
|
+
writer.appendLine(`double gain = ${this.gain}`);
|
338
|
+
}
|
339
|
+
if (typeof this.auralMode === "string") {
|
340
|
+
writer.appendLine(`token auralMode = "${this.auralMode}"`);
|
341
|
+
}
|
342
|
+
if (typeof this.multiplePerformOperation === "string") {
|
343
|
+
writer.appendLine(`token multiplePerformOperation = "${this.multiplePerformOperation}"`);
|
344
|
+
}
|
322
345
|
writer.closeBlock();
|
323
346
|
}
|
324
347
|
}
|
@@ -355,6 +378,18 @@
|
|
355
378
|
}
|
356
379
|
}
|
357
380
|
|
381
|
+
export enum PlayAction {
|
382
|
+
Play = "play",
|
383
|
+
Pause = "pause",
|
384
|
+
Stop = "stop",
|
385
|
+
}
|
386
|
+
|
387
|
+
export enum AuralMode {
|
388
|
+
Spatial = "spatial",
|
389
|
+
NonSpatial = "nonSpatial",
|
390
|
+
Ambient = "ambient",
|
391
|
+
}
|
392
|
+
|
358
393
|
export class ActionBuilder {
|
359
394
|
|
360
395
|
static sequence(...params: IBehaviorElement[]) {
|
@@ -454,6 +489,17 @@
|
|
454
489
|
return act;
|
455
490
|
}
|
456
491
|
|
492
|
+
static playAudioAction(targets: Target, audio: string, type: PlayAction = PlayAction.Play, gain: number = 1, auralMode: AuralMode = AuralMode.Spatial) {
|
493
|
+
const act = new ActionModel(targets);
|
494
|
+
act.tokenId = "Audio";
|
495
|
+
act.type = type;
|
496
|
+
act.audio = audio;
|
497
|
+
act.gain = gain;
|
498
|
+
act.auralMode = auralMode;
|
499
|
+
act.multiplePerformOperation = MultiplePerformOperation.Allow;
|
500
|
+
return act;
|
501
|
+
}
|
502
|
+
|
457
503
|
}
|
458
504
|
|
459
505
|
export { Vec3 as USDVec3 }
|
@@ -193,7 +193,7 @@
|
|
193
193
|
this.previousParent = this.gameObject.parent;
|
194
194
|
// console.log(this.previousParent?.name + "/" + this.gameObject.name);
|
195
195
|
|
196
|
-
if (this.renderOnTop) {
|
196
|
+
if (this.renderOnTop || this.screenspace) {
|
197
197
|
// This is just a test but in reality it should be combined with all world canvases with render on top in one render pass
|
198
198
|
this.gameObject.removeFromParent();
|
199
199
|
}
|
@@ -195,9 +195,6 @@
|
|
195
195
|
* @param instance component to move to the GO
|
196
196
|
*/
|
197
197
|
public static moveComponent(go: IGameObject, instance: Component): void {
|
198
|
-
if (instance.gameObject == null) {
|
199
|
-
throw new Error("Did you mean to create a new component? Use addNewComponent");
|
200
|
-
}
|
201
198
|
moveComponentInstance(go, instance as any);
|
202
199
|
}
|
203
200
|
|
@@ -277,6 +274,7 @@
|
|
277
274
|
// these are implemented via threejs object extensions
|
278
275
|
abstract activeSelf: boolean;
|
279
276
|
abstract addNewComponent<T>(type: Constructor<T>): T | null;
|
277
|
+
// TODO: add method for addExisting component
|
280
278
|
abstract removeComponent(comp: Component): Component;
|
281
279
|
abstract getOrAddComponent<T>(typeName: Constructor<T> | null): T;
|
282
280
|
abstract getComponent<T>(type: Constructor<T>): T | null;
|
@@ -12,6 +12,7 @@
|
|
12
12
|
export { AnimatorController } from "../AnimatorController";
|
13
13
|
export { Antialiasing } from "../postprocessing/Effects/Antialiasing";
|
14
14
|
export { AttachedObject } from "../webxr/WebXRController";
|
15
|
+
export { AudioExtension } from "../export/usdz/extensions/behavior/AudioExtension";
|
15
16
|
export { AudioListener } from "../AudioListener";
|
16
17
|
export { AudioSource } from "../AudioSource";
|
17
18
|
export { AudioTrackHandler } from "../timeline/TimelineTracks";
|
@@ -114,6 +115,7 @@
|
|
114
115
|
export { PixelationEffect } from "../postprocessing/Effects/Pixelation";
|
115
116
|
export { PlayableDirector } from "../timeline/PlayableDirector";
|
116
117
|
export { PlayAnimationOnClick } from "../export/usdz/extensions/behavior/BehaviourComponents";
|
118
|
+
export { PlayAudioOnClick } from "../export/usdz/extensions/behavior/BehaviourComponents";
|
117
119
|
export { PlayerColor } from "../PlayerColor";
|
118
120
|
export { PointerEventData } from "../ui/PointerEvents";
|
119
121
|
export { PostProcessingHandler } from "../postprocessing/PostProcessingHandler";
|
@@ -107,7 +107,8 @@
|
|
107
107
|
function showMessage(type: LogType, element: HTMLElement, msg: string) {
|
108
108
|
const container = getLogsContainer(element);
|
109
109
|
if (container.childElementCount >= 20) {
|
110
|
-
|
110
|
+
const last = container.lastElementChild;
|
111
|
+
returnMessageContainer(last as HTMLElement);
|
111
112
|
}
|
112
113
|
// truncate long messages before they go into the cache/set
|
113
114
|
if(msg.length > 300) msg = msg.substring(0, 300) + "...";
|
@@ -160,7 +160,8 @@
|
|
160
160
|
|
161
161
|
export function getComponents<T>(obj: Object3D, componentType: Constructor<T>, arr?: T[] | null): T[] {
|
162
162
|
if (!arr) arr = [];
|
163
|
-
|
163
|
+
onGetComponent(obj, componentType, arr);
|
164
|
+
return arr;
|
164
165
|
}
|
165
166
|
|
166
167
|
export function getComponentInChildren<T>(obj: Object3D, componentType: Constructor<T>, includeInactive?: boolean) {
|
@@ -186,7 +186,7 @@
|
|
186
186
|
if (loadingStyle === "light")
|
187
187
|
this._loadingElement.style.backgroundColor = "#ddd";
|
188
188
|
else
|
189
|
-
this._loadingElement.style.backgroundColor = "#
|
189
|
+
this._loadingElement.style.backgroundColor = "#222";
|
190
190
|
this._loadingElement.style.display = "flex";
|
191
191
|
this._loadingElement.style.alignItems = "center";
|
192
192
|
this._loadingElement.style.justifyContent = "center";
|
@@ -289,7 +289,7 @@
|
|
289
289
|
messageContainer.style.fontSize = ".8em";
|
290
290
|
messageContainer.style.paddingTop = ".5em";
|
291
291
|
messageContainer.style.fontWeight = "200";
|
292
|
-
messageContainer.style.fontFamily = "Roboto, sans-serif";
|
292
|
+
messageContainer.style.fontFamily = "Roboto, sans-serif, Arial";
|
293
293
|
// messageContainer.style.border = "1px solid rgba(255,255,255,.1)";
|
294
294
|
messageContainer.style.justifyContent = "center";
|
295
295
|
this._loadingElement.appendChild(messageContainer);
|
@@ -113,6 +113,9 @@
|
|
113
113
|
}
|
114
114
|
|
115
115
|
function internalDestroy(instance: Object3D | Component, recursive: boolean = true, dispose: boolean = false, isRoot: boolean = true) {
|
116
|
+
if (instance === null || instance === undefined)
|
117
|
+
return;
|
118
|
+
|
116
119
|
const comp = instance as Component;
|
117
120
|
if (comp.isComponent) {
|
118
121
|
comp.__internalDisable();
|
@@ -611,9 +611,12 @@
|
|
611
611
|
|
612
612
|
const lf = this._pointerPositionsLastFrame[evt.button];
|
613
613
|
lf.copy(this._pointerPositions[evt.button]);
|
614
|
+
// accumulate delta (it's reset in end of frame), if we just write it here it's not correct when the browser console is open
|
615
|
+
const delta = this._pointerPositionsDelta[evt.button];
|
614
616
|
const dx = evt.clientX - lf.x;
|
615
617
|
const dy = evt.clientY - lf.y;
|
616
|
-
|
618
|
+
delta.x += dx;
|
619
|
+
delta.y += dy;
|
617
620
|
|
618
621
|
this._pointerPositions[evt.button].x = evt.clientX;
|
619
622
|
this._pointerPositions[evt.button].y = evt.clientY;
|
@@ -207,7 +207,8 @@
|
|
207
207
|
private async internalInitialization() {
|
208
208
|
// NEEDLE_PHYSICS_INIT_START
|
209
209
|
// use .env file with VITE_NEEDLE_USE_RAPIER=false to treeshape rapier
|
210
|
-
|
210
|
+
// @ts-ignore
|
211
|
+
if ("env" in import.meta && import.meta.env.VITE_NEEDLE_USE_RAPIER === "false") {
|
211
212
|
return false;
|
212
213
|
}
|
213
214
|
// Can be transformed during build time to disable rapier
|
@@ -434,7 +434,14 @@
|
|
434
434
|
if (targetMember !== undefined) continue;
|
435
435
|
// resolve serialized primitive types
|
436
436
|
if (isPrimitiveType(data[key]) && !isPrimitiveType(member)) {
|
437
|
-
|
437
|
+
|
438
|
+
const prop = tryFindPropertyDescriptor(member, key);
|
439
|
+
if (!prop?.writable === false || (prop && prop.set === undefined)) {
|
440
|
+
if (debug)
|
441
|
+
console.warn("Property is not writable \"" + key + "\"", member, prop, data[key], member[key]);
|
442
|
+
continue;
|
443
|
+
}
|
444
|
+
// console.log("ASSIGN", key, member, member[key], targetMember, data[key], prop);
|
438
445
|
member[key] = data[key];
|
439
446
|
}
|
440
447
|
}
|
@@ -442,6 +449,15 @@
|
|
442
449
|
}
|
443
450
|
}
|
444
451
|
|
452
|
+
function tryFindPropertyDescriptor(obj: object, key: string) : PropertyDescriptor | undefined {
|
453
|
+
while(obj){
|
454
|
+
const desc = Object.getOwnPropertyDescriptor(obj, key);
|
455
|
+
if(desc) return desc;
|
456
|
+
obj = Object.getPrototypeOf(obj);
|
457
|
+
}
|
458
|
+
return undefined;
|
459
|
+
}
|
460
|
+
|
445
461
|
function isPrimitiveType(val): boolean {
|
446
462
|
switch (typeof val) {
|
447
463
|
case "number":
|
@@ -255,7 +255,7 @@
|
|
255
255
|
if (!hits) return;
|
256
256
|
this.lastPointerEvent = args;
|
257
257
|
|
258
|
-
const evt
|
258
|
+
const evt: AfterHandleInputEvent = {
|
259
259
|
sender: this,
|
260
260
|
args: args,
|
261
261
|
hasActiveUI: this.currentActiveMeshUIComponents.length > 0,
|
@@ -433,6 +433,10 @@
|
|
433
433
|
}
|
434
434
|
}
|
435
435
|
|
436
|
+
if (comp.onPointerMove) {
|
437
|
+
comp.onPointerMove(args);
|
438
|
+
}
|
439
|
+
|
436
440
|
if (args.isDown) {
|
437
441
|
if (comp.onPointerDown && !this.raisedPointerDownEvents.includes(comp)) {
|
438
442
|
comp.onPointerDown(args);
|
@@ -1,11 +1,12 @@
|
|
1
|
-
import { USDObject } from "./ThreeUSDZExporter";
|
1
|
+
import { USDObject, USDZExporterContext } from "./ThreeUSDZExporter";
|
2
|
+
import { Object3D } from "three";
|
2
3
|
|
3
4
|
export interface IUSDExporterExtension {
|
4
5
|
|
5
6
|
get extensionName(): string;
|
6
7
|
onBeforeBuildDocument?(context);
|
7
8
|
onAfterBuildDocument?(context);
|
8
|
-
onExportObject?(object, model : USDObject, context);
|
9
|
+
onExportObject?(object: Object3D, model : USDObject, context: USDZExporterContext);
|
9
10
|
onAfterSerialize?(context);
|
10
11
|
onAfterHierarchy?(context, writer : any);
|
11
12
|
}
|
@@ -5,13 +5,28 @@
|
|
5
5
|
|
6
6
|
class Sprite {
|
7
7
|
@serializable(Texture)
|
8
|
-
texture
|
8
|
+
texture: Texture | null = null;
|
9
9
|
|
10
10
|
rect?: { width: number, height: number };
|
11
11
|
}
|
12
12
|
|
13
13
|
export class Image extends MaskableGraphic {
|
14
14
|
|
15
|
+
set image(img: Texture | null) {
|
16
|
+
if (this.sprite)
|
17
|
+
this.sprite.texture = img;
|
18
|
+
else {
|
19
|
+
this.sprite = new Sprite();
|
20
|
+
this.sprite.texture = img;
|
21
|
+
}
|
22
|
+
this.onAfterCreated();
|
23
|
+
}
|
24
|
+
get image(): Texture | null {
|
25
|
+
if (this.sprite)
|
26
|
+
return this.sprite.texture;
|
27
|
+
return null;
|
28
|
+
}
|
29
|
+
|
15
30
|
@serializable(Sprite)
|
16
31
|
get sprite(): Sprite | undefined {
|
17
32
|
return this._sprite;
|
@@ -1,7 +1,7 @@
|
|
1
1
|
import { Behaviour } from "../Component";
|
2
2
|
import { IComponent } from "../../engine/engine_types";
|
3
3
|
|
4
|
-
export interface ICanvas {
|
4
|
+
export interface ICanvas extends IComponent {
|
5
5
|
get isCanvas(): boolean;
|
6
6
|
get screenspace(): boolean;
|
7
7
|
registerTransform(rt: IRectTransform);
|
@@ -250,6 +250,8 @@
|
|
250
250
|
const rt = GameObject.getComponent(ch, RectTransform);
|
251
251
|
if (rt?.activeAndEnabled) {
|
252
252
|
rt.pivot?.set(.5, .5);
|
253
|
+
rt.anchorMin.set(0, 1);
|
254
|
+
rt.anchorMax.set(0, 1);
|
253
255
|
// Horizontal padding
|
254
256
|
const x = totalWidth * .5 + leftOffset * .5;
|
255
257
|
if (rt.anchoredPosition.x !== x)
|
@@ -48,8 +48,9 @@
|
|
48
48
|
let settings: SceneLightSettings | undefined = undefined;
|
49
49
|
// If the result scene has only one child we add the LightingSettingsComponent to that child
|
50
50
|
if (_result.scene.children.length === 1) {
|
51
|
+
const obj = _result.scene.children[0];
|
51
52
|
// add a component to the root of the scene
|
52
|
-
settings = GameObject.addNewComponent(
|
53
|
+
settings = GameObject.addNewComponent(obj, SceneLightSettings, false);
|
53
54
|
}
|
54
55
|
// if the scene already has multiple children we add it as a new object
|
55
56
|
else {
|
@@ -117,6 +118,15 @@
|
|
117
118
|
}
|
118
119
|
});
|
119
120
|
}
|
121
|
+
|
122
|
+
// make sure the component is in the end of the list
|
123
|
+
// (e.g. if we have an animation on the first component from an instance and add the scenelightingcomponent the animation binding will break)
|
124
|
+
const comps = this.gameObject.userData?.components;
|
125
|
+
if (comps) {
|
126
|
+
const index = comps.indexOf(this);
|
127
|
+
comps.splice(index, 1);
|
128
|
+
comps.push(this);
|
129
|
+
}
|
120
130
|
}
|
121
131
|
|
122
132
|
onDestroy(): void {
|
@@ -11,12 +11,22 @@
|
|
11
11
|
// TODO: should we make this a getter and return the input used state instead? -> this.context.getPointerUsed(this.pointerId);
|
12
12
|
used: boolean = false;
|
13
13
|
|
14
|
-
|
14
|
+
use() {
|
15
15
|
this.used = true;
|
16
16
|
if (this.pointerId !== undefined)
|
17
17
|
this.input.setPointerUsed(this.pointerId);
|
18
18
|
}
|
19
19
|
|
20
|
+
stopPropagation() {
|
21
|
+
this.event?.stopImmediatePropagation();
|
22
|
+
}
|
23
|
+
|
24
|
+
/**@deprecated use use() */
|
25
|
+
Use() {
|
26
|
+
this.use();
|
27
|
+
}
|
28
|
+
|
29
|
+
/**@deprecated use stopPropagation() */
|
20
30
|
StopPropagation() {
|
21
31
|
this.event?.stopImmediatePropagation();
|
22
32
|
}
|
@@ -52,6 +62,10 @@
|
|
52
62
|
onPointerEnter?(args: PointerEventData);
|
53
63
|
}
|
54
64
|
|
65
|
+
export interface IPointerMoveHandler {
|
66
|
+
onPointerMove?(args: PointerEventData);
|
67
|
+
}
|
68
|
+
|
55
69
|
export interface IPointerExitHandler {
|
56
70
|
onPointerExit?(args: PointerEventData);
|
57
71
|
}
|
@@ -61,4 +75,4 @@
|
|
61
75
|
}
|
62
76
|
|
63
77
|
export interface IPointerEventHandler extends IPointerDownHandler,
|
64
|
-
IPointerUpHandler, IPointerEnterHandler, IPointerExitHandler, IPointerClickHandler { }
|
78
|
+
IPointerUpHandler, IPointerEnterHandler, IPointerMoveHandler, IPointerExitHandler, IPointerClickHandler { }
|
@@ -20,7 +20,12 @@
|
|
20
20
|
};
|
21
21
|
|
22
22
|
static isInteractable(obj: THREE.Object3D, out?: { canvasGroup?: ICanvasGroup, graphic?: IGraphic }): boolean {
|
23
|
-
|
23
|
+
// reset state
|
24
|
+
if (out) {
|
25
|
+
out.canvasGroup = undefined;
|
26
|
+
out.graphic = undefined;
|
27
|
+
}
|
28
|
+
if (obj === null || obj === undefined || !obj.visible) return false;
|
24
29
|
|
25
30
|
obj = this.getObject(obj);
|
26
31
|
|
@@ -31,6 +31,9 @@
|
|
31
31
|
|
32
32
|
export class RectTransform extends BaseUIComponent implements IRectTransform, IRectTransformChangedReceiver {
|
33
33
|
|
34
|
+
get parent() {
|
35
|
+
return this._parentRectTransform;
|
36
|
+
}
|
34
37
|
offset: number = .01;
|
35
38
|
|
36
39
|
// @serializable(Object3D)
|
@@ -51,24 +54,21 @@
|
|
51
54
|
this._anchoredPosition = value;
|
52
55
|
}
|
53
56
|
|
54
|
-
@serializable(Rect)
|
55
|
-
private rect?: Rect; // TODO: should we use the rect or sizeDelta?
|
56
|
-
|
57
57
|
@serializable(Vector2)
|
58
|
-
sizeDelta
|
58
|
+
sizeDelta: Vector2 = new Vector2(100, 100);
|
59
59
|
|
60
60
|
@serializable(Vector2)
|
61
|
-
pivot
|
61
|
+
pivot: Vector2 = new Vector2(.5, .5);
|
62
62
|
|
63
63
|
@serializable(Vector2)
|
64
|
-
anchorMin
|
64
|
+
anchorMin: Vector2 = new Vector2(0, 0);
|
65
65
|
@serializable(Vector2)
|
66
|
-
anchorMax
|
66
|
+
anchorMax: Vector2 = new Vector2(1, 1);
|
67
67
|
|
68
68
|
@serializable(Vector2)
|
69
|
-
offsetMin
|
69
|
+
offsetMin: Vector2 = new Vector2(0, 0);
|
70
70
|
@serializable(Vector2)
|
71
|
-
offsetMax
|
71
|
+
offsetMax: Vector2 = new Vector2(0, 0);
|
72
72
|
|
73
73
|
get width() {
|
74
74
|
if (this.anchorMin.x !== this.anchorMax.x) {
|
@@ -301,8 +301,8 @@
|
|
301
301
|
// })
|
302
302
|
|
303
303
|
const opts = {
|
304
|
-
width: this.
|
305
|
-
height: this.
|
304
|
+
width: this.sizeDelta!.x,
|
305
|
+
height: this.sizeDelta!.y,// * this.context.mainCameraComponent!.aspect,
|
306
306
|
offset: this.offset,
|
307
307
|
backgroundOpacity: 0,
|
308
308
|
borderWidth: 0, // if we dont specify width here a border will automatically propagated to child blocks
|
@@ -40,8 +40,6 @@
|
|
40
40
|
|
41
41
|
export class Text extends Graphic {
|
42
42
|
|
43
|
-
@serializable(Canvas)
|
44
|
-
canvas?: Canvas;
|
45
43
|
@serializable()
|
46
44
|
alignment: TextAnchor = TextAnchor.UpperLeft;
|
47
45
|
@serializable()
|
@@ -334,7 +332,7 @@
|
|
334
332
|
private * renderOnTopCoroutine() {
|
335
333
|
if (!this.canvas) return;
|
336
334
|
const updatedRendering: boolean[] = [];
|
337
|
-
const canvas = this.canvas;
|
335
|
+
const canvas = this.canvas as Canvas;
|
338
336
|
const settings = {
|
339
337
|
renderOnTop: canvas.renderOnTop,
|
340
338
|
depthWrite: canvas.depthWrite,
|
@@ -1,3 +1,5 @@
|
|
1
|
+
import { Renderer } from '../../Renderer';
|
2
|
+
import { GameObject } from '../../Component';
|
1
3
|
import {
|
2
4
|
PlaneGeometry,
|
3
5
|
Texture,
|
@@ -19,6 +21,9 @@
|
|
19
21
|
MeshStandardMaterial,
|
20
22
|
sRGBEncoding,
|
21
23
|
MeshPhysicalMaterial,
|
24
|
+
Object3D,
|
25
|
+
MeshBasicMaterial,
|
26
|
+
SkinnedMesh,
|
22
27
|
} from 'three';
|
23
28
|
import * as fflate from 'three/examples/jsm/libs/fflate.module.js';
|
24
29
|
|
@@ -436,7 +441,7 @@
|
|
436
441
|
|
437
442
|
parseDocument( context );
|
438
443
|
|
439
|
-
invokeAll( context, 'onAfterSerialize' );
|
444
|
+
await invokeAll( context, 'onAfterSerialize' );
|
440
445
|
|
441
446
|
context.output += buildMaterials( materials, textures );
|
442
447
|
|
@@ -509,21 +514,33 @@
|
|
509
514
|
|
510
515
|
}
|
511
516
|
|
512
|
-
function traverseVisible( object, parentModel, context ) {
|
517
|
+
function traverseVisible( object: Object3D, parentModel: USDObject, context: USDZExporterContext ) {
|
513
518
|
|
514
519
|
if ( ! object.visible ) return;
|
515
|
-
|
520
|
+
|
516
521
|
let model: USDObject | undefined = undefined;
|
517
|
-
|
518
|
-
|
522
|
+
let geometry: BufferGeometry | undefined = undefined;
|
523
|
+
let material: Material | Material[] | undefined = undefined;
|
524
|
+
|
525
|
+
if (object instanceof Mesh) {
|
526
|
+
geometry = object.geometry;
|
527
|
+
material = object.material;
|
528
|
+
}
|
519
529
|
|
530
|
+
// TODO what should be do with disabled renderers?
|
531
|
+
// Here we just assume they're off, and don't export them
|
532
|
+
const renderer = GameObject.getComponent( object, Renderer )
|
533
|
+
if (renderer && !renderer.enabled) {
|
534
|
+
geometry = undefined;
|
535
|
+
material = undefined;
|
536
|
+
}
|
520
537
|
|
521
|
-
if ( object
|
538
|
+
if ( object instanceof Mesh && material && (material instanceof MeshStandardMaterial || material instanceof MeshBasicMaterial) && ! (object instanceof SkinnedMesh )) {
|
522
539
|
|
523
540
|
const name = getObjectId( object );
|
524
541
|
model = new USDObject( object.uuid, name, object.matrix, geometry, material );
|
525
542
|
|
526
|
-
} else if ( object
|
543
|
+
} else if ( object instanceof Camera ) {
|
527
544
|
|
528
545
|
const name = getObjectId( object );
|
529
546
|
model = new USDObject( object.uuid, name, object.matrix, undefined, undefined, object );
|
@@ -577,7 +594,7 @@
|
|
577
594
|
|
578
595
|
}
|
579
596
|
|
580
|
-
function parseDocument( context: USDZExporterContext ) {
|
597
|
+
async function parseDocument( context: USDZExporterContext ) {
|
581
598
|
|
582
599
|
for ( const child of context.document.children ) {
|
583
600
|
|
@@ -666,15 +683,28 @@
|
|
666
683
|
|
667
684
|
}
|
668
685
|
|
669
|
-
function invokeAll( context: USDZExporterContext, name: string, writer: USDWriter | null = null ) {
|
686
|
+
async function invokeAll( context: USDZExporterContext, name: string, writer: USDWriter | null = null ) {
|
670
687
|
|
671
688
|
if ( context.extensions ) {
|
672
689
|
|
673
690
|
for ( const ext of context.extensions ) {
|
674
691
|
|
675
|
-
if (
|
676
|
-
ext[ name ]( context, writer );
|
692
|
+
if ( !ext ) continue;
|
677
693
|
|
694
|
+
if ( typeof ext[ name ] === 'function' ) {
|
695
|
+
|
696
|
+
const method = ext[ name ];
|
697
|
+
|
698
|
+
const isAsync = method.constructor.name === "AsyncFunction";
|
699
|
+
|
700
|
+
if ( isAsync ) {
|
701
|
+
await method.call( ext, context, writer );
|
702
|
+
} else {
|
703
|
+
method.call( ext, context, writer );
|
704
|
+
}
|
705
|
+
|
706
|
+
}
|
707
|
+
|
678
708
|
}
|
679
709
|
|
680
710
|
}
|
@@ -841,6 +871,14 @@
|
|
841
871
|
const material = model.material;
|
842
872
|
const camera = model.camera;
|
843
873
|
const name = model.name;
|
874
|
+
|
875
|
+
// postprocess node
|
876
|
+
if ( model.onBeforeSerialize ) {
|
877
|
+
|
878
|
+
model.onBeforeSerialize( writer, context );
|
879
|
+
|
880
|
+
}
|
881
|
+
|
844
882
|
const transform = buildMatrix( matrix );
|
845
883
|
|
846
884
|
if ( matrix.determinant() < 0 ) {
|
@@ -849,11 +887,12 @@
|
|
849
887
|
|
850
888
|
}
|
851
889
|
|
852
|
-
if ( geometry )
|
890
|
+
if ( geometry ) {
|
853
891
|
writer.beginBlock( `def Xform "${name}" (
|
854
|
-
|
855
|
-
|
892
|
+
prepend references = @./geometries/Geometry_${geometry.id}.usd@</Geometry>
|
893
|
+
prepend apiSchemas = ["MaterialBindingAPI"]
|
856
894
|
)` );
|
895
|
+
}
|
857
896
|
else if ( camera )
|
858
897
|
writer.beginBlock( `def Camera "${name}"` );
|
859
898
|
else
|
@@ -14,6 +14,7 @@
|
|
14
14
|
import { WebARSessionRoot } from "../../webxr/WebARSessionRoot";
|
15
15
|
import { hasProLicense } from "../../../engine/engine_license";
|
16
16
|
import { BehaviorExtension } from "./extensions/behavior/Behaviour";
|
17
|
+
import { AudioExtension } from "./extensions/behavior/AudioExtension";
|
17
18
|
|
18
19
|
const debug = getParam("debugusdz");
|
19
20
|
|
@@ -41,6 +42,9 @@
|
|
41
42
|
@serializable()
|
42
43
|
exportFileName?: string;
|
43
44
|
|
45
|
+
@serializable(URL)
|
46
|
+
customUsdzFile?: string;
|
47
|
+
|
44
48
|
@serializable(QuickLookOverlay)
|
45
49
|
overlay?: QuickLookOverlay;
|
46
50
|
|
@@ -91,6 +95,7 @@
|
|
91
95
|
|
92
96
|
if (this.interactive) {
|
93
97
|
this.extensions.push(new BehaviorExtension());
|
98
|
+
this.extensions.push(new AudioExtension());
|
94
99
|
}
|
95
100
|
}
|
96
101
|
|
@@ -120,6 +125,35 @@
|
|
120
125
|
}
|
121
126
|
|
122
127
|
async exportAsync() {
|
128
|
+
|
129
|
+
let name = this.exportFileName ?? this.objectToExport?.name ?? this.name;
|
130
|
+
if (!hasProLicense()) name += "-MadeWithNeedle";
|
131
|
+
name += "-" + getFormattedDate(); // seems iOS caches the file in some cases, this ensures we always have a fresh file
|
132
|
+
|
133
|
+
// ability to specify a custom USDZ file to be used instead of a dynamic one
|
134
|
+
if (this.customUsdzFile) {
|
135
|
+
|
136
|
+
// see https://developer.apple.com/documentation/arkit/adding_an_apple_pay_button_or_a_custom_action_in_ar_quick_look
|
137
|
+
const overlay = this.buildQuicklookOverlay();
|
138
|
+
if(debug) console.log(overlay);
|
139
|
+
const callToAction = overlay.callToAction ? encodeURIComponent(overlay.callToAction) : "";
|
140
|
+
const checkoutTitle = overlay.checkoutTitle ? encodeURIComponent(overlay.checkoutTitle) : "";
|
141
|
+
const checkoutSubtitle = overlay.checkoutSubtitle ? encodeURIComponent(overlay.checkoutSubtitle) : "";
|
142
|
+
this.link.href = this.customUsdzFile + `#callToAction=${callToAction}&checkoutTitle=${checkoutTitle}&checkoutSubtitle=${checkoutSubtitle}&callToActionURL=${overlay.callToActionURL}`;
|
143
|
+
|
144
|
+
console.log(this.link.href)
|
145
|
+
|
146
|
+
if (!this.lastCallback) {
|
147
|
+
this.lastCallback = this.quicklookCallback.bind(this)
|
148
|
+
this.link.addEventListener('message', this.lastCallback);
|
149
|
+
}
|
150
|
+
|
151
|
+
// open quicklook
|
152
|
+
this.link.download = name + ".usdz";
|
153
|
+
this.link.click();
|
154
|
+
return;
|
155
|
+
}
|
156
|
+
|
123
157
|
if (!this.objectToExport) return;
|
124
158
|
|
125
159
|
// make sure we apply the AR scale
|
@@ -145,10 +179,6 @@
|
|
145
179
|
const eventArgs = { self: this, exporter: exporter, extensions: extensions, object: this.objectToExport };
|
146
180
|
this.dispatchEvent(new CustomEvent("before-export", { detail: eventArgs }))
|
147
181
|
|
148
|
-
let name = this.exportFileName ?? this.objectToExport?.name ?? this.name;
|
149
|
-
if (!hasProLicense()) name += "-MadeWithNeedle";
|
150
|
-
name += "-" + getFormattedDate(); // seems iOS caches the file in some cases, this ensures we always have a fresh file
|
151
|
-
|
152
182
|
//@ts-ignore
|
153
183
|
exporter.debug = debug;
|
154
184
|
|
@@ -0,0 +1,63 @@
|
|
1
|
+
import { GameObject } from "../../../../Component";
|
2
|
+
import { IUSDExporterExtension } from "../../Extension";
|
3
|
+
import { USDObject, USDWriter, USDZExporterContext } from "../../ThreeUSDZExporter";
|
4
|
+
import { Object3D } from "three";
|
5
|
+
import { AudioSource } from "../../../../AudioSource";
|
6
|
+
|
7
|
+
export class AudioExtension implements IUSDExporterExtension {
|
8
|
+
|
9
|
+
get extensionName(): string {
|
10
|
+
return "Audio";
|
11
|
+
}
|
12
|
+
|
13
|
+
private files: string[] = [];
|
14
|
+
|
15
|
+
onExportObject?(object: Object3D, model : USDObject, _context: USDZExporterContext) {
|
16
|
+
// check if this object has an audio source, add the relevant schema in that case.
|
17
|
+
const audioSources = GameObject.getComponents(object, AudioSource);
|
18
|
+
if (audioSources.length) {
|
19
|
+
for (const audioSource of audioSources) {
|
20
|
+
|
21
|
+
// do nothing if this audio source is not set to play on awake -
|
22
|
+
// should be controlled via PlayAudioOnClick instead then.
|
23
|
+
if (!audioSource.playOnAwake)
|
24
|
+
continue;
|
25
|
+
|
26
|
+
const clipName = audioSource.clip.split("/").pop();
|
27
|
+
|
28
|
+
if (!this.files.includes(audioSource.clip)) {
|
29
|
+
this.files.push(audioSource.clip);
|
30
|
+
}
|
31
|
+
|
32
|
+
model.addEventListener('serialize', (writer: USDWriter, _context: USDZExporterContext) => {
|
33
|
+
writer.appendLine();
|
34
|
+
writer.beginBlock(`def SpatialAudio "${model.name}"`);
|
35
|
+
writer.appendLine(`uniform asset filePath = @audio/${clipName}@`);
|
36
|
+
writer.appendLine(`uniform token auralMode = "${ audioSource.spatialBlend > 0 ? "spatial" : "nonSpatial" }"`);
|
37
|
+
// theoretically we could do timeline-like audio sequencing with this.
|
38
|
+
writer.appendLine(`uniform token playbackMode = "${audioSource.loop ? "loopFromStage" : "onceFromStart" }"`);
|
39
|
+
writer.appendLine(`uniform float gain = ${audioSource.volume}`);
|
40
|
+
writer.closeBlock();
|
41
|
+
});
|
42
|
+
}
|
43
|
+
}
|
44
|
+
}
|
45
|
+
|
46
|
+
async onAfterSerialize(context: USDZExporterContext) {
|
47
|
+
console.warn("onAfterSerialize", this);
|
48
|
+
// write the files to the context.
|
49
|
+
for (const file of this.files) {
|
50
|
+
|
51
|
+
const clipName = file.split("/").pop();
|
52
|
+
|
53
|
+
// convert file (which is a path) to a blob.
|
54
|
+
const audio = await fetch(file);
|
55
|
+
const audioBlob = await audio.blob();
|
56
|
+
const arrayBuffer = await audioBlob.arrayBuffer();
|
57
|
+
|
58
|
+
const audioData: Uint8Array = new Uint8Array(arrayBuffer)
|
59
|
+
|
60
|
+
context.files["audio/" + clipName] = audioData;
|
61
|
+
}
|
62
|
+
}
|
63
|
+
}
|