Needle Engine

Changes between version 3.40.0-alpha.4 and 3.40.0-alpha.5
Files changed (7) hide show
  1. src/engine/debug/debug.ts +0 -2
  2. src/engine/engine_scenetools.ts +54 -7
  3. src/engine/engine_serialization_builtin_serializer.ts +9 -2
  4. src/engine/engine_utils.ts +5 -3
  5. src/engine/debug/index.ts +2 -1
  6. src/engine-components/OrbitControls.ts +1 -1
  7. src/engine/engine_utils_format.ts +74 -0
src/engine/debug/debug.ts CHANGED
@@ -1,9 +1,7 @@
1
1
  import { isLocalNetwork } from "../engine_networking_utils.js";
2
2
  import { getParam } from "../engine_utils.js";
3
- import { showDebugConsole } from "./debug_console.js";
4
3
  import { addLog, clearMessages, LogType, setAllowBalloonMessages, setAllowOverlayMessages } from "./debug_overlay.js";
5
4
 
6
- export { showDebugConsole }
7
5
  export {
8
6
  clearMessages as clearBalloonMessages,
9
7
  /** @deprecated use clearBalloonMessages instead */
src/engine/engine_scenetools.ts CHANGED
@@ -1,7 +1,7 @@
1
- import { Camera, Mesh, MeshPhongMaterial, MeshStandardMaterial, Object3D, ObjectLoader, Scene } from "three";
1
+ import { Camera, Mesh, MeshPhongMaterial, MeshStandardMaterial, Object3D, Scene } from "three";
2
2
  import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader.js'
3
3
  import { type GLTF, GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
4
- import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js'
4
+ import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js';
5
5
 
6
6
  import { showBalloonMessage } from "./debug/index.js";
7
7
  import { getLoader, type INeedleGltfLoader, registerLoader } from "./engine_gltf.js";
@@ -15,6 +15,7 @@
15
15
  import * as utils from "./engine_utils.js";
16
16
  import { invokeAfterImportPluginHooks, registerComponentExtension, registerExtensions } from "./extensions/extensions.js";
17
17
  import { NEEDLE_components } from "./extensions/NEEDLE_components.js";
18
+ import { FileType, tryDetermineFileTypeFromBinary, tryDetermineFileTypeFromURL } from "./engine_utils_format.js"
18
19
 
19
20
  /** @internal */
20
21
  export class NeedleGltfLoader implements INeedleGltfLoader {
@@ -94,17 +95,43 @@
94
95
  await getLoader().createBuiltinComponents(context, gltfId, gltf, seed, componentsExtension);
95
96
  }
96
97
 
97
- export async function createLoader(url: string, context: Context): Promise<GLTFLoader | FBXLoader> {
98
- const ext = (url.split("?")[0].split(".").pop() || "").toLowerCase();
98
+ async function determineFileTypeFromURL(url: string): Promise<FileType> {
99
+
100
+ const ext = url.split("?")[0].split(".").pop()?.toUpperCase() || "";
99
101
  switch (ext) {
102
+ case "GLTF":
103
+ return "gltf";
104
+ case "GLB":
105
+ return "glb";
106
+ case "FBX":
107
+ return "fbx";
108
+ }
109
+
110
+ return await tryDetermineFileTypeFromURL(url) || "unknown";
111
+ }
112
+
113
+ export async function createLoader(url: string, context: Context): Promise<GLTFLoader | FBXLoader | OBJLoader | null> {
114
+
115
+ const type = await determineFileTypeFromURL(url);
116
+
117
+ switch (type) {
118
+ case "unknown":
119
+ console.warn("Unknown file type:", url);
120
+ return new GLTFLoader();
100
121
  case "fbx":
101
122
  return new FBXLoader();
102
123
  case "obj":
103
124
  return new OBJLoader();
125
+ case "usdz":
126
+ console.warn("USDZ is not supported yet...");
127
+ return null;
128
+ default:
129
+ case "gltf":
130
+ case "glb":
131
+ const loader = new GLTFLoader();
132
+ await registerExtensions(loader, context, url);
133
+ return loader;
104
134
  }
105
- const loader = new GLTFLoader();
106
- await registerExtensions(loader, context, url);
107
- return loader;
108
135
  }
109
136
 
110
137
  /** Load a gltf file from a url. This is the core method used by Needle Engine to load gltf files. All known extensions are registered here.
@@ -121,7 +148,23 @@
121
148
  }
122
149
  if (printGltf) console.log("Parse glTF", path)
123
150
  const loader = await createLoader(path, context);
151
+ if (!loader) {
152
+ return undefined;
153
+ }
124
154
 
155
+ // Handle OBJ Loader
156
+ if (loader instanceof OBJLoader) {
157
+ if (typeof data !== "string") {
158
+ data = new TextDecoder().decode(data);
159
+ }
160
+ const res = loader.parse(data);
161
+ return {
162
+ animations: res.animations,
163
+ scene: res,
164
+ scenes: [res]
165
+ } as GLTF;
166
+ }
167
+ // Handle any other loader that is not a GLTFLoader
125
168
  if (!(loader instanceof GLTFLoader)) {
126
169
  const res = loader.parse(data, path);
127
170
  return {
@@ -192,7 +235,11 @@
192
235
  // creating new loaders should not be expensive as well
193
236
  checkIfUserAttemptedToLoadALocalFile(url)
194
237
  const loader = await createLoader(url, context);
238
+ if (!loader) {
239
+ return undefined;
240
+ }
195
241
 
242
+ // Handle any loader that is not a GLTFLoader
196
243
  if (!(loader instanceof GLTFLoader)) {
197
244
  const res = await loader.loadAsync(url, prog);
198
245
  res.traverseVisible(obj => {
src/engine/engine_serialization_builtin_serializer.ts CHANGED
@@ -10,6 +10,7 @@
10
10
  import { SerializationContext, TypeSerializer } from "./engine_serialization_core.js";
11
11
  import { RenderTexture } from "./engine_texture.js";
12
12
  import { resolveUrl } from "./engine_utils.js";
13
+ import { IComponent } from "./engine_types.js";
13
14
 
14
15
  // export class SourcePath {
15
16
  // src?:string
@@ -132,8 +133,14 @@
132
133
  console.warn("Could not resolve object reference", context.path, data, context.target, context.context.scene);
133
134
  data["could_not_resolve"] = true;
134
135
  }
135
- else if (debugExtension)
136
- console.log("Deserialized object reference?", data, res, context?.nodeToObject);
136
+ else {
137
+ if (res && (res as IComponent).isComponent === true) {
138
+ if(debugExtension) console.warn("Deserialized object reference is a component");
139
+ res = (res as IComponent).gameObject;
140
+ }
141
+ if (debugExtension)
142
+ console.log("Deserialized object reference?", data, res, context?.nodeToObject);
143
+ }
137
144
  return res;
138
145
  }
139
146
  }
src/engine/engine_utils.ts CHANGED
@@ -565,9 +565,11 @@
565
565
  let _ismobile: boolean | undefined;
566
566
  /** @returns `true` if it's a phone or tablet */
567
567
  export function isMobileDevice() {
568
- if(_ismobile !== undefined) return _ismobile;
569
- _ismobile = /iPhone|iPad|iPod|Android|IEMobile/i.test(navigator.userAgent);
570
- return _ismobile;
568
+ if (_ismobile !== undefined) return _ismobile;
569
+ if ((typeof window.orientation !== "undefined") || (navigator.userAgent.indexOf('IEMobile') !== -1)) {
570
+ return _ismobile = true;
571
+ }
572
+ return _ismobile = /iPhone|iPad|iPod|Android|IEMobile/i.test(navigator.userAgent);
571
573
  }
572
574
 
573
575
  export function isAndroidDevice() {
src/engine/debug/index.ts CHANGED
@@ -1,1 +1,2 @@
1
- export * from "./debug.js";
1
+ export * from "./debug.js";
2
+ export * from "./debug_console.js";
src/engine-components/OrbitControls.ts CHANGED
@@ -350,7 +350,7 @@
350
350
  private _clickOnBackgroundCount: number = 0;
351
351
  private _onPointerDown = (evt: NEPointerEvent) => {
352
352
 
353
- if (this.clickBackgroundToFitScene > 0 && evt.isClick) {
353
+ if (this.clickBackgroundToFitScene > 0 && evt.isClick && evt.button === 0) {
354
354
 
355
355
  // it's possible that we didnt raycast in this frame
356
356
  if (!evt.hasRay) {
src/engine/engine_utils_format.ts ADDED
@@ -0,0 +1,74 @@
1
+ import { getParam } from "./engine_utils";
2
+
3
+ const debug = getParam("debugfileformat");
4
+
5
+ export declare type FileType = "gltf" | "glb" | "fbx" | "obj" | "usdz" | "unknown";
6
+
7
+
8
+ export async function tryDetermineFileTypeFromURL(url: string): Promise<FileType> {
9
+
10
+ // If the URL doesnt contain a filetype we need to check the header
11
+ // This is the case for example if we load a file from a data url
12
+ const header = await fetch(url, {
13
+ method: "GET",
14
+ headers: {
15
+ "range": "bytes=0-32"
16
+ }
17
+ }).catch(_ => {
18
+ return null;
19
+ });
20
+
21
+ if (header?.ok) {
22
+ const data = await header.arrayBuffer();
23
+ const res = tryDetermineFileTypeFromBinary(data);
24
+ if(debug) console.log("Determined file type from header", res);
25
+ return res;
26
+ }
27
+
28
+ return "unknown";
29
+ }
30
+
31
+
32
+ /** Attempts to determine the file type of a binary file by looking at the first few bytes of the file.
33
+ * @hidden
34
+ */
35
+ export function tryDetermineFileTypeFromBinary(data: ArrayBuffer): FileType {
36
+
37
+ if (data.byteLength < 4) {
38
+ return "unknown";
39
+ }
40
+
41
+ const bytes = new Uint8Array(data);
42
+
43
+ if (debug) {
44
+ console.warn("Trying to determine file type from binary data\n", "\"" + new TextDecoder().decode(data) + "\"\n", bytes);
45
+ }
46
+
47
+ // USDZ
48
+ if (bytes[0] == 80 && bytes[1] == 75 && bytes[2] == 3 && bytes[3] == 4) {
49
+ return "usdz";
50
+ }
51
+ // FBX
52
+ if (bytes[0] == 75 && bytes[1] == 97 && bytes[2] == 121 && bytes[3] == 100 && bytes[4] == 97 && bytes[5] == 114 && bytes[6] == 97 && bytes[7] == 32) {
53
+ return "fbx";
54
+ }
55
+ // GLTF or GLB
56
+ else if (bytes[0] == 103 && bytes[1] == 108 && bytes[2] == 84 && bytes[3] == 70) {
57
+ return "glb";
58
+ }
59
+ // OBJ - in this case exported from blender it starts with "# Blender" - we only check the first 10 bytes, technically it could still be a different file so we should do this check at the end
60
+ else if (bytes[0] == 35 && bytes[1] == 32 && bytes[2] == 66 && bytes[3] == 108 && bytes[4] == 101 && bytes[5] == 110 && bytes[6] == 100 && bytes[7] == 101 && bytes[8] == 114 && bytes[9] == 32) {
61
+ // const text = new TextDecoder().decode(data.slice(0, 9));
62
+ return "obj";
63
+ }
64
+
65
+ // const text = new TextDecoder().decode(data);
66
+ // if (text.startsWith("Kaydara FBX")) {
67
+ // return "fbx";
68
+ // }
69
+ // else if (text.startsWith("glTF")) {
70
+ // return "gltf";
71
+ // }
72
+
73
+ return "unknown";
74
+ }