Needle Engine

Changes between version 3.40.0-alpha and 3.40.0-alpha.1
Files changed (4) hide show
  1. src/engine-components/AnimationUtils.ts +6 -6
  2. src/engine-components/export/usdz/extensions/behavior/BehaviourComponents.ts +3 -20
  3. src/engine-components/export/usdz/extensions/behavior/BehavioursBuilder.ts +30 -1
  4. src/engine/engine_utils.ts +4 -1
src/engine-components/AnimationUtils.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Object3D } from "three";
1
+ import { Object3D, PropertyBinding } from "three";
2
2
  import type { GLTF } from "three/examples/jsm/loaders/GLTFLoader.js";
3
3
 
4
4
  import { addNewComponent } from "../engine/engine_components.js";
@@ -39,7 +39,7 @@
39
39
 
40
40
  ContextRegistry.registerCallback(ContextEvent.ContextCreated, args => {
41
41
  const autoplay = args.context.domElement.getAttribute("autoplay");
42
- if (autoplay !== undefined && (autoplay === "" || autoplay === "true")) {
42
+ if (autoplay !== undefined && (autoplay === "" || autoplay === "true" || autoplay === "1")) {
43
43
  if (args.files) {
44
44
  for (const file of args.files) {
45
45
  const hasAnimation = GameObject.foreachComponent(file.file.scene, comp => {
@@ -75,12 +75,12 @@
75
75
  for (const t in animation.tracks) {
76
76
  const track = animation.tracks[t];
77
77
  // TODO use PropertyBinding API directly, needs testing
78
- // const parsedPath = PropertyBinding.parseTrackName(track.name);
79
- // const obj = PropertyBinding.findNode(gltf.scene, parsedPath.nodeName);
78
+ const parsedPath = PropertyBinding.parseTrackName(track.name);
79
+ let obj = PropertyBinding.findNode(gltf.scene, parsedPath.nodeName);
80
80
 
81
- const objectName = track["__objectName"] ?? track.name.substring(0, track.name.indexOf("."));
82
- let obj = gltf.scene.getObjectByName(objectName);
83
81
  if (!obj) {
82
+ const objectName = track["__objectName"] ?? track.name.substring(0, track.name.indexOf("."));
83
+ // let obj = gltf.scene.getObjectByName(objectName);
84
84
  // this finds unnamed objects that still have tracks targeting them
85
85
  obj = gltf.scene.getObjectByProperty('uuid', objectName);
86
86
 
src/engine-components/export/usdz/extensions/behavior/BehaviourComponents.ts CHANGED
@@ -357,7 +357,7 @@
357
357
  private targetModel?: USDObject;
358
358
  private toggleModel?: USDObject;
359
359
 
360
- createBehaviours(_, model, _context) {
360
+ createBehaviours(_, model: USDObject, _context: USDZExporterContext) {
361
361
  if (model.uuid === this.gameObject.uuid) {
362
362
  this.selfModel = model;
363
363
  this.selfModelClone = model.clone();
@@ -389,7 +389,7 @@
389
389
  this.target.visible = true;
390
390
  }
391
391
 
392
- afterCreateDocument(ext, context) {
392
+ afterCreateDocument(ext: BehaviorExtension, context: USDZExporterContext) {
393
393
  if (!this.target) return;
394
394
 
395
395
  // Parameters:
@@ -521,23 +521,6 @@
521
521
  ));
522
522
  }
523
523
 
524
- // add InputTargetComponent for VisionOS direct/indirect interactions
525
- const addInputTargetComponent = (model: USDObject) => {
526
- const empty = USDObject.createEmpty();
527
- empty.name = "InputTarget_" + empty.name;
528
- empty.displayName = undefined;
529
- empty.type = "RealityKitComponent";
530
- empty.onSerialize = (writer: USDWriter) => {
531
- writer.appendLine("bool allowsDirectInput = 1");
532
- writer.appendLine("bool allowsIndirectInput = 1");
533
- writer.appendLine('uniform token info:id = "RealityKit.InputTarget"');
534
- };
535
- model.add(empty);
536
- }
537
-
538
- addInputTargetComponent(selfModel);
539
- if (this.toggleModel) addInputTargetComponent(this.toggleModel);
540
-
541
524
  // Ensure initial states are set correctly so that we get the same result as was currently active in the runtime
542
525
  const objectsToHide = new Array<USDObject>();
543
526
  if (!this.targetStateBeforeCreatingDocument)
@@ -713,7 +696,7 @@
713
696
  // TODO use play "type" which can be start/stop/pause
714
697
  if (this.toggleOnClick) (playAction as ActionModel).multiplePerformOperation = "stop";
715
698
  const playClipOnTap = new BehaviorModel("playAudio" + behaviorName,
716
- TriggerBuilder.tapTrigger(this.gameObject),
699
+ TriggerBuilder.tapTrigger(model),
717
700
  playAction,
718
701
  );
719
702
  ext.addBehavior(playClipOnTap);
src/engine-components/export/usdz/extensions/behavior/BehavioursBuilder.ts CHANGED
@@ -176,6 +176,21 @@
176
176
  }
177
177
  }
178
178
 
179
+ /** Adds `RealityKit.InputTarget` child prim to enable Vision OS direct/indirect interactions
180
+ (by default, only indirect interactions are allowed) */
181
+ function addInputTargetComponent(model: USDObject, options: { direct: boolean, indirect: boolean } = { direct: true, indirect: true }) {
182
+ const empty = USDObject.createEmpty();
183
+ empty.name = "InputTarget_" + empty.name;
184
+ empty.displayName = undefined;
185
+ empty.type = "RealityKitComponent";
186
+ empty.onSerialize = (writer: USDWriter) => {
187
+ writer.appendLine("bool allowsDirectInput = " + (options.direct ? 1 : 0));
188
+ writer.appendLine("bool allowsIndirectInput = " + (options.indirect ? 1 : 0));
189
+ writer.appendLine('uniform token info:id = "RealityKit.InputTarget"');
190
+ };
191
+ model.add(empty);
192
+ }
193
+
179
194
  export class TriggerBuilder {
180
195
 
181
196
  static sceneStartTrigger(): TriggerModel {
@@ -186,8 +201,22 @@
186
201
  return trigger;
187
202
  }
188
203
 
189
- static tapTrigger(targetObject: Target): TriggerModel {
204
+ /** Trigger that fires when an object has been tapped/clicked.
205
+ * @param targetObject The object or list of objects that can be interacted with.
206
+ * @param inputMode Input Mode (direct and/or indirect). Only available for USDObject targets. Only supported on Vision OS at the moment. */
207
+ static tapTrigger(targetObject: Target, inputMode: { direct: boolean, indirect: boolean } = { direct: true, indirect: true }): TriggerModel {
190
208
  const trigger = new TriggerModel(targetObject);
209
+ if (Array.isArray(targetObject) && targetObject.length > 1) {
210
+ for (const obj of targetObject) {
211
+ if (!(obj instanceof USDObject)) continue;
212
+ addInputTargetComponent(obj, inputMode);
213
+ }
214
+ }
215
+ else {
216
+ if (targetObject instanceof USDObject) {
217
+ addInputTargetComponent(targetObject, inputMode);
218
+ }
219
+ }
191
220
  trigger.tokenId = "TapGesture";
192
221
  return trigger;
193
222
  }
src/engine/engine_utils.ts CHANGED
@@ -562,9 +562,12 @@
562
562
  return standalone && !isHololens && !isiOS();
563
563
  }
564
564
 
565
+ let _ismobile: boolean | undefined;
565
566
  /** @returns `true` if it's a phone or tablet */
566
567
  export function isMobileDevice() {
567
- return (typeof window.orientation !== "undefined") || (navigator.userAgent.indexOf('IEMobile') !== -1);
568
+ if(_ismobile !== undefined) return _ismobile;
569
+ _ismobile = /iPhone|iPad|iPod|Android|IEMobile/i.test(navigator.userAgent);
570
+ return _ismobile;
568
571
  }
569
572
 
570
573
  export function isAndroidDevice() {