@needle-tools/engine 4.11.0-beta → 4.11.0-next.8bfb2f5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,129 @@
1
+
2
+
3
+
4
+
5
+ /**
6
+ * @type {() => import("vite").Plugin}
7
+ */
8
+ export function viteFixWorkerImport() {
9
+ return {
10
+ name: 'vite-rewriteWorkerImport',
11
+ config: function (config, env) {
12
+ if (!config.build) {
13
+ config.build = {};
14
+ }
15
+ if (!config.build.rollupOptions) {
16
+ config.build.rollupOptions = {};
17
+ }
18
+ if (!config.build.rollupOptions.plugins) {
19
+ config.build.rollupOptions.plugins = [];
20
+ }
21
+ if (!Array.isArray(config.build.rollupOptions.plugins)) {
22
+ const value = config.build.rollupOptions.plugins;
23
+ config.build.rollupOptions.plugins = [];
24
+ config.build.rollupOptions.plugins.push(value);
25
+ }
26
+ config.build.rollupOptions.plugins.push(rollupFixWorkerImport({ logFail: false }));
27
+ }
28
+ }
29
+ }
30
+
31
+
32
+
33
+ // https://regex101.com/r/hr01H4/1
34
+ const regex = /new\s+Worker\s*\(\s*new\s+URL\s*\(\s*(?:\/\*.*?\*\/\s*)?\"(?<url>[^"]+)\"\s*,\s*(?<base>import\.meta\.url|self\.location)[^)]*\)/gm;
35
+
36
+
37
+
38
+ /**
39
+ * @type {(opts?: {logFail:boolean}) => import("vite").Plugin}
40
+ */
41
+ export function rollupFixWorkerImport(opts = { logFail: true }) {
42
+ return {
43
+ name: 'rewriteWorkerImport',
44
+ renderChunk: {
45
+ order: 'post',
46
+ async handler(code, chunk, outputOptions) {
47
+ let regexMatchedWorkerCode = false;
48
+ const newWorkerStartIndex = code.indexOf("new Worker");
49
+ if (newWorkerStartIndex >= 0) {
50
+ const res = code.replace(regex, (match, url, _base) => {
51
+ regexMatchedWorkerCode = true;
52
+ // console.log("WORKER?", url)
53
+ if (url?.startsWith("/")) {
54
+ console.log(`[rollup] Rewrite worker import in ${chunk.fileName}`);
55
+ // Make url file-relative
56
+ const newUrl = url.replace(/^\//, "");
57
+ // For CORS issues we need to use importScripts: https://linear.app/needle/issue/NE-6572#comment-ea5dc65e
58
+ const output = `/* new-worker */ new Worker(URL.createObjectURL(new Blob(["import '" + \`\${new URL('./${newUrl}', import.meta.url).toString()}\` + "';"], { type: 'text/javascript' }))`;
59
+ console.log("[rollup] Did rewrite worker output to:", output);
60
+ return output;
61
+ // return `new Worker(new URL("./${newUrl}", import.meta.url)`;
62
+ }
63
+ return match;
64
+ });
65
+ if (!regexMatchedWorkerCode) {
66
+
67
+ const fixedCode = fixWorkerSelfLocation(chunk.fileName, code);
68
+ if (fixedCode !== code) {
69
+ return fixedCode;
70
+ }
71
+ if (opts?.logFail !== false) {
72
+ const str = `[...]${code.substring(newWorkerStartIndex, newWorkerStartIndex + 200)}[...]`
73
+ console.warn(`\n[rollup] Worker import in ${chunk.fileName} was not rewritten: ${str}`);
74
+ }
75
+ }
76
+ return res;
77
+ }
78
+ },
79
+ }
80
+ };
81
+ }
82
+
83
+
84
+ /**
85
+ * Fix worker self.location to import.meta.url
86
+ * @param {string} filename
87
+ * @param {string} code
88
+ */
89
+ function fixWorkerSelfLocation(filename, code) {
90
+ let lastIndex = 0;
91
+ while (true) {
92
+ const startIndex = code.indexOf("new Worker", lastIndex);
93
+ if (startIndex < 0) break;
94
+ let index = startIndex + 1;
95
+ let endIndex = -1;
96
+ let openingBraceCount = 0;
97
+ let foundAnyOpening = false;
98
+ while (true) {
99
+ const char = code[index];
100
+ if (char === "(") {
101
+ openingBraceCount++;
102
+ foundAnyOpening = true;
103
+ }
104
+ if (char === ")") openingBraceCount--;
105
+ if (openingBraceCount === 0 && foundAnyOpening) {
106
+ endIndex = index;
107
+ break;
108
+ }
109
+ // console.log(openingBraceCount, char, index, code.length);
110
+ index++;
111
+ if (index >= code.length) break;
112
+ }
113
+ if (endIndex > startIndex) {
114
+ const workerCode = code.substring(startIndex, endIndex + 1);
115
+ if (workerCode.indexOf("self.location") >= 0) {
116
+ const fixedCode = workerCode.replace("self.location", "import.meta.url");
117
+ code = code.substring(0, startIndex) + fixedCode + code.substring(endIndex + 1);
118
+ lastIndex = startIndex + fixedCode.length;
119
+ console.log(`[rollup] Rewrite worker 'self.location' to 'import.meta.url' in ${filename}`);
120
+ } else {
121
+ lastIndex = endIndex;
122
+ }
123
+ }
124
+ else {
125
+ lastIndex = startIndex + "new Worker".length;
126
+ }
127
+ }
128
+ return code;
129
+ }
@@ -1,5 +1,6 @@
1
1
  import { existsSync, readdirSync, readFileSync, writeFileSync } from 'fs';
2
2
  import path from 'path';
3
+ import { tryParseNeedleEngineSrcAttributeFromHtml } from '../common/needle-engine.js';
3
4
  import { preloadScriptPaths } from './dependencies.js';
4
5
  import { makeFilesLocalIsEnabled } from './local-files.js';
5
6
 
@@ -159,10 +160,6 @@ function generateScriptPreloadLinks(_config, tags) {
159
160
  // @ts-ignore
160
161
  const codegenRegex = /\"(?<gltf>.+(.glb|.gltf)(\?.*)?)\"/gm;
161
162
 
162
- // https://regex101.com/r/SVhzzD/1
163
- // @ts-ignore
164
- const needleEngineRegex = /<needle-engine.*?src=["'](?<src>[\w\d]+?)["']>/gm;
165
-
166
163
  /**
167
164
  * @param {import('../types').needleConfig} config
168
165
  * @param {string} html
@@ -171,25 +168,10 @@ const needleEngineRegex = /<needle-engine.*?src=["'](?<src>[\w\d]+?)["']>/gm;
171
168
  function generateGltfPreloadLinks(config, html, tags) {
172
169
 
173
170
  // TODO: try to get the <needle-engine src> element src attribute and preload that
174
- const needleEngineMatches = html.matchAll(needleEngineRegex);
175
- if (needleEngineMatches) {
176
- while (true) {
177
- const match = needleEngineMatches.next();
178
- if (match.done) break;
179
- /** @type {undefined | null | string} */
180
- const value = match.value?.groups?.src;
181
- if (value) {
182
- if (value.startsWith("[")) {
183
- // we have an array assigned
184
- const arr = JSON.parse(value);
185
- for (const item of arr) {
186
- insertPreloadLink(tags, item, "model/gltf+json");
187
- }
188
- }
189
- else {
190
- insertPreloadLink(tags, value, "model/gltf+json");
191
- }
192
- }
171
+ const needleEngineMatches = tryParseNeedleEngineSrcAttributeFromHtml(html);
172
+ if (needleEngineMatches?.length) {
173
+ for (const item of needleEngineMatches) {
174
+ insertPreloadLink(tags, item, "model/gltf+json");
193
175
  }
194
176
  }
195
177
 
@@ -8,23 +8,26 @@ export const preloadScriptPaths = [];
8
8
 
9
9
  /**
10
10
  * @param {import('../types').userSettings} userSettings
11
+ * @returns {import('vite').Plugin[]}
11
12
  */
12
13
  export const needleDependencies = (command, config, userSettings) => {
13
14
 
14
15
  /**
15
16
  * @type {import('vite').Plugin}
16
17
  */
17
- return {
18
- name: 'needle:dependencies',
19
- enforce: 'pre',
20
- /**
21
- * @param {import('vite').UserConfig} config
22
- */
23
- config: (config, env) => {
24
- handleOptimizeDeps(config);
25
- handleManualChunks(config);
26
- }
27
- }
18
+ return [
19
+ {
20
+ name: 'needle:dependencies',
21
+ enforce: 'pre',
22
+ /**
23
+ * @param {import('vite').UserConfig} config
24
+ */
25
+ config: (config, env) => {
26
+ handleOptimizeDeps(config);
27
+ handleManualChunks(config);
28
+ },
29
+ },
30
+ ]
28
31
  }
29
32
 
30
33
  const excludeDependencies = [
@@ -167,6 +170,13 @@ function handleManualChunks(config) {
167
170
  return name;
168
171
  }
169
172
  }
173
+ // else if(chunk.name === 'index') {
174
+ // console.log(chunk);
175
+ // debugger
176
+ // // this is the main chunk
177
+ // // we don't want to add a hash here to be able to easily import the main script
178
+ // return `index.js`;
179
+ // }
170
180
  return `assets/[name].[hash].js`;
171
181
  }
172
182
 
@@ -68,6 +68,8 @@ import { needleNPM } from "./npm.js";
68
68
  import { needleTransformCode } from "./transform.js";
69
69
  import { needleMaterialXLoader } from "./materialx.js";
70
70
  import { needleLogger } from "./logger.js";
71
+ import { needleApp } from "./needle-app.js";
72
+ import { viteFixWorkerImport } from "../common/worker.js";
71
73
  export { needleServer } from "./server.js";
72
74
 
73
75
 
@@ -137,6 +139,8 @@ export const needlePlugins = async (command, config = undefined, userSettings =
137
139
  needleServer(command, config, userSettings),
138
140
  needleNPM(command, config, userSettings),
139
141
  needleMaterialXLoader(command, config, userSettings),
142
+ needleApp(command, config, userSettings),
143
+ viteFixWorkerImport()
140
144
  ];
141
145
 
142
146
  const asap = await needleAsap(command, config, userSettings);
@@ -0,0 +1,148 @@
1
+ import { writeFile } from 'fs';
2
+ import { tryParseNeedleEngineSrcAttributeFromHtml } from '../common/needle-engine.js';
3
+
4
+
5
+
6
+ /**
7
+ * @param {'serve' | 'build'} command
8
+ * @param {{} | undefined | null} config
9
+ * @param {import('../types').userSettings} userSettings
10
+ * @returns {import('vite').Plugin[] | null}
11
+ */
12
+ export const needleApp = (command, config, userSettings) => {
13
+
14
+ if (command !== "build") {
15
+ return null;
16
+ }
17
+
18
+ /** @type {Array<import("rollup").OutputChunk>} */
19
+ const entryFiles = new Array();
20
+
21
+ let outputDir = "dist";
22
+
23
+ /**
24
+ * @type {import('vite').Plugin}
25
+ */
26
+ return [
27
+ {
28
+ name: 'needle:app',
29
+ enforce: "post",
30
+ configResolved(config) {
31
+ outputDir = config.build.outDir || "dist";
32
+ },
33
+ transformIndexHtml: {
34
+ handler: async function (html, context) {
35
+ const name = context.filename;
36
+ if (name.includes("index.html")) {
37
+ if (context.chunk?.isEntry) {
38
+ try {
39
+ entryFiles.push(context.chunk);
40
+ const path = context.chunk.fileName;
41
+ // console.log("[needle-dependencies] entry chunk imports", {
42
+ // name: context.chunk.fileName,
43
+ // imports: context.chunk.imports,
44
+ // dynamicImports: context.chunk.dynamicImports,
45
+ // refs: context.chunk.referencedFiles,
46
+ // });
47
+
48
+ const referencedGlbs = tryParseNeedleEngineSrcAttributeFromHtml(html);
49
+ const webComponent = generateNeedleEmbedWebComponent(path, referencedGlbs);
50
+ await writeFile(`${outputDir}/needle-app.js`, webComponent, (err) => {
51
+ if (err) {
52
+ console.error("[needle-app] could not create needle-app.js", err);
53
+ }
54
+ else {
55
+ console.log("[needle-app] created needle-app.js");
56
+ }
57
+ });
58
+ }
59
+ catch (e) {
60
+ console.warn("WARN: could not create needle-app.js\n", e);
61
+ }
62
+ }
63
+ }
64
+ }
65
+ },
66
+
67
+ }
68
+ ]
69
+ }
70
+
71
+
72
+ /**
73
+ * @param {string} filepath
74
+ * @param {string[]} needleEngineSrcPaths
75
+ * @returns {string}
76
+ */
77
+ function generateNeedleEmbedWebComponent(filepath, needleEngineSrcPaths) {
78
+
79
+
80
+ // filepath is e.g. `assets/index-XXXXXXXX.js`
81
+ // we want to make sure the path is correct relative to where the component will be used
82
+ // this script will be emitted in the output directory root (e.g. needle-embed.js)
83
+
84
+ const src = needleEngineSrcPaths?.length ? `${needleEngineSrcPaths[0]}` : "";
85
+
86
+ const componentName = 'needle-app';
87
+ const className = 'NeedleApp';
88
+
89
+ return `
90
+ class ${className} extends HTMLElement {
91
+ constructor() {
92
+ super();
93
+ this.attachShadow({ mode: 'open' });
94
+ const template = document.createElement('template');
95
+ template.innerHTML = \`
96
+ <style>
97
+ :host {
98
+ position: relative;
99
+ display: block;
100
+ width: 100%;
101
+ height: 100%;
102
+ margin: 0;
103
+ padding: 0;
104
+ }
105
+ needle-engine {
106
+ position: absolute;
107
+ top: 0;
108
+ left: 0;
109
+ width: 100%;
110
+ height: 100%;
111
+ }
112
+ </style>
113
+ \`;
114
+ this.shadowRoot.appendChild(template.content.cloneNode(true));
115
+
116
+ const script = document.createElement('script');
117
+ script.type = 'module';
118
+ const url = new URL('.', import.meta.url);
119
+ let basePath = this.getAttribute('base-path') || \`\${url.protocol}//\${url.host}\${url.pathname}\`;
120
+ while(basePath.endsWith('/')) {
121
+ basePath = basePath.slice(0, -1);
122
+ }
123
+ script.src = this.getAttribute('script-src') || \`\${basePath}/${filepath}\`;
124
+ this.shadowRoot.appendChild(script);
125
+
126
+ const needleEngine = document.createElement('needle-engine');
127
+ needleEngine.src = this.getAttribute('src') || ${src?.length ? `\${basePath}/${needleEngineSrcPaths}` : undefined};
128
+ this.shadowRoot.appendChild(needleEngine);
129
+
130
+ console.debug(basePath, script.src, needleEngine.getAttribute("src"));
131
+ }
132
+
133
+ onConnectedCallback() {
134
+ console.debug('NeedleEmbed connected to the DOM');
135
+ }
136
+
137
+ disconnectedCallback() {
138
+ console.debug('NeedleEmbed disconnected from the DOM');
139
+ }
140
+ }
141
+
142
+
143
+ if (!customElements.get('${componentName}')) {
144
+ console.debug("Defining ${componentName}");
145
+ customElements.define('${componentName}', ${className});
146
+ }
147
+ `
148
+ }
@@ -572,6 +572,9 @@ declare module 'three' {
572
572
 
573
573
 
574
574
  namespace NEMeshBVH {
575
+
576
+ let failedToCreateMeshBVHWorker = 0;
577
+
575
578
  export function runMeshBVHRaycast(method: Raycaster | Sphere, mesh: Mesh, results: Intersection[], context: Pick<Context, "xr">, options: { allowSlowRaycastFallback?: boolean }): boolean {
576
579
  if (!mesh.geometry) {
577
580
  return false;
@@ -630,6 +633,10 @@ namespace NEMeshBVH {
630
633
  || geom.index && geom.index?.["isInterleavedBufferAttribute"]) {
631
634
  canUseWorker = false;
632
635
  }
636
+ else if (failedToCreateMeshBVHWorker > 10) {
637
+ // if we failed to create a worker multiple times, don't try again
638
+ canUseWorker = false;
639
+ }
633
640
 
634
641
  // if we have a worker use that
635
642
  if (canUseWorker && _GenerateMeshBVHWorker) {
@@ -646,8 +653,23 @@ namespace NEMeshBVH {
646
653
  }
647
654
  // if there are no workers available, create a new one
648
655
  if (!workerInstance && workerInstances.length < 3) {
649
- workerInstance = new _GenerateMeshBVHWorker();
650
- workerInstances.push(workerInstance);
656
+ try {
657
+ workerInstance = new _GenerateMeshBVHWorker();
658
+ workerInstances.push(workerInstance);
659
+ }
660
+ catch (err) {
661
+ const isSecurityError = err instanceof DOMException && err.name === "SecurityError";
662
+ if (isSecurityError) {
663
+ console.warn("Failed to create MeshBVH worker, falling back to main thread generation. This can happen when running from file://, if the browser does not support workers or if the browser is blocking workers for other reasons.");
664
+ console.debug(err);
665
+ failedToCreateMeshBVHWorker += 10;
666
+ }
667
+ else {
668
+ console.error("Failed to create MeshBVH worker");
669
+ console.debug(err);
670
+ }
671
+ failedToCreateMeshBVHWorker++;
672
+ }
651
673
  }
652
674
 
653
675
  if (workerInstance != null && !workerInstance.running) {
@@ -806,6 +828,9 @@ namespace NEMeshBVH {
806
828
  if (debugPhysics || isDevEnvironment()) {
807
829
  console.warn("Failed to setup mesh bvh worker");
808
830
  }
831
+ else {
832
+ console.debug("Failed to setup mesh bvh worker", _err);
833
+ }
809
834
  })
810
835
  .finally(() => {
811
836
  isRequestingWorker = false;
@@ -4,14 +4,16 @@ import { MeshBVH } from 'three-mesh-bvh';
4
4
  // Modified according to https://github.com/gkjohnson/three-mesh-bvh/issues/636#issuecomment-2209571751
5
5
  import { WorkerBase } from "three-mesh-bvh/src/workers/utils/WorkerBase.js";
6
6
 
7
+
7
8
  export class GenerateMeshBVHWorker extends WorkerBase {
8
9
 
9
10
  constructor() {
10
11
  // TODO: make mesh bvh worker "work" for prebundled CDN loading
11
12
  // https://linear.app/needle/issue/NE-6572
12
13
  // Also we don't use toplevel imports to not completely fail to load needle-engine where loading the worker fails
13
- // const url = new URL('three-mesh-bvh/src/workers/generateMeshBVH.worker.js', import.meta.url)
14
+ // const meta_url = import.meta.url;
14
15
  // const getWorker = () => new Worker(url, { type: 'module' })
16
+ // console.log(meta_url, url, getWorker());
15
17
  super(new Worker(new URL('three-mesh-bvh/src/workers/generateMeshBVH.worker.js', import.meta.url), { type: 'module' }));
16
18
  this.name = 'GenerateMeshBVHWorker';
17
19