Needle Engine

Changes between version 3.5.2-alpha and 3.5.3-alpha
Files changed (28) hide show
  1. src/engine/codegen/register_types.js +6 -2
  2. src/engine-components/ui/BaseUIComponent.ts +8 -3
  3. src/engine-components/export/usdz/extensions/behavior/Behaviour.ts +23 -1
  4. src/engine-components/export/usdz/extensions/behavior/BehaviourComponents.ts +79 -2
  5. src/engine-components/export/usdz/extensions/behavior/BehavioursBuilder.ts +46 -0
  6. src/engine-components/ui/Canvas.ts +1 -1
  7. src/engine-components/Component.ts +1 -3
  8. src/engine-components/codegen/components.ts +2 -0
  9. src/engine/debug/debug_overlay.ts +2 -1
  10. src/engine/engine_components.ts +2 -1
  11. src/engine/engine_element_loading.ts +2 -2
  12. src/engine/engine_gameobject.ts +3 -0
  13. src/engine/engine_input.ts +4 -1
  14. src/engine/engine_physics_rapier.ts +2 -1
  15. src/engine/engine_serialization_core.ts +17 -1
  16. src/engine-components/ui/EventSystem.ts +5 -1
  17. src/engine-components/export/usdz/Extension.ts +3 -2
  18. src/engine-components/ui/Image.ts +16 -1
  19. src/engine-components/ui/Interfaces.ts +1 -1
  20. src/engine-components/ui/Layout.ts +2 -0
  21. src/engine/extensions/NEEDLE_lighting_settings.ts +11 -1
  22. src/engine-components/ui/PointerEvents.ts +16 -2
  23. src/engine-components/ui/RaycastUtils.ts +6 -1
  24. src/engine-components/ui/RectTransform.ts +11 -11
  25. src/engine-components/ui/Text.ts +1 -3
  26. src/engine-components/export/usdz/ThreeUSDZExporter.ts +53 -14
  27. src/engine-components/export/usdz/USDZExporter.ts +34 -4
  28. src/engine-components/export/usdz/extensions/behavior/AudioExtension.ts +63 -0
src/engine/codegen/register_types.js CHANGED
@@ -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);
src/engine-components/ui/BaseUIComponent.ts CHANGED
@@ -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
- const cv = this.Root as any as ICanvas;
55
- if (cv?.isCanvas) return cv;
56
- return null;
61
+ return this.canvas;
57
62
  }
58
63
 
59
64
  // private _intermediate?: Object3D;
src/engine-components/export/usdz/extensions/behavior/Behaviour.ts CHANGED
@@ -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
src/engine-components/export/usdz/extensions/behavior/BehaviourComponents.ts CHANGED
@@ -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, AnimationAction } from "three";
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)
src/engine-components/export/usdz/extensions/behavior/BehavioursBuilder.ts CHANGED
@@ -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 }
src/engine-components/ui/Canvas.ts CHANGED
@@ -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
  }
src/engine-components/Component.ts CHANGED
@@ -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;
src/engine-components/codegen/components.ts CHANGED
@@ -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";
src/engine/debug/debug_overlay.ts CHANGED
@@ -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
- return;
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) + "...";
src/engine/engine_components.ts CHANGED
@@ -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
- return onGetComponent(obj, componentType, arr);
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) {
src/engine/engine_element_loading.ts CHANGED
@@ -186,7 +186,7 @@
186
186
  if (loadingStyle === "light")
187
187
  this._loadingElement.style.backgroundColor = "#ddd";
188
188
  else
189
- this._loadingElement.style.backgroundColor = "#000";
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);
src/engine/engine_gameobject.ts CHANGED
@@ -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();
src/engine/engine_input.ts CHANGED
@@ -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
- this._pointerPositionsDelta[evt.button].set(dx, dy);
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;
src/engine/engine_physics_rapier.ts CHANGED
@@ -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
- if (import.meta.env.VITE_NEEDLE_USE_RAPIER === "false") {
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
src/engine/engine_serialization_core.ts CHANGED
@@ -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
- // console.log("ASSIGN", key, member, member[key], targetMember, data[key]);
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":
src/engine-components/ui/EventSystem.ts CHANGED
@@ -255,7 +255,7 @@
255
255
  if (!hits) return;
256
256
  this.lastPointerEvent = args;
257
257
 
258
- const evt : AfterHandleInputEvent = {
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);
src/engine-components/export/usdz/Extension.ts CHANGED
@@ -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
  }
src/engine-components/ui/Image.ts CHANGED
@@ -5,13 +5,28 @@
5
5
 
6
6
  class Sprite {
7
7
  @serializable(Texture)
8
- texture?: THREE.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;
src/engine-components/ui/Interfaces.ts CHANGED
@@ -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);
src/engine-components/ui/Layout.ts CHANGED
@@ -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)
src/engine/extensions/NEEDLE_lighting_settings.ts CHANGED
@@ -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(_result.scene.children[0], SceneLightSettings, false);
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 {
src/engine-components/ui/PointerEvents.ts CHANGED
@@ -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
- Use() {
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 { }
src/engine-components/ui/RaycastUtils.ts CHANGED
@@ -20,7 +20,12 @@
20
20
  };
21
21
 
22
22
  static isInteractable(obj: THREE.Object3D, out?: { canvasGroup?: ICanvasGroup, graphic?: IGraphic }): boolean {
23
- if(obj === null || obj === undefined || !obj.visible) return false;
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
 
src/engine-components/ui/RectTransform.ts CHANGED
@@ -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!: Vector2;
58
+ sizeDelta: Vector2 = new Vector2(100, 100);
59
59
 
60
60
  @serializable(Vector2)
61
- pivot?: Vector2;
61
+ pivot: Vector2 = new Vector2(.5, .5);
62
62
 
63
63
  @serializable(Vector2)
64
- anchorMin!: Vector2;
64
+ anchorMin: Vector2 = new Vector2(0, 0);
65
65
  @serializable(Vector2)
66
- anchorMax!: Vector2;
66
+ anchorMax: Vector2 = new Vector2(1, 1);
67
67
 
68
68
  @serializable(Vector2)
69
- offsetMin!: Vector2;
69
+ offsetMin: Vector2 = new Vector2(0, 0);
70
70
  @serializable(Vector2)
71
- offsetMax!: Vector2;
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.rect!.width,
305
- height: this.rect!.height,// * this.context.mainCameraComponent!.aspect,
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
src/engine-components/ui/Text.ts CHANGED
@@ -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,
src/engine-components/export/usdz/ThreeUSDZExporter.ts CHANGED
@@ -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
- const geometry = object.geometry;
518
- const material = object.material;
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.isMesh && material && (material.isMeshStandardMaterial || material.isMeshBasicMaterial) && ! object.isSkinnedMesh ) {
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.isCamera ) {
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 ( typeof ext[ name ] === 'function' )
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
- prepend references = @./geometries/Geometry_${geometry.id}.usd@</Geometry>
855
- prepend apiSchemas = ["MaterialBindingAPI"]
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
src/engine-components/export/usdz/USDZExporter.ts CHANGED
@@ -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
 
src/engine-components/export/usdz/extensions/behavior/AudioExtension.ts ADDED
@@ -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
+ }