Needle Engine

Changes between version 4.2.0 and 4.2.2
Files changed (18) hide show
  1. src/engine-components/export/usdz/extensions/behavior/Behaviour.ts +17 -11
  2. src/engine-components/export/usdz/extensions/behavior/BehaviourComponents.ts +41 -23
  3. src/engine-components/export/usdz/extensions/behavior/BehavioursBuilder.ts +5 -2
  4. src/engine-components/ui/Button.ts +3 -3
  5. src/engine-components/Duplicatable.ts +2 -2
  6. src/engine/engine_input.ts +47 -9
  7. src/engine/engine_serialization_core.ts +13 -4
  8. src/engine/engine_types.ts +5 -0
  9. src/engine-components/ui/EventSystem.ts +45 -62
  10. src/engine-components/EventTrigger.ts +14 -0
  11. src/engine-components/ui/InputField.ts +9 -1
  12. src/engine/webcomponents/needle menu/needle-menu-spatial.ts +2 -2
  13. src/engine-components/utils/OpenURL.ts +2 -2
  14. src/engine-components/Renderer.ts +5 -0
  15. src/engine-components/ScreenCapture.ts +2 -2
  16. src/engine-components/export/usdz/ThreeUSDZExporter.ts +0 -1
  17. src/engine-components/export/usdz/USDZExporter.ts +2 -2
  18. src/engine-components/webxr/WebXRImageTracking.ts +5 -1
src/engine-components/export/usdz/extensions/behavior/Behaviour.ts CHANGED
@@ -120,19 +120,21 @@
120
120
  if (actionModel.tokenId === "StartAnimation") {
121
121
  playAnimationActions.add(actionModel);
122
122
  }
123
+ let actionType = actionModel.tokenId;
124
+ if (actionModel.type !== undefined) actionType += ":" + actionModel.type;
123
125
  const affected = actionModel.affectedObjects;
124
126
  if (affected) {
125
127
  if (Array.isArray(affected)) {
126
128
  for (const a of affected) {
127
129
  actionTargets.add(a as Target);
128
130
  //@ts-ignore
129
- if (createMermaidGraphForDebugging) mermaidGraphTopLevel += `${actionModel.id}[${actionModel.id}] -- ${actionModel.tokenId} --> ${a.uuid}(("${a.displayName || a.name || a.uuid}"))\n`;
131
+ if (createMermaidGraphForDebugging) mermaidGraphTopLevel += `${actionModel.id}[${actionModel.id}\n${actionType}] -- ${actionType} --> ${a.uuid}(("${a.displayName || a.name || a.uuid}"))\n`;
130
132
  }
131
133
  }
132
134
  else if (typeof affected === "object") {
133
135
  actionTargets.add(affected as Target);
134
136
  //@ts-ignore
135
- if (createMermaidGraphForDebugging) mermaidGraphTopLevel += `${actionModel.id}[${actionModel.id}] -- ${actionModel.tokenId} --> ${affected.uuid}(("${affected.displayName || affected.name || affected.uuid}"))\n`;
137
+ if (createMermaidGraphForDebugging) mermaidGraphTopLevel += `${actionModel.id}[${actionModel.id}\n${actionType}] -- ${actionType} --> ${affected.uuid}(("${affected.displayName || affected.name || affected.uuid}"))\n`;
136
138
  }
137
139
  else if (typeof affected === "string") {
138
140
  actionTargets.add({uuid: affected} as any as Target);
@@ -144,7 +146,7 @@
144
146
  if (typeof xform === "object") {
145
147
  actionTargets.add(xform as Target);
146
148
  //@ts-ignore
147
- if (createMermaidGraphForDebugging) mermaidGraphTopLevel += `${actionModel.id}[${actionModel.id}] -- ${actionModel.tokenId} --> ${xform.uuid}(("${xform.displayName || xform.name || xform.uuid}"))\n`;
149
+ if (createMermaidGraphForDebugging) mermaidGraphTopLevel += `${actionModel.id}[${actionModel.id}\n${actionType}] -- ${actionType} --> ${xform.uuid}(("${xform.displayName || xform.name || xform.uuid}"))\n`;
148
150
  }
149
151
  else if (typeof xform === "string") {
150
152
  actionTargets.add({uuid: xform} as any as Target);
@@ -159,19 +161,21 @@
159
161
  collectTrigger(t, action);
160
162
  }
161
163
  else if (trigger instanceof TriggerModel) {
164
+ let triggerType = trigger.tokenId;
165
+ if (trigger.type !== undefined) triggerType += ":" + trigger.type;
162
166
  if (typeof trigger.targetId === "object") {
163
167
  triggerSources.add(trigger.targetId as Target);
164
168
  //@ts-ignore
165
- if (createMermaidGraphForDebugging) mermaidGraphTopLevel += `${trigger.targetId.uuid}(("${trigger.targetId.displayName}")) --> ${trigger.id}[${trigger.id}]\n`;
169
+ if (createMermaidGraphForDebugging) mermaidGraphTopLevel += `${trigger.targetId.uuid}(("${trigger.targetId.displayName}")) --> ${trigger.id}[${trigger.id}\n${triggerType}]\n`;
166
170
  }
167
171
  //@ts-ignore
168
- if (createMermaidGraphForDebugging) mermaidGraph += `${trigger.id}((${trigger.id})) -- ${trigger.tokenId}${trigger.type ? ":" + trigger.type : ""} --> ${action.id}[${action.tokenId || action.id}]\n`;
172
+ if (createMermaidGraphForDebugging) mermaidGraph += `${trigger.id}((${trigger.id})) -- ${triggerType} --> ${action.id}[${action.tokenId || action.id}]\n`;
169
173
  }
170
174
  }
171
175
 
172
176
  // collect all targets of all triggers and actions
173
177
  for (const beh of this.behaviours) {
174
- if (createMermaidGraphForDebugging) mermaidGraph += `subgraph Behavior_${beh.id}\n`;
178
+ if (createMermaidGraphForDebugging) mermaidGraph += `subgraph ${beh.id}\n`;
175
179
  collectAction(beh.action);
176
180
  collectTrigger(beh.trigger, beh.action);
177
181
  if (createMermaidGraphForDebugging) mermaidGraph += `end\n`;
@@ -180,10 +184,12 @@
180
184
 
181
185
  if (createMermaidGraphForDebugging) {
182
186
  console.log("All USDZ behaviours", this.behaviours);
183
- console.warn("The Mermaid graph can be pasted into https://massive-mermaid.glitch.me/ or https://mermaid.live/edit. It should be in your clipboard already!");
184
- console.log(mermaidGraph);
185
- // copy to clipboard, can be pasted into https://massive-mermaid.glitch.me/ or https://mermaid.live/edit
186
- navigator.clipboard.writeText(mermaidGraph);
187
+ if (this.behaviours.length) {
188
+ console.warn("The Mermaid graph can be pasted into https://massive-mermaid.glitch.me/ or https://mermaid.live/edit. It should be in your clipboard already!");
189
+ console.log(mermaidGraph);
190
+ // copy to clipboard, can be pasted into https://massive-mermaid.glitch.me/ or https://mermaid.live/edit
191
+ navigator.clipboard.writeText(mermaidGraph);
192
+ }
187
193
  }
188
194
 
189
195
  {
@@ -212,7 +218,7 @@
212
218
  }
213
219
  }
214
220
 
215
- if (createMermaidGraphForDebugging) {
221
+ if (createMermaidGraphForDebugging && playAnimationActions.size) {
216
222
  console.log(animationsGraph);
217
223
  }
218
224
 
src/engine-components/export/usdz/extensions/behavior/BehaviourComponents.ts CHANGED
@@ -17,7 +17,7 @@
17
17
  import { AnimationExtension, RegisteredAnimationInfo, type UsdzAnimation } from "../Animation.js";
18
18
  import { AudioExtension } from "./AudioExtension.js";
19
19
  import type { BehaviorExtension, UsdzBehaviour } from "./Behaviour.js";
20
- import { ActionBuilder, ActionModel, BehaviorModel, EmphasizeActionMotionType,type IBehaviorElement, Target, TriggerBuilder } from "./BehavioursBuilder.js";
20
+ import { ActionBuilder, ActionModel, BehaviorModel, EmphasizeActionMotionType,GroupActionModel,type IBehaviorElement, Target, TriggerBuilder } from "./BehavioursBuilder.js";
21
21
 
22
22
  const debug = getParam("debugusdzbehaviours");
23
23
 
@@ -203,10 +203,10 @@
203
203
  }
204
204
 
205
205
  onPointerEnter(_args: PointerEventData) {
206
- this.context.input.setCursorPointer();
206
+ this.context.input.setCursor("pointer");
207
207
  }
208
208
  onPointerExit(_: PointerEventData) {
209
- this.context.input.setCursorNormal();
209
+ this.context.input.unsetCursor("pointer");
210
210
  }
211
211
  onPointerClick(args: PointerEventData) {
212
212
  args.use();
@@ -251,8 +251,9 @@
251
251
  private targetModels!: USDObject[];
252
252
 
253
253
  private static _materialTriggersPerId: { [key: string]: ChangeMaterialOnClick[] } = {}
254
+ private static _startHiddenBehaviour: BehaviorModel | null = null;
255
+ private static _parallelStartHiddenActions: USDObject[] = [];
254
256
 
255
-
256
257
  async beforeCreateDocument(_ext: BehaviorExtension, _context) {
257
258
  this.targetModels = [];
258
259
  ChangeMaterialOnClick._materialTriggersPerId = {}
@@ -309,23 +310,25 @@
309
310
  }
310
311
 
311
312
  private createAndAttachBehaviors(ext: BehaviorExtension, myVariants: Array<USDObject>, otherVariants: Array<USDObject>) {
312
- const start: ActionModel[] = [];
313
313
  const select: ActionModel[] = [];
314
314
 
315
315
  const fadeDuration = Math.max(0, this.fadeDuration);
316
316
 
317
317
  select.push(ActionBuilder.fadeAction([...this.targetModels, ...otherVariants], fadeDuration, false));
318
- start.push(ActionBuilder.fadeAction(myVariants, fadeDuration, false));
319
318
  select.push(ActionBuilder.fadeAction(myVariants, fadeDuration, true));
320
319
 
321
320
  ext.addBehavior(new BehaviorModel("Select_" + this.selfModel.name,
322
321
  TriggerBuilder.tapTrigger(this.selfModel),
323
322
  ActionBuilder.parallel(...select))
324
323
  );
325
- ext.addBehavior(new BehaviorModel("StartHidden_" + this.selfModel.name,
326
- TriggerBuilder.sceneStartTrigger(),
327
- ActionBuilder.parallel(...start))
328
- );
324
+ ChangeMaterialOnClick._parallelStartHiddenActions.push(...myVariants);
325
+ if (!ChangeMaterialOnClick._startHiddenBehaviour) {
326
+ ChangeMaterialOnClick._startHiddenBehaviour =
327
+ new BehaviorModel("StartHidden_" + this.selfModel.name,
328
+ TriggerBuilder.sceneStartTrigger(),
329
+ ActionBuilder.fadeAction(ChangeMaterialOnClick._parallelStartHiddenActions, fadeDuration, false));
330
+ ext.addBehavior(ChangeMaterialOnClick._startHiddenBehaviour);
331
+ }
329
332
  }
330
333
 
331
334
  private static getMaterialName(material: Material) {
@@ -526,9 +529,9 @@
526
529
  sequence.push(ActionBuilder.fadeAction(selfModel, 0, false));
527
530
  sequence.push(ActionBuilder.fadeAction(this.targetModel, 0, targetState));
528
531
 
529
- ext.addBehavior(new BehaviorModel("Toggle_" + selfModel.name + "_toggleTo" + (targetState ? "On" : "Off"),
532
+ ext.addBehavior(new BehaviorModel("Toggle_" + selfModel.name + "_ToggleTo" + (targetState ? "On" : "Off"),
530
533
  TriggerBuilder.tapTrigger(selfModel),
531
- ActionBuilder.parallel(...sequence)
534
+ sequence.length > 1 ? ActionBuilder.parallel(...sequence) : sequence[0],
532
535
  ));
533
536
  }
534
537
  // We have a toggleModel, so we need to set up two sequences:
@@ -540,7 +543,7 @@
540
543
  toggleSequence.push(ActionBuilder.fadeAction(this.toggleModel, 0, true));
541
544
  toggleSequence.push(ActionBuilder.fadeAction(this.targetModel, 0, targetState));
542
545
 
543
- ext.addBehavior(new BehaviorModel("Toggle_" + selfModel.name + "_toggleTo" + (targetState ? "On" : "Off"),
546
+ ext.addBehavior(new BehaviorModel("Toggle_" + selfModel.name + "_ToggleTo" + (targetState ? "On" : "Off"),
544
547
  TriggerBuilder.tapTrigger(selfModel),
545
548
  ActionBuilder.parallel(...toggleSequence)
546
549
  ));
@@ -550,7 +553,7 @@
550
553
  reverseSequence.push(ActionBuilder.fadeAction(selfModel, 0, true));
551
554
  reverseSequence.push(ActionBuilder.fadeAction(this.targetModel, 0, !targetState));
552
555
 
553
- ext.addBehavior(new BehaviorModel("Toggle_" + selfModel.name + "_toggleTo" + (!targetState ? "On" : "Off"),
556
+ ext.addBehavior(new BehaviorModel("Toggle_" + selfModel.name + "_ToggleTo" + (!targetState ? "On" : "Off"),
554
557
  TriggerBuilder.tapTrigger(this.toggleModel),
555
558
  ActionBuilder.parallel(...reverseSequence)
556
559
  ));
@@ -565,10 +568,7 @@
565
568
  if (this.toggleModel)
566
569
  objectsToHide.push(this.toggleModel);
567
570
 
568
- ext.addBehavior(new BehaviorModel("HideOnStart_" + this.gameObject.name,
569
- TriggerBuilder.sceneStartTrigger(),
570
- ActionBuilder.fadeAction(objectsToHide, 0, false)
571
- ));
571
+ HideOnStart.add(objectsToHide, ext);
572
572
  }
573
573
  }
574
574
 
@@ -595,6 +595,27 @@
595
595
  */
596
596
  export class HideOnStart extends Behaviour implements UsdzBehaviour {
597
597
 
598
+ private static _fadeBehaviour?: BehaviorModel;
599
+ private static _fadeObjects: Array<USDObject | Object3D> = [];
600
+
601
+ static add(target: Target, ext: BehaviorExtension) {
602
+ const arr = Array.isArray(target) ? target : [target];
603
+ for (const entry of arr) {
604
+ if (!HideOnStart._fadeObjects.includes(entry)) {
605
+ console.log("adding hide on start", entry);
606
+ HideOnStart._fadeObjects.push(entry);
607
+ }
608
+ }
609
+ if (HideOnStart._fadeBehaviour === undefined) {
610
+ HideOnStart._fadeBehaviour = new BehaviorModel("HideOnStart",
611
+ TriggerBuilder.sceneStartTrigger(),
612
+ //@ts-ignore
613
+ ActionBuilder.fadeAction(HideOnStart._fadeObjects, 0, false)
614
+ );
615
+ ext.addBehavior(HideOnStart._fadeBehaviour);
616
+ }
617
+ }
618
+
598
619
  start() {
599
620
  GameObject.setActive(this.gameObject, false);
600
621
  }
@@ -603,10 +624,7 @@
603
624
  if (model.uuid === this.gameObject.uuid) {
604
625
  // we only want to mark the object as HideOnStart if it's still hidden
605
626
  if (!this.wasVisible) {
606
- ext.addBehavior(new BehaviorModel("HideOnStart_" + this.gameObject.name,
607
- TriggerBuilder.sceneStartTrigger(),
608
- ActionBuilder.fadeAction(model, 0, false)
609
- ));
627
+ HideOnStart.add(model, ext);
610
628
  }
611
629
  }
612
630
  }
@@ -752,7 +770,7 @@
752
770
  // automatically play audio on start too if the referenced AudioSource has playOnAwake enabled
753
771
  if (this.target && this.target.playOnAwake && this.target.enabled) {
754
772
  if (anyChildHasGeometry && this.trigger === "tap") {
755
- // HACK Currently (20240509) we MUST not emit this behaviour if we're also expecting the tap trigger to work.
773
+ // WORKAROUND Currently (20240509) we MUST not emit this behaviour if we're also expecting the tap trigger to work.
756
774
  // Seems to be a regression in QuickLook... audio clips can't be stopped anymore as soon as they start playing.
757
775
  console.warn("USDZExport: Audio sources that are played on tap can't also auto-play at scene start due to a QuickLook bug.");
758
776
  }
src/engine-components/export/usdz/extensions/behavior/BehavioursBuilder.ts CHANGED
@@ -199,11 +199,14 @@
199
199
 
200
200
  export class TriggerBuilder {
201
201
 
202
+ private static __sceneStartTrigger?: TriggerModel;
203
+
202
204
  static sceneStartTrigger(): TriggerModel {
203
- const trigger = new TriggerModel();
204
- trigger.targetId = undefined;
205
+ if (this.__sceneStartTrigger !== undefined) return this.__sceneStartTrigger;
206
+ const trigger = new TriggerModel(undefined, "SceneStart");
205
207
  trigger.tokenId = "SceneTransition";
206
208
  trigger.type = "enter";
209
+ this.__sceneStartTrigger = trigger;
207
210
  return trigger;
208
211
  }
209
212
 
src/engine-components/ui/Button.ts CHANGED
@@ -90,7 +90,7 @@
90
90
  else if (this.transition === Transition.ColorTint && this.colors) {
91
91
  this._image?.setState("hovered");
92
92
  }
93
- if (canSetCursor) this.context.input.setCursorPointer();
93
+ if (canSetCursor) this.context.input.setCursor("pointer");
94
94
  }
95
95
 
96
96
  onPointerExit() {
@@ -107,7 +107,7 @@
107
107
  else if (this.transition === Transition.ColorTint && this.colors) {
108
108
  this._image?.setState("normal");
109
109
  }
110
- this.context.input.setCursorNormal();
110
+ this.context.input.unsetCursor("pointer");
111
111
  }
112
112
 
113
113
  onPointerDown(_) {
@@ -211,7 +211,7 @@
211
211
  }
212
212
 
213
213
  onDestroy(): void {
214
- if (this._isHovered) this.context.input.setCursorNormal();
214
+ if (this._isHovered) this.context.input.unsetCursor("pointer");
215
215
  }
216
216
 
217
217
  private _requestedAnimatorTrigger?: string;
src/engine-components/Duplicatable.ts CHANGED
@@ -105,14 +105,14 @@
105
105
  if (!this.object) return;
106
106
  if (!this.context.connection.allowEditing) return;
107
107
  if (args.button !== 0) return;
108
- this.context.input.setCursorPointer();
108
+ this.context.input.setCursor("pointer");
109
109
  }
110
110
  onPointerExit(args: PointerEventData) {
111
111
  if (args.used) return;
112
112
  if (!this.object) return;
113
113
  if (!this.context.connection.allowEditing) return;
114
114
  if (args.button !== 0) return;
115
- this.context.input.setCursorNormal();
115
+ this.context.input.unsetCursor("pointer");
116
116
  }
117
117
 
118
118
  /** @internal */
src/engine/engine_input.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  import { showBalloonMessage, showBalloonWarning } from './debug/debug.js';
4
4
  import { Context } from './engine_setup.js';
5
5
  import { getTempVector, getWorldQuaternion } from './engine_three_utils.js';
6
- import type { ButtonName, IGameObject, IInput, MouseButtonName, Vec2 } from './engine_types.js';
6
+ import type { ButtonName, CursorTypeName, IGameObject, IInput, MouseButtonName, Vec2 } from './engine_types.js';
7
7
  import { DeviceUtilities, type EnumToPrimitiveUnion, getParam } from './engine_utils.js';
8
8
 
9
9
  const debug = getParam("debuginput");
@@ -394,21 +394,46 @@
394
394
  get mouseWheelChanged(): boolean { return this.getMouseWheelChanged(0); }
395
395
 
396
396
  /** Is the primary pointer double clicked (usually the left button). This is equivalent to `input.mouseDoubleClick` */
397
- get click() : boolean { return this._pointerClick[0]; }
397
+ get click(): boolean { return this._pointerClick[0]; }
398
398
  /** Was a double click detected for the primary pointer? */
399
- get doubleClick() : boolean { return this._pointerDoubleClick[0]; }
399
+ get doubleClick(): boolean { return this._pointerDoubleClick[0]; }
400
400
 
401
- private _specialCursorTrigger: number = 0;
401
+ private readonly _setCursorTypes: CursorTypeName[] = [];
402
402
 
403
+ /** @deprecated use setCursor("pointer") */
403
404
  setCursorPointer() {
404
- this._specialCursorTrigger += 1;
405
- this.context.domElement.style.cursor = "pointer";
405
+ this.setCursor("pointer");
406
406
  }
407
+ /** @deprecated use unsetCursor() */
407
408
  setCursorNormal() {
408
- this._specialCursorTrigger -= 1;
409
- this._specialCursorTrigger = Math.max(0, this._specialCursorTrigger);
410
- if (this._specialCursorTrigger === 0)
409
+ this.unsetCursor("pointer");
410
+ }
411
+ /**
412
+ * Set a custom cursor. This will set the cursor type until unsetCursor is called
413
+ */
414
+ setCursor(type: CursorTypeName) {
415
+ this._setCursorTypes.push(type);
416
+ if (this._setCursorTypes.length > 10) {
417
+ this._setCursorTypes.shift();
418
+ }
419
+ this.updateCursor();
420
+ }
421
+ /**
422
+ * Unset a custom cursor. This will set the cursor type to the previous type or default
423
+ */
424
+ unsetCursor(type: CursorTypeName) {
425
+ for (let i = this._setCursorTypes.length - 1; i >= 0; i--) {
426
+ if (this._setCursorTypes[i] === type) {
427
+ this._setCursorTypes.splice(i, 1);
428
+ this.updateCursor();
429
+ break;
430
+ }
431
+ }
432
+ }
433
+ private updateCursor() {
434
+ if (this._setCursorTypes?.length == 0)
411
435
  this.context.domElement.style.cursor = "default";
436
+ else this.context.domElement.style.cursor = this._setCursorTypes[this._setCursorTypes.length - 1];
412
437
  }
413
438
 
414
439
  /**
@@ -756,6 +781,10 @@
756
781
  window.removeEventListener('pointerup', this.onPointerUp);
757
782
  window.removeEventListener('pointercancel', this.onPointerCancel);
758
783
 
784
+ window.removeEventListener("touchstart", this.onTouchStart);
785
+ window.removeEventListener("touchmove", this.onTouchMove);
786
+ window.removeEventListener("touchend", this.onTouchEnd);
787
+
759
788
  this._htmlEventSource?.removeEventListener('wheel', this.onMouseWheel, false);
760
789
  window.removeEventListener("wheel", this.onWheelWindow, false);
761
790
 
@@ -778,7 +807,10 @@
778
807
  }
779
808
  }
780
809
 
810
+ private readonly _receivedPointerMoveEventsThisFrame = new Array<number>;
811
+
781
812
  private onEndOfFrame = () => {
813
+ this._receivedPointerMoveEventsThisFrame.length = 0;
782
814
  for (let i = 0; i < this._pointerUp.length; i++)
783
815
  this._pointerUp[i] = false;
784
816
  for (let i = 0; i < this._pointerDown.length; i++)
@@ -901,8 +933,14 @@
901
933
  }
902
934
  private onPointerMove = (evt: PointerEvent) => {
903
935
  if (this.context.isInAR) return;
936
+
937
+ // Prevent multiple pointerMove events per frame
938
+ if (this._receivedPointerMoveEventsThisFrame.includes(evt.pointerId)) return;
939
+ this._receivedPointerMoveEventsThisFrame.push(evt.pointerId);
940
+
904
941
  // We want to keep receiving move events until pointerUp and not stop handling events just because we're hovering over *some* HTML element
905
942
  // if (this.canReceiveInput(evt) === false) return;
943
+
906
944
  let button = evt.button;
907
945
  if (evt.pointerType === "mouse") {
908
946
  const pressedButton = this.getFirstPressedButtonForPointer(0);
src/engine/engine_serialization_core.ts CHANGED
@@ -1,8 +1,8 @@
1
- import { AnimationClip, Material, Mesh, Object3D, Texture } from "three";
1
+ import { AnimationClip, BufferGeometry, InstancedBufferGeometry, Material, Mesh, Object3D, Texture, WireframeGeometry } from "three";
2
2
  import { type GLTF } from "three/examples/jsm/loaders/GLTFLoader.js";
3
3
 
4
4
  import { debugExtension } from "../engine/engine_default_parameters.js";
5
- import { addLog,LogType } from "./debug/debug_overlay.js";
5
+ import { addLog, LogType } from "./debug/debug_overlay.js";
6
6
  import { isLocalNetwork } from "./engine_networking_utils.js";
7
7
  import { Context } from "./engine_setup.js";
8
8
  import type { Constructor, ConstructorConcrete, SourceIdentifier } from "./engine_types.js";
@@ -515,8 +515,17 @@
515
515
 
516
516
  // e.g. when @serializable(Texture) and the texture is already resolved via json pointer from gltf
517
517
  // then we dont need to do anything else
518
- if (!typeIsFunction && currentValue instanceof type) return currentValue;
519
-
518
+ if (!typeIsFunction && currentValue) {
519
+ if (currentValue instanceof Material) return currentValue;
520
+ if (currentValue instanceof Texture) return currentValue;
521
+ if (currentValue instanceof Mesh) return currentValue;
522
+ if (currentValue instanceof BufferGeometry) return currentValue;
523
+ if (currentValue instanceof AnimationClip) return currentValue;
524
+ }
525
+ // Removed this line because it prevents assigning serialized values to existing instances for e.g. EventList
526
+ // https://linear.app/needle/issue/NE-5350
527
+ // if (!typeIsFunction && currentValue instanceof type) return currentValue;
528
+
520
529
  // try to resolve the serializer for a type only once
521
530
  if (!typeContext) {
522
531
  typeContext = {
src/engine/engine_types.ts CHANGED
@@ -528,6 +528,11 @@
528
528
  debugRenderRaycasts: boolean;
529
529
  }
530
530
 
531
+ /**
532
+ * Available cursor types
533
+ * @link https://developer.mozilla.org/en-US/docs/Web/CSS/cursor
534
+ */
535
+ export type CursorTypeName = "auto" | "default" | "none" | "context-menu" | "help" | "pointer" | "progress" | "wait" | "cell" | "crosshair" | "text" | "vertical-text" | "alias" | "copy" | "move" | "no-drop" | "not-allowed" | "grab" | "grabbing" | "all-scroll" | "col-resize" | "row-resize" | "n-resize" | "e-resize" | "s-resize" | "w-resize" | "nw-resize" | "se-resize" | "sw-resize" | "ew-resize" | "ns-resize" | "nesw-resize" | "nwse-resize" | "zoom-in" | "zoom-out";
531
536
 
532
537
  /** Typical mouse button names for most devices */
533
538
  export type MouseButtonName = "left" | "right" | "middle";
src/engine-components/ui/EventSystem.ts CHANGED
@@ -1,11 +1,12 @@
1
- import { type Intersection, Mesh,Object3D } from "three";
1
+ import { type Intersection, Mesh, Object3D } from "three";
2
2
 
3
3
  import { isDevEnvironment, showBalloonMessage } from "../../engine/debug/index.js";
4
- import { type InputEventNames, InputEvents, NEPointerEvent, NEPointerEventIntersection,PointerType } from "../../engine/engine_input.js";
4
+ import { type InputEventNames, InputEvents, NEPointerEvent, NEPointerEventIntersection, PointerType } from "../../engine/engine_input.js";
5
+ import { onInitialized } from "../../engine/engine_lifecycle_api.js";
5
6
  import { Mathf } from "../../engine/engine_math.js";
6
7
  import { RaycastOptions, type RaycastTestObjectReturnType } from "../../engine/engine_physics.js";
7
8
  import { Context } from "../../engine/engine_setup.js";
8
- import type { IComponent } from "../../engine/engine_types.js";
9
+ import { HideFlags, type IComponent } from "../../engine/engine_types.js";
9
10
  import { getParam } from "../../engine/engine_utils.js";
10
11
  import { Behaviour, GameObject } from "../Component.js";
11
12
  import { $shadowDomOwner } from "./BaseUIComponent.js";
@@ -30,47 +31,15 @@
30
31
 
31
32
  declare type IComponentCanMaybeReceiveEvents = IPointerEventHandler & IComponent & { interactable?: boolean };
32
33
 
34
+ onInitialized((ctx) => {
35
+ EventSystem.createIfNoneExists(ctx);
36
+ })
37
+
33
38
  /**
34
39
  * @category User Interface
35
40
  * @group Components
36
41
  */
37
42
  export class EventSystem extends Behaviour {
38
- private static _eventSystemMap = new Map<Context, EventSystem[]>();
39
-
40
- static didSearchEventSystem: boolean = false;
41
- static createIfNoneExists(context: Context) {
42
- if (!this.didSearchEventSystem) {
43
- this.didSearchEventSystem = true;
44
- if (EventSystem.systems.length <= 0) {
45
- EventSystem.systems.push(...GameObject.findObjectsOfType(EventSystem, context));
46
- }
47
- }
48
- for (const sys of EventSystem.systems) {
49
- if (sys.context === context) return; // exists
50
- }
51
- const go = new Object3D();
52
- GameObject.addComponent(go, EventSystem);
53
- context.scene.add(go);
54
- }
55
-
56
- static get systems(): EventSystem[] {
57
- const context = Context.Current;
58
- if (!this._eventSystemMap.has(context)) {
59
- this._eventSystemMap.set(context, []);
60
- }
61
- return this._eventSystemMap.get(context)!;
62
- }
63
-
64
- static get(ctx: Context): EventSystem | null {
65
- const systems = this._eventSystemMap.get(ctx);
66
- if (systems && systems.length > 0) return systems[0];
67
- return null;
68
- }
69
-
70
- static get instance(): EventSystem | null {
71
- return this.systems[0];
72
- }
73
-
74
43
  //@ts-ignore
75
44
  static ensureUpdateMeshUI(instance, context: Context, force: boolean = false) {
76
45
  MeshUIHelper.update(instance, context, force);
@@ -79,32 +48,22 @@
79
48
  MeshUIHelper.markDirty();
80
49
  }
81
50
 
82
- private raycaster: Raycaster[] = [];
83
-
84
- constructor() {
85
- super();
86
- EventSystem.systems.push(this);
51
+ static createIfNoneExists(context: Context) {
52
+ if (!context.scene.getComponent(EventSystem)) {
53
+ context.scene.addComponent(EventSystem);
54
+ }
87
55
  }
88
56
 
89
- get hasActiveUI() { return this.currentActiveMeshUIComponents.length > 0; }
90
- get isHoveringObjects() { return this.hoveredByID.size > 0; }
91
-
92
- onDestroy(): void {
93
- EventSystem.systems.splice(EventSystem.systems.indexOf(this), 1);
57
+ static get(ctx: Context): EventSystem | null {
58
+ this.createIfNoneExists(ctx);
59
+ return ctx.scene.getComponent(EventSystem);
94
60
  }
95
61
 
96
- start() {
97
- if (this.raycaster.length <= 0) {
98
- const res = GameObject.findObjectOfType(Raycaster, this.context);
99
- if (!res) {
100
- const rc = GameObject.addComponent(this.context.scene, ObjectRaycaster);
101
- this.raycaster.push(rc);
102
- if (isDevEnvironment() || debug)
103
- console.warn("Added an ObjectRaycaster to the scene because no raycaster was found.");
104
- }
105
- }
62
+ static get instance(): EventSystem | null {
63
+ return this.get(Context.Current);
106
64
  }
107
65
 
66
+ private readonly raycaster: Raycaster[] = [];
108
67
  register(rc: Raycaster) {
109
68
  if (rc && this.raycaster && !this.raycaster.includes(rc))
110
69
  this.raycaster?.push(rc);
@@ -116,6 +75,23 @@
116
75
  }
117
76
  }
118
77
 
78
+ get hasActiveUI() { return this.currentActiveMeshUIComponents.length > 0; }
79
+ get isHoveringObjects() { return this.hoveredByID.size > 0; }
80
+
81
+ awake(): void {
82
+ // We only want ONE eventsystem on the root scene
83
+ // as long as this component is not implemented in core we need to check this here
84
+ if (this.gameObject as Object3D !== this.context.scene) {
85
+ this.enabled = false;
86
+ }
87
+ }
88
+
89
+ start() {
90
+ if (!this.context.scene.getComponent(Raycaster)) {
91
+ this.context.scene.addComponent(ObjectRaycaster);
92
+ }
93
+ }
94
+
119
95
  onEnable(): void {
120
96
  this.context.input.addEventListener(InputEvents.PointerDown, this.onPointerEvent);
121
97
  this.context.input.addEventListener(InputEvents.PointerUp, this.onPointerEvent);
@@ -183,7 +159,6 @@
183
159
  options.screenPoint = this.context.input.getPointerPositionRC(pointerEvent.pointerId)!;
184
160
  }
185
161
 
186
-
187
162
  const hits = this.performRaycast(options) as Array<NEPointerEventIntersection>;
188
163
  if (hits) {
189
164
  for (const hit of hits) {
@@ -242,6 +217,14 @@
242
217
  // TODO: this implementation below should be removed and we should regularly raycast objects in the scene unless marked as "do not raycast"
243
218
  // with the introduction of the mesh-bvh based raycasting the performance impact should be greatly reduced. But this needs further testing
244
219
 
220
+ const raycasterOnObject = obj && "getComponent" in obj ? obj.getComponent(Raycaster) : null;
221
+ if (raycasterOnObject && raycasterOnObject != this._currentlyActiveRaycaster) {
222
+ return false;
223
+ }
224
+ // if (this._currentPointerEventName == "pointermove") {
225
+ // console.log(this.context.time.frame, obj.name, obj.type, obj.guid)
226
+ // }
227
+
245
228
  // check if this object is actually a UI shadow hierarchy object
246
229
  let uiOwner: Object3D | null = null;
247
230
  const isUI = isUIObject(obj);
@@ -450,7 +433,7 @@
450
433
  isShadow = true;
451
434
  }
452
435
  }
453
-
436
+
454
437
  // adding this to have a way for allowing to receive events on TMUI elements without shadow hierarchy
455
438
  // if(parent["needle:use_eventsystem"] == true){
456
439
  // // if use_eventsystem is true, we want to handle the event
@@ -671,7 +654,7 @@
671
654
  comp[symbol] = state;
672
655
  return true;
673
656
  }
674
- else {
657
+ else {
675
658
  if (!state || !state.includes(pointerId)) return false;
676
659
  const i = state.indexOf(pointerId);
677
660
  if (i !== -1) {
src/engine-components/EventTrigger.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { serializable } from "../engine/engine_serialization.js";
2
+ import { EventSystem } from "./api.js";
2
3
  import { Behaviour } from "./Component.js"
3
4
  import { EventList } from "./EventList.js";
4
5
  import { EventType } from "./EventType.js"
@@ -33,6 +34,13 @@
33
34
  }
34
35
  }
35
36
 
37
+ private hasTrigger(type: EventType) {
38
+ return this.triggers?.some(t => t.eventID === type) ?? false;
39
+ }
40
+ private shouldChangeCursor() {
41
+ return this.hasTrigger(EventType.PointerClick) || this.hasTrigger(EventType.PointerDown) || this.hasTrigger(EventType.PointerUp);
42
+ }
43
+
36
44
  /** @internal */
37
45
  onPointerClick(_: PointerEventData) {
38
46
  this.invoke(EventType.PointerClick);
@@ -40,11 +48,17 @@
40
48
 
41
49
  /** @internal */
42
50
  onPointerEnter(_: PointerEventData) {
51
+ if (this.shouldChangeCursor()) {
52
+ this.context.input.setCursor("pointer");
53
+ }
43
54
  this.invoke(EventType.PointerEnter);
44
55
  }
45
56
 
46
57
  /** @internal */
47
58
  onPointerExit(_: PointerEventData) {
59
+ if (this.shouldChangeCursor()) {
60
+ this.context.input.unsetCursor("pointer");
61
+ }
48
62
  this.invoke(EventType.PointerExit);
49
63
  }
50
64
 
src/engine-components/ui/InputField.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  import { DeviceUtilities, getParam } from "../../engine/engine_utils.js";
4
4
  import { Behaviour, GameObject } from "../Component.js";
5
5
  import { EventList } from "../EventList.js";
6
- import { type IPointerEventHandler } from "./PointerEvents.js";
6
+ import { type IPointerEventHandler,PointerEventData } from "./PointerEvents.js";
7
7
  import { Text } from "./Text.js";
8
8
  import { tryGetUIComponent } from "./Utils.js";
9
9
 
@@ -113,6 +113,14 @@
113
113
  this.onDeselected();
114
114
  }
115
115
 
116
+ onPointerEnter(_args: PointerEventData) {
117
+ const canSetCursor = _args.event.pointerType === "mouse" && _args.button === 0;
118
+ if(canSetCursor) this.context.input.setCursor("text");
119
+ }
120
+ onPointerExit(_args: PointerEventData) {
121
+ this.context.input.unsetCursor("text")
122
+ }
123
+
116
124
  onPointerClick(_args) {
117
125
  if (debug) console.log("CLICK", _args, InputField.active);
118
126
  InputField.activeTime = this.context.time.time;
src/engine/webcomponents/needle menu/needle-menu-spatial.ts CHANGED
@@ -548,14 +548,14 @@
548
548
  }
549
549
 
550
550
  onPointerEnter() {
551
- this.context.input.setCursorPointer();
551
+ this.context.input.setCursor("pointer");
552
552
  if (this.allowModifyUI) {
553
553
  this.element.set({ backgroundOpacity: 1 });
554
554
  ThreeMeshUI.update();
555
555
  }
556
556
  }
557
557
  onPointerExit() {
558
- this.context.input.setCursorNormal();
558
+ this.context.input.unsetCursor("pointer");
559
559
  if (this.allowModifyUI) {
560
560
  this.element.set({ backgroundOpacity: 0 });
561
561
  ThreeMeshUI.update();
src/engine-components/utils/OpenURL.ts CHANGED
@@ -91,12 +91,12 @@
91
91
  /** @internal */
92
92
  onPointerEnter(args) {
93
93
  if (!args.used && this.clickable)
94
- this.context.input.setCursorPointer();
94
+ this.context.input.setCursor("pointer");
95
95
  }
96
96
  /** @internal */
97
97
  onPointerExit() {
98
98
  if (this.clickable)
99
- this.context.input.setCursorNormal();
99
+ this.context.input.unsetCursor("pointer");
100
100
  }
101
101
  /** @internal */
102
102
  onPointerClick(args: PointerEventData) {
src/engine-components/Renderer.ts CHANGED
@@ -716,6 +716,11 @@
716
716
  const factor = this.hasLightmap ? Math.PI : 1;
717
717
  const environmentIntensity = this.context.mainCameraComponent?.environmentIntensity ?? 1;
718
718
  material.envMapIntensity = Math.max(0, environmentIntensity * this.context.sceneLighting.environmentIntensity / factor);
719
+
720
+ // since three 163 we need to set the envMap to the scene envMap if it is not set
721
+ // otherwise the envmapIntensity has no effect: https://github.com/mrdoob/three.js/pull/27903
722
+ // internal issue: https://linear.app/needle/issue/NE-6363
723
+ if (!material.envMap) material.envMap = this.context.scene.environment;
719
724
  }
720
725
 
721
726
  if (this._lightmaps) {
src/engine-components/ScreenCapture.ts CHANGED
@@ -88,13 +88,13 @@
88
88
  onPointerEnter() {
89
89
  if (this.context.connection.allowEditing == false) return;
90
90
  if (!this.allowStartOnClick) return;
91
- this.context.input.setCursorPointer();
91
+ this.context.input.setCursor("pointer");
92
92
  }
93
93
  /** @internal */
94
94
  onPointerExit() {
95
95
  if (this.context.connection.allowEditing == false) return;
96
96
  if (!this.allowStartOnClick) return;
97
- this.context.input.setCursorNormal();
97
+ this.context.input.unsetCursor("pointer");
98
98
  }
99
99
 
100
100
  /** @internal */
src/engine-components/export/usdz/ThreeUSDZExporter.ts CHANGED
@@ -1966,7 +1966,6 @@
1966
1966
 
1967
1967
  const materialName = getMaterialName(material);
1968
1968
 
1969
- console.log(material);
1970
1969
  // Special case: occluder material
1971
1970
  // Supported on iOS 18+ and visionOS 1+
1972
1971
  const isShadowCatcherMaterial =
src/engine-components/export/usdz/USDZExporter.ts CHANGED
@@ -403,7 +403,7 @@
403
403
  if (this.autoExportAnimations) {
404
404
  implicitBehaviors.push(...registerAnimatorsImplictly(objectToExport, animExt));
405
405
  }
406
- const audioExt = this.extensions.find(ext => ext.extensionName === "Audio");
406
+ const audioExt = extensions.find(ext => ext.extensionName === "Audio");
407
407
  if (audioExt && this.autoExportAudioSources)
408
408
  implicitBehaviors.push(...registerAudioSourcesImplictly(objectToExport, audioExt as AudioExtension));
409
409
 
@@ -454,7 +454,7 @@
454
454
  });
455
455
  }
456
456
 
457
- const behaviorExt = this.extensions.find(ext => ext.extensionName === "Behaviour") as BehaviorExtension | undefined;
457
+ const behaviorExt = extensions.find(ext => ext.extensionName === "Behaviour") as BehaviorExtension | undefined;
458
458
  if (this.interactive && behaviorExt && objectsToDisableAtSceneStart.length > 0) {
459
459
  behaviorExt.addBehavior(disableObjectsAtStart(objectsToDisableAtSceneStart));
460
460
  }
src/engine-components/webxr/WebXRImageTracking.ts CHANGED
@@ -160,9 +160,13 @@
160
160
  }
161
161
 
162
162
  onAfterHierarchy(_context: USDZExporterContext, writer: USDWriter) {
163
+ const iOSVersion = DeviceUtilities.getiOSVersion();
164
+ const majorVersion = iOSVersion ? parseInt(iOSVersion.split(".")[0]) : 18;
165
+ const workaroundForFB16119331 = majorVersion >= 18;
166
+ const multiplier = workaroundForFB16119331 ? 1 : 100;
163
167
  writer.beginBlock(`def Preliminary_ReferenceImage "AnchoringReferenceImage"`);
164
168
  writer.appendLine(`uniform asset image = @image_tracking/` + this.filename + `@`);
165
- writer.appendLine(`uniform double physicalWidth = ` + (this.widthInMeters * 100).toFixed(8));
169
+ writer.appendLine(`uniform double physicalWidth = ` + (this.widthInMeters * multiplier).toFixed(8));
166
170
  writer.closeBlock();
167
171
  }