Needle Engine

Changes between version 3.40.0-alpha.1 and 3.40.0-alpha.2
Files changed (4) hide show
  1. plugins/vite/build-pipeline.js +41 -8
  2. src/engine/engine_scenetools.ts +46 -4
  3. src/engine-components/NeedleMenu.ts +23 -11
  4. plugins/types/userconfig.d.ts +7 -2
plugins/vite/build-pipeline.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { ChildProcess, exec, execSync, spawn } from 'child_process';
2
2
  import { getOutputDirectory, loadConfig, tryLoadProjectConfig } from './config.js';
3
- import { existsSync, readdirSync } from 'fs';
3
+ import { existsSync, readFileSync, readdirSync, writeFileSync } from 'fs';
4
4
 
5
5
  // see https://linear.app/needle/issue/NE-3798
6
6
 
@@ -12,17 +12,30 @@
12
12
  * @returns {import('vite').Plugin}
13
13
  */
14
14
  export const needleBuildPipeline = async (command, config, userSettings) => {
15
+
15
16
  // we only want to run compression here if this is a distribution build
16
17
  // this is handled however in the `apply` hook
17
18
  if (userSettings.noBuildPipeline) return;
18
19
 
19
- const meta = await loadConfig();
20
+ const packageJsonPath = process.cwd() + "/package.json";
21
+ await fixPackageJson(packageJsonPath);
22
+
23
+
20
24
  let shouldRun = false;
21
- if (meta && meta.developmentBuild === false) {
25
+ const productionArgument = process.argv.indexOf("--production");
26
+ if (productionArgument >= 0) {
22
27
  shouldRun = true;
23
28
  }
29
+ else {
30
+ const meta = await loadConfig();
31
+ if (meta && meta.developmentBuild === false) {
32
+ shouldRun = true;
33
+ }
34
+ }
35
+
24
36
  if (!shouldRun) {
25
- log("Skipping build pipeline because this is a development build");
37
+ log("Skipping build pipeline because this is a development build.\n- Invoke with `--production` to run the build pipeline.\n- For example \"vite build -- --production\".");
38
+ await new Promise((resolve, _) => setTimeout(resolve, 1000));
26
39
  return;
27
40
  }
28
41
 
@@ -36,7 +49,7 @@
36
49
  apply: 'build',
37
50
  buildEnd() {
38
51
  // start the compression process once vite is done copying the files
39
- task = invokeBuildPipeline().then((res) => {
52
+ task = invokeBuildPipeline(userSettings).then((res) => {
40
53
  taskFinished = true;
41
54
  taskSucceeded = res;
42
55
  });
@@ -57,6 +70,25 @@
57
70
  }
58
71
  }
59
72
 
73
+ /**
74
+ * Previously we did always install the build pipeline and run an extra command to invoke the build pipeline.
75
+ * This is now done automatically by the needle build pipeline plugin - so we update all legacy projects to use the new method.
76
+ */
77
+ async function fixPackageJson(packageJsonPath) {
78
+ if (!existsSync(packageJsonPath)) {
79
+ return;
80
+ }
81
+ const text = readFileSync(packageJsonPath, "utf8");
82
+ const oldScript = `"build:production": "npm run build:dev && npm run gltf:transform"`;
83
+ const newScript = `"build:production": "vite build -- --production"`;
84
+ const fixed = text.replace(oldScript, newScript);
85
+ if (fixed === text) return;
86
+ log("Automatically updated package.json production build script");
87
+ log("- FROM " + oldScript);
88
+ log("- TO " + newScript);
89
+ writeFileSync(packageJsonPath, fixed);
90
+ }
91
+
60
92
  function log(...args) {
61
93
  console.log("[needle-buildpipeline]", ...args);
62
94
  }
@@ -65,7 +97,7 @@
65
97
  }
66
98
 
67
99
  /**
68
- * @param {{}} opts
100
+ * @param {import('../types').userSettings} opts
69
101
  * @returns {Promise<boolean>}
70
102
  */
71
103
  async function invokeBuildPipeline(opts) {
@@ -106,8 +138,9 @@
106
138
  sub = exec(cmd, { cwd: installPath });
107
139
  }
108
140
  else {
109
- const cmd = `npx --yes @needle-tools/gltf-build-pipeline@latest transform "${outputDirectory}"`;
110
- log("Running command \"" + cmd + "\" at " + process.cwd() + "...");
141
+ const version = opts.buildPipelineVersion || "latest";
142
+ const cmd = `npx --yes @needle-tools/gltf-build-pipeline@${version} transform "${outputDirectory}"`;
143
+ log("Running command \"" + cmd);
111
144
  sub = exec(cmd);
112
145
  }
113
146
  sub.stdout.on('data', data => {
src/engine/engine_scenetools.ts CHANGED
@@ -1,5 +1,7 @@
1
- import { Camera, Object3D, Scene } from "three";
1
+ import { Camera, Mesh, MeshPhongMaterial, MeshStandardMaterial, Object3D, ObjectLoader, Scene } from "three";
2
+ import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader.js'
2
3
  import { type GLTF, GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
4
+ import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js'
3
5
 
4
6
  import { showBalloonMessage } from "./debug/index.js";
5
7
  import { getLoader, type INeedleGltfLoader, registerLoader } from "./engine_gltf.js";
@@ -92,7 +94,14 @@
92
94
  await getLoader().createBuiltinComponents(context, gltfId, gltf, seed, componentsExtension);
93
95
  }
94
96
 
95
- export async function createGLTFLoader(url: string, context: Context) {
97
+ export async function createLoader(url: string, context: Context): Promise<GLTFLoader | FBXLoader> {
98
+ const ext = (url.split("?")[0].split(".").pop() || "").toLowerCase();
99
+ switch (ext) {
100
+ case "fbx":
101
+ return new FBXLoader();
102
+ case "obj":
103
+ return new OBJLoader();
104
+ }
96
105
  const loader = new GLTFLoader();
97
106
  await registerExtensions(loader, context, url);
98
107
  return loader;
@@ -111,7 +120,17 @@
111
120
  path = "";
112
121
  }
113
122
  if (printGltf) console.log("Parse glTF", path)
114
- const loader = await createGLTFLoader(path, context);
123
+ const loader = await createLoader(path, context);
124
+
125
+ if (!(loader instanceof GLTFLoader)) {
126
+ const res = loader.parse(data, path);
127
+ return {
128
+ animations: res.animations,
129
+ scene: res,
130
+ scenes: [res]
131
+ } as GLTF;
132
+ }
133
+
115
134
  const componentsExtension = registerComponentExtension(loader);
116
135
  return new Promise((resolve, reject) => {
117
136
  try {
@@ -172,7 +191,30 @@
172
191
  // we need to make sure the extensions dont override each other
173
192
  // creating new loaders should not be expensive as well
174
193
  checkIfUserAttemptedToLoadALocalFile(url)
175
- const loader = await createGLTFLoader(url, context);
194
+ const loader = await createLoader(url, context);
195
+
196
+ if (!(loader instanceof GLTFLoader)) {
197
+ const res = await loader.loadAsync(url, prog);
198
+ res.traverseVisible(obj => {
199
+ // HACK: for FBXLoader loading all materials as MeshPhongMaterial
200
+ if (obj instanceof Mesh && obj.material instanceof MeshPhongMaterial) {
201
+ const prev = obj.material;
202
+ obj["material:source"] = prev;
203
+ obj.material = new MeshStandardMaterial();
204
+ for (const key of Object.keys(prev)) {
205
+ if (key === "type") continue;
206
+ if (obj.material[key] !== undefined)
207
+ obj.material[key] = prev[key];
208
+ }
209
+ }
210
+ });
211
+ return {
212
+ animations: res.animations,
213
+ scene: res,
214
+ scenes: [res]
215
+ } as GLTF;
216
+ }
217
+
176
218
  const componentsExtension = registerComponentExtension(loader);
177
219
  return new Promise((resolve, reject) => {
178
220
  try {
src/engine-components/NeedleMenu.ts CHANGED
@@ -15,20 +15,31 @@
15
15
  @serializable()
16
16
  showNeedleLogo: boolean = true;
17
17
 
18
- /** When enabled the menu will also be visible in VR/AR when you look up */
18
+ /** When enabled the menu will also be visible in VR/AR when you look up
19
+ * @default undefined
20
+ */
19
21
  @serializable()
20
- showSpatialMenu: boolean = true;
22
+ showSpatialMenu?: boolean;
21
23
 
22
- /** When enabled a button to enter fullscreen will be added to the menu */
24
+ /** When enabled a button to enter fullscreen will be added to the menu
25
+ * @default undefined
26
+ */
23
27
  @serializable()
24
- createFullscreenButton: boolean = true;
25
- /** When enabled a button to mute the application will be added to the menu */
28
+ createFullscreenButton?: boolean;
29
+ /** When enabled a button to mute the application will be added to the menu
30
+ * @default undefined
31
+ */
26
32
  @serializable()
27
- createMuteButton: boolean = true;
33
+ createMuteButton?: boolean;
28
34
 
35
+ /**
36
+ * When enabled a button to show a QR code will be added to the menu.
37
+ * @default undefined
38
+ */
29
39
  @serializable()
30
- createQRCodeButton: boolean = true;
40
+ createQRCodeButton?: boolean;
31
41
 
42
+ /** @hidden */
32
43
  onEnable() {
33
44
  this.applyOptions();
34
45
  }
@@ -37,12 +48,13 @@
37
48
  applyOptions() {
38
49
  this.context.menu.setPosition(this.position);
39
50
  this.context.menu.showNeedleLogo(this.showNeedleLogo);
40
- if (this.createFullscreenButton)
51
+ if (this.createFullscreenButton === true)
41
52
  this.context.menu.showFullscreenOption(true);
42
- if (this.createMuteButton)
53
+ if (this.createMuteButton === true)
43
54
  this.context.menu.showAudioPlaybackOption(true);
44
- this.context.menu.showSpatialMenu(this.showSpatialMenu);
45
- if (this.createQRCodeButton) {
55
+ if (this.showSpatialMenu === true)
56
+ this.context.menu.showSpatialMenu(this.showSpatialMenu);
57
+ if (this.createQRCodeButton === true) {
46
58
  if (!isMobileDevice()) {
47
59
  this.context.menu.showQRCodeButton(true);
48
60
  }
plugins/types/userconfig.d.ts CHANGED
@@ -34,10 +34,15 @@
34
34
  logModuleImportChains: boolean;
35
35
 
36
36
  /** Set to true to disable generating the buildinfo.json file in your output directory */
37
- noBuildInfo: boolean;
37
+ noBuildInfo?: boolean;
38
38
 
39
39
  /** Set to true to disable the needle build pipeline (running compression and optimization as a postprocessing step on the exported glTF files) */
40
- noBuildPipeline: boolean;
40
+ noBuildPipeline?: boolean;
41
+ /** Set to a specific version of the Needle Build Pipeline.
42
+ * @default "latest"
43
+ * @example "2.2.0-alpha"
44
+ */
45
+ buildPipelineVersion?: string;
41
46
 
42
47
  /** required for @serializable https://github.com/vitejs/vite/issues/13736 */
43
48
  vite44Hack: boolean;