@needle-tools/engine 5.1.0-canary.db0c38f → 5.1.0-canary.deec6e4

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.
Files changed (106) hide show
  1. package/.needle/generated/needle-bindings.gen.d.ts +5 -0
  2. package/CHANGELOG.md +34 -0
  3. package/components.needle.json +1 -1
  4. package/dist/{needle-engine.bundle-YnpzzOPL.min.js → needle-engine.bundle-1s2gOoKZ.min.js} +144 -144
  5. package/dist/{needle-engine.bundle-B29kieh0.js → needle-engine.bundle-CvtELXh0.js} +6650 -6584
  6. package/dist/{needle-engine.bundle-Dq0Ly8fW.umd.cjs → needle-engine.bundle-j4nGJXCs.umd.cjs} +138 -138
  7. package/dist/needle-engine.d.ts +101 -89
  8. package/dist/needle-engine.js +188 -186
  9. package/dist/needle-engine.min.js +1 -1
  10. package/dist/needle-engine.umd.cjs +1 -1
  11. package/lib/engine/api.d.ts +1 -1
  12. package/lib/engine/debug/debug_spatial_console.d.ts +2 -0
  13. package/lib/engine/debug/debug_spatial_console.js +10 -7
  14. package/lib/engine/debug/debug_spatial_console.js.map +1 -1
  15. package/lib/engine/engine_addressables.d.ts +2 -0
  16. package/lib/engine/engine_addressables.js +6 -3
  17. package/lib/engine/engine_addressables.js.map +1 -1
  18. package/lib/engine/engine_context.d.ts +21 -20
  19. package/lib/engine/engine_context.js +25 -14
  20. package/lib/engine/engine_context.js.map +1 -1
  21. package/lib/engine/engine_init.js +15 -0
  22. package/lib/engine/engine_init.js.map +1 -1
  23. package/lib/engine/engine_license.d.ts +2 -0
  24. package/lib/engine/engine_license.js +14 -6
  25. package/lib/engine/engine_license.js.map +1 -1
  26. package/lib/engine/engine_lifecycle_functions_internal.js +5 -0
  27. package/lib/engine/engine_lifecycle_functions_internal.js.map +1 -1
  28. package/lib/engine/engine_pmrem.js +2 -2
  29. package/lib/engine/engine_pmrem.js.map +1 -1
  30. package/lib/engine/engine_scenedata.d.ts +13 -17
  31. package/lib/engine/engine_scenedata.js +56 -29
  32. package/lib/engine/engine_scenedata.js.map +1 -1
  33. package/lib/engine/engine_serialization_builtin_serializer.d.ts +10 -16
  34. package/lib/engine/engine_serialization_builtin_serializer.js +28 -41
  35. package/lib/engine/engine_serialization_builtin_serializer.js.map +1 -1
  36. package/lib/engine/engine_ssr.d.ts +2 -0
  37. package/lib/engine/engine_ssr.js +20 -0
  38. package/lib/engine/engine_ssr.js.map +1 -1
  39. package/lib/engine/engine_types.d.ts +2 -0
  40. package/lib/engine/engine_types.js.map +1 -1
  41. package/lib/engine/webcomponents/jsx.d.ts +51 -0
  42. package/lib/engine/webcomponents/logo-element.js.map +1 -1
  43. package/lib/engine/webcomponents/needle menu/needle-menu.d.ts +2 -3
  44. package/lib/engine/webcomponents/needle menu/needle-menu.js.map +1 -1
  45. package/lib/engine/webcomponents/needle-button.js.map +1 -1
  46. package/lib/engine/webcomponents/needle-engine.js.map +1 -1
  47. package/lib/engine-components/AnimatorController.d.ts +2 -0
  48. package/lib/engine-components/AnimatorController.js +4 -1
  49. package/lib/engine-components/AnimatorController.js.map +1 -1
  50. package/lib/engine-components/Light.d.ts +6 -8
  51. package/lib/engine-components/Light.js +40 -27
  52. package/lib/engine-components/Light.js.map +1 -1
  53. package/lib/engine-components/ReflectionProbe.js +2 -0
  54. package/lib/engine-components/ReflectionProbe.js.map +1 -1
  55. package/lib/engine-components/postprocessing/VolumeParameter.d.ts +2 -0
  56. package/lib/engine-components/postprocessing/VolumeParameter.js +4 -1
  57. package/lib/engine-components/postprocessing/VolumeParameter.js.map +1 -1
  58. package/lib/needle-engine.d.ts +2 -0
  59. package/lib/needle-engine.js +2 -0
  60. package/lib/needle-engine.js.map +1 -1
  61. package/package.json +3 -2
  62. package/plugins/dts-generator/dts.codegen.js +255 -50
  63. package/plugins/dts-generator/dts.scan.js +37 -9
  64. package/plugins/dts-generator/dts.writer.js +1 -1
  65. package/plugins/dts-generator/glb.discovery.js +140 -23
  66. package/plugins/dts-generator/glb.extractor.js +48 -8
  67. package/plugins/dts-generator/glb.reader.js +80 -27
  68. package/plugins/dts-generator/index.js +1 -1
  69. package/plugins/types/needle-bindings.d.ts +25 -14
  70. package/plugins/types/userconfig.d.ts +12 -0
  71. package/plugins/vite/asap.js +1 -1
  72. package/plugins/vite/dependency-watcher.d.ts +2 -2
  73. package/plugins/vite/dependency-watcher.js +3 -4
  74. package/plugins/vite/drop.d.ts +2 -2
  75. package/plugins/vite/drop.js +3 -4
  76. package/plugins/vite/dts-generator.d.ts +2 -2
  77. package/plugins/vite/dts-generator.js +43 -9
  78. package/plugins/vite/index.d.ts +9 -3
  79. package/plugins/vite/index.js +23 -10
  80. package/plugins/vite/meta.js +4 -2
  81. package/plugins/vite/poster.d.ts +2 -2
  82. package/plugins/vite/poster.js +3 -5
  83. package/plugins/vite/reload.d.ts +2 -2
  84. package/plugins/vite/reload.js +22 -22
  85. package/src/engine/api.ts +1 -1
  86. package/src/engine/debug/debug_spatial_console.ts +10 -7
  87. package/src/engine/engine_addressables.ts +6 -3
  88. package/src/engine/engine_context.ts +34 -20
  89. package/src/engine/engine_init.ts +14 -0
  90. package/src/engine/engine_license.ts +12 -10
  91. package/src/engine/engine_lifecycle_functions_internal.ts +7 -0
  92. package/src/engine/engine_pmrem.ts +3 -3
  93. package/src/engine/engine_scenedata.ts +53 -27
  94. package/src/engine/engine_serialization_builtin_serializer.ts +32 -43
  95. package/src/engine/engine_ssr.ts +29 -3
  96. package/src/engine/engine_types.ts +2 -0
  97. package/src/engine/webcomponents/jsx.d.ts +51 -0
  98. package/src/engine/webcomponents/logo-element.ts +1 -0
  99. package/src/engine/webcomponents/needle menu/needle-menu.ts +2 -1
  100. package/src/engine/webcomponents/needle-button.ts +1 -0
  101. package/src/engine/webcomponents/needle-engine.ts +1 -0
  102. package/src/engine-components/AnimatorController.ts +4 -1
  103. package/src/engine-components/Light.ts +40 -26
  104. package/src/engine-components/ReflectionProbe.ts +2 -0
  105. package/src/engine-components/postprocessing/VolumeParameter.ts +4 -1
  106. package/src/needle-engine.ts +3 -0
@@ -3,81 +3,284 @@
3
3
  * Pure string generators — no I/O.
4
4
  *
5
5
  * Takes `BindingEntry[]` from dts.scan.js and produces:
6
- * - `needle-bindings.d.ts` (TypeScript ambient module augmentation)
6
+ * - `needle-bindings.gen.d.ts` (TypeScript ambient module augmentation)
7
7
  * - `needle-html-data.json` (VS Code HTML custom data for data-bind-needle completions)
8
+ *
9
+ * Each scene node is emitted as a named type alias so VS Code hover shows
10
+ * the alias name + JSDoc summary rather than expanding the full object type:
11
+ *
12
+ * /** `Minimal/Cube` — MeshRenderer, BoxCollider *\/
13
+ * type $Minimal__Cube = { $object: THREE.Mesh; $components: { ... }; };
14
+ *
15
+ * interface SceneData {
16
+ * Minimal: { Minimal: { Cube: $Minimal__Minimal__Cube; }; }; };
17
+ * }
8
18
  */
9
19
 
10
20
  /** @typedef {import('./dts.scan.js').BindingEntry} BindingEntry */
11
21
 
22
+ /** Append `?view` to Needle Cloud asset URLs so the link opens the viewer. @param {string} url @returns {string} */
23
+ function addViewParam(url) {
24
+ if (!url.includes("cloud.needle.tools")) return url;
25
+ return url.includes("?") ? `${url}&view` : `${url}?view`;
26
+ }
27
+
12
28
  /** @param {string} name @returns {string} */
13
29
  function propKey(name) {
14
30
  return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name) ? name : `"${name}"`;
15
31
  }
16
32
 
17
33
  /**
18
- * Generate the `needle-bindings.d.ts` content from binding entries.
34
+ * Convert a node path like "Minimal/Cube_1/Child Of Cube" to a safe type alias name.
35
+ * @param {string} nodePath
36
+ * @returns {string}
37
+ */
38
+ function typeAliasName(nodePath) {
39
+ const safe = nodePath.replace(/[^a-zA-Z0-9]/g, "_");
40
+ return `$${safe}`;
41
+ }
42
+
43
+ /**
44
+ * @typedef {{
45
+ * threeType: string,
46
+ * components: Map<string, { isEngineComponent: boolean, fields: Map<string, Set<string>> }>,
47
+ * children: Map<string, TreeNode>
48
+ * }} TreeNode
49
+ */
50
+
51
+ /** @returns {TreeNode} */
52
+ function makeNode() {
53
+ return { threeType: `import("three").Object3D`, components: new Map(), children: new Map() };
54
+ }
55
+
56
+ /**
57
+ * Build a tree from BindingEntry[]. Each entry's nodePath (e.g. "UI/Camera/Target")
58
+ * defines where in the tree the node lives.
59
+ *
60
+ * @param {BindingEntry[]} entries
61
+ * @returns {Map<string, TreeNode>} Root-level nodes
62
+ */
63
+ function buildTree(entries) {
64
+ /** @type {Map<string, TreeNode>} */
65
+ const roots = new Map();
66
+
67
+ for (const { nodePath, componentName, fieldTypes, isEngineComponent, nodeThreeType } of entries) {
68
+ const parts = nodePath.split("/").filter(Boolean);
69
+ if (parts.length === 0) continue;
70
+
71
+ let map = roots;
72
+ /** @type {TreeNode | null} */
73
+ let node = null;
74
+ for (const part of parts) {
75
+ if (!map.has(part)) map.set(part, makeNode());
76
+ node = /** @type {TreeNode} */ (map.get(part));
77
+ map = node.children;
78
+ }
79
+ if (!node) continue;
80
+
81
+ node.threeType = nodeThreeType;
82
+
83
+ if (componentName) {
84
+ if (!node.components.has(componentName)) {
85
+ node.components.set(componentName, { isEngineComponent, fields: new Map() });
86
+ }
87
+ const comp = /** @type {{ isEngineComponent: boolean, fields: Map<string, Set<string>> }} */ (node.components.get(componentName));
88
+ for (const [field, tsType] of Object.entries(fieldTypes)) {
89
+ if (!comp.fields.has(field)) comp.fields.set(field, new Set());
90
+ /** @type {Set<string>} */ (comp.fields.get(field)).add(tsType);
91
+ }
92
+ }
93
+ }
94
+
95
+ return roots;
96
+ }
97
+
98
+ /**
99
+ * Compute the human-readable summary for a node (component names or Three.js type).
100
+ * @param {TreeNode} node
101
+ * @returns {string}
102
+ */
103
+ function nodeSummary(node) {
104
+ const threeShortType = node.threeType.replace(/import\("three"\)\./g, "THREE.");
105
+ const compNames = Array.from(node.components.keys()).filter(n => n !== "").sort();
106
+ return compNames.length > 0 ? compNames.join(", ") : threeShortType;
107
+ }
108
+
109
+ /**
110
+ * Recursively collect type alias declarations for all nodes in the tree.
111
+ * Emits one `type $Alias = { ... }` per node, with JSDoc summary.
112
+ *
113
+ * @param {string} _key
114
+ * @param {TreeNode} node
115
+ * @param {string} nodePath Full path e.g. "Minimal/Cube"
116
+ * @param {string[]} aliases Accumulator — lines pushed in bottom-up order
117
+ */
118
+ function collectTypeAliases(_key, node, nodePath, aliases) {
119
+ // Recurse into children first (bottom-up so aliases are defined before use)
120
+ for (const [childKey, childNode] of Array.from(node.children.entries()).sort(([a], [b]) => a.localeCompare(b))) {
121
+ collectTypeAliases(childKey, childNode, `${nodePath}/${childKey}`, aliases);
122
+ }
123
+
124
+ const threeShortType = node.threeType.replace(/import\("three"\)\./g, "THREE.");
125
+ const compEntries = Array.from(node.components.entries())
126
+ .filter(([n]) => n !== "")
127
+ .sort(([a], [b]) => a.localeCompare(b));
128
+ const summary = nodeSummary(node);
129
+
130
+ const alias = typeAliasName(nodePath);
131
+
132
+ // Emit as an interface so each child property can carry JSDoc
133
+ const lines = [];
134
+ lines.push(`/** \`${nodePath}\` — ${summary} */`);
135
+ lines.push(`interface ${alias} {`);
136
+ lines.push(` $object: ${threeShortType};`);
137
+
138
+ if (compEntries.length > 0) {
139
+ const compParts = compEntries.map(([compName, { isEngineComponent, fields }]) => {
140
+ if (isEngineComponent) {
141
+ return `${propKey(compName)}: NE.${compName}`;
142
+ } else {
143
+ const fieldParts = [`enabled: boolean`];
144
+ for (const [field, types] of Array.from(fields.entries()).sort(([a], [b]) => a.localeCompare(b))) {
145
+ fieldParts.push(`${field}: ${Array.from(types).join(" | ")}`);
146
+ }
147
+ return `${propKey(compName)}: { ${fieldParts.join("; ")} }`;
148
+ }
149
+ });
150
+ lines.push(` /** Needle Engine components on this node. Access via \`getComponent()\`, \`addComponent()\`, or \`findObjectOfType()\` / \`findObjectsOfType()\`. */`);
151
+ lines.push(` $components: { ${compParts.join("; ")} };`);
152
+ }
153
+
154
+ for (const [childKey, childNode] of Array.from(node.children.entries()).sort(([a], [b]) => a.localeCompare(b))) {
155
+ const childAlias = typeAliasName(`${nodePath}/${childKey}`);
156
+ const childSummary = nodeSummary(childNode);
157
+ lines.push(` /** ${childSummary} */`);
158
+ lines.push(` ${propKey(childKey)}: ${childAlias};`);
159
+ }
160
+
161
+ lines.push(`}`);
162
+
163
+ aliases.push(lines.join("\n"));
164
+ }
165
+
166
+ /**
167
+ * Recursively render a tree node as indented JSDoc lines.
168
+ * @param {string} key
169
+ * @param {TreeNode} node
170
+ * @param {number} depth
171
+ * @param {string[]} lines
172
+ */
173
+ /** @param {string} threeType @returns {string} */
174
+ function nodeEmoji(threeType) {
175
+ if (threeType.includes("Scene")) return "🎬";
176
+ if (threeType.includes("SkinnedMesh")) return "🦴";
177
+ if (threeType.includes("Mesh")) return "⊞";
178
+ if (threeType.includes("PerspectiveCamera") || threeType.includes("OrthographicCamera") || threeType.includes("Camera")) return "👁";
179
+ if (threeType.includes("Light")) return "💡";
180
+ return "";
181
+ }
182
+
183
+ function buildTreeDoc(key, node, depth, lines) {
184
+ const indent = " ".repeat(depth);
185
+ const hasComponents = Array.from(node.components.keys()).some(n => n !== "");
186
+ const summary = nodeSummary(node);
187
+ const emoji = nodeEmoji(node.threeType);
188
+ const prefix = emoji ? `${emoji} ` : "";
189
+ // Bold + components for nodes that have something actionable; plain name for empty containers
190
+ const label = hasComponents ? `${prefix}**${key}** — ${summary}` : `${prefix}${key}`;
191
+ lines.push(` * ${indent}- ${label}`);
192
+ for (const [childKey, childNode] of Array.from(node.children.entries()).sort(([a], [b]) => a.localeCompare(b))) {
193
+ buildTreeDoc(childKey, childNode, depth + 1, lines);
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Generate the `needle-bindings.gen.d.ts` content from binding entries.
19
199
  *
20
200
  * @param {BindingEntry[]} entries
21
201
  * @returns {string}
22
202
  */
23
203
  export function generateDts(entries) {
24
- // Build: nodeName → { nodePath, components: Map<componentName, { isEngineComponent, fields }> }
25
- /** @type {Map<string, { nodePath: string, nodeThreeType: string, components: Map<string, { isEngineComponent: boolean, fields: Map<string, Set<string>> }> }>} */
26
- const byNode = new Map();
27
-
28
- for (const { nodeName, nodePath, componentName, fieldTypes, isEngineComponent, nodeThreeType } of entries) {
29
- const nodeKey = nodeName || "_";
30
- if (!byNode.has(nodeKey)) byNode.set(nodeKey, { nodePath: nodePath ?? nodeKey, nodeThreeType, components: new Map() });
31
- const nodeEntry = /** @type {{ nodePath: string, nodeThreeType: string, components: Map<string, { isEngineComponent: boolean, fields: Map<string, Set<string>> }> }} */ (byNode.get(nodeKey));
32
- const byComp = nodeEntry.components;
33
-
34
- if (!byComp.has(componentName)) byComp.set(componentName, { isEngineComponent, fields: new Map() });
35
- const entry = /** @type {{ isEngineComponent: boolean, fields: Map<string, Set<string>> }} */ (byComp.get(componentName));
36
-
37
- for (const [field, tsType] of Object.entries(fieldTypes)) {
38
- if (!entry.fields.has(field)) entry.fields.set(field, new Set());
39
- /** @type {Set<string>} */ (entry.fields.get(field)).add(tsType);
204
+ /** @type {Map<string, BindingEntry[]>} */
205
+ const byGlb = new Map();
206
+ /** @type {Map<string, string>} glbKey → source path/URL */
207
+ const glbSrcMap = new Map();
208
+ /** @type {Map<string, Set<string>>} glbKey set of source files referencing it */
209
+ const glbSourceFilesMap = new Map();
210
+ for (const entry of entries) {
211
+ const k = entry.glbKey;
212
+ if (!byGlb.has(k)) byGlb.set(k, []);
213
+ /** @type {BindingEntry[]} */ (byGlb.get(k)).push(entry);
214
+ if (entry.glbSrc && !glbSrcMap.has(k)) glbSrcMap.set(k, entry.glbSrc);
215
+ if (entry.glbSourceFiles?.length) {
216
+ if (!glbSourceFilesMap.has(k)) glbSourceFilesMap.set(k, new Set());
217
+ for (const sf of entry.glbSourceFiles) /** @type {Set<string>} */ (glbSourceFilesMap.get(k)).add(sf);
40
218
  }
41
219
  }
42
220
 
43
- const lines = [
221
+ const header = [
44
222
  `// Auto-generated by @needle-tools/engine — do not edit`,
45
223
  `// Regenerated on each vite dev-server start and GLB change.`,
46
- `// Augments the base "needle:bindings" declaration in @needle-tools/engine.`,
47
- `declare module "needle:bindings" {`,
224
+ `// Augments the base "needle-bindings" declaration in @needle-tools/engine.`,
225
+ `import type * as NE from "../../lib/needle-engine.js";`,
226
+ `import type * as THREE from "three";`,
227
+ ];
228
+
229
+ /** @type {string[]} */
230
+ const aliases = [];
231
+ /** @type {string[]} */
232
+ const interfaceLines = [
233
+ `declare module "needle-bindings" {`,
48
234
  ` interface SceneData {`,
49
235
  ];
50
236
 
51
- for (const [nodeKey, { nodePath, nodeThreeType, components }] of Array.from(byNode.entries()).sort(([a], [b]) => a.localeCompare(b))) {
52
- const compEntries = Array.from(components.entries()).filter(([n]) => n !== "").sort(([a], [b]) => a.localeCompare(b));
53
-
54
- if (compEntries.length === 0) {
55
- // Node-only entry skip, nothing useful to emit without components
56
- } else {
57
- lines.push(` /** @path ${nodePath} */`);
58
- lines.push(` ${propKey(nodeKey)}: {`);
59
- lines.push(` $node: ${nodeThreeType};`);
60
- for (const [compName, { isEngineComponent, fields }] of compEntries) {
61
- if (isEngineComponent) {
62
- lines.push(` ${propKey(compName)}: import("@needle-tools/engine").${compName};`);
63
- } else {
64
- lines.push(` ${propKey(compName)}: {`);
65
- lines.push(` enabled: boolean;`);
66
- for (const [field, types] of Array.from(fields.entries()).sort(([a], [b]) => a.localeCompare(b))) {
67
- lines.push(` ${field}: ${Array.from(types).join(" | ")};`);
68
- }
69
- lines.push(` };`);
70
- }
237
+ for (const [glbKey, glbEntries] of Array.from(byGlb.entries()).sort(([a], [b]) => a.localeCompare(b))) {
238
+ const roots = buildTree(glbEntries);
239
+ if (roots.size === 0) continue;
240
+
241
+ // Collect type aliases for all nodes in this GLB
242
+ for (const [rootKey, rootNode] of Array.from(roots.entries()).sort(([a], [b]) => a.localeCompare(b))) {
243
+ collectTypeAliases(rootKey, rootNode, `${glbKey}/${rootKey}`, aliases);
244
+ }
245
+
246
+ // Emit SceneData property references with hierarchy JSDoc
247
+ const glbSrc = glbSrcMap.get(glbKey);
248
+ const sourceFiles = glbSourceFilesMap.get(glbKey);
249
+ const treeDocLines = [` * GLB/glTF scene file`];
250
+ const glbSrcLink = glbSrc
251
+ ? (glbSrc.startsWith("http://") || glbSrc.startsWith("https://")
252
+ ? ` [${glbSrc}](${addViewParam(glbSrc)})`
253
+ : ` \`${glbSrc}\``)
254
+ : "";
255
+ treeDocLines.push(` * \`${glbKey}\`${glbSrcLink}`);
256
+ if (sourceFiles?.size) {
257
+ treeDocLines.push(` *`);
258
+ treeDocLines.push(` * **Referenced from:**`);
259
+ for (const sf of Array.from(sourceFiles).sort()) {
260
+ treeDocLines.push(` * - \`${sf}\``);
71
261
  }
72
- lines.push(` };`);
73
262
  }
263
+ treeDocLines.push(` *`);
264
+ treeDocLines.push(` * ---`);
265
+ treeDocLines.push(` *`);
266
+ for (const [rootKey, rootNode] of Array.from(roots.entries()).sort(([a], [b]) => a.localeCompare(b))) {
267
+ buildTreeDoc(rootKey, rootNode, 0, treeDocLines);
268
+ }
269
+ interfaceLines.push(` /**\n ${treeDocLines.join("\n ")}\n */`);
270
+ interfaceLines.push(` $${glbKey}: {`);
271
+ for (const [rootKey, rootNode] of Array.from(roots.entries()).sort(([a], [b]) => a.localeCompare(b))) {
272
+ const alias = typeAliasName(`${glbKey}/${rootKey}`);
273
+ const summary = nodeSummary(rootNode);
274
+ interfaceLines.push(` /** ${summary} */`);
275
+ interfaceLines.push(` ${propKey(rootKey)}: ${alias};`);
276
+ }
277
+ interfaceLines.push(` };`);
74
278
  }
75
279
 
76
- lines.push(` }`);
77
- lines.push(`}`);
78
- lines.push(``);
280
+ interfaceLines.push(` }`);
281
+ interfaceLines.push(`}`);
79
282
 
80
- return lines.join("\n");
283
+ return [...header, ``, ...aliases, ``, ...interfaceLines, ``].join("\n");
81
284
  }
82
285
 
83
286
  /**
@@ -88,14 +291,16 @@ export function generateDts(entries) {
88
291
  */
89
292
  export function generateHtmlCustomData(entries) {
90
293
  const pairs = Array.from(
91
- new Set(entries.filter(e => e.componentName).map(e => `${e.nodeName}/${e.componentName}`))
294
+ new Set(entries.filter(e => e.componentName).map(e => `${e.nodePath}/${e.componentName}`))
92
295
  ).sort();
93
296
 
94
297
  const values = pairs.map(pair => {
95
- const [nodeName, compName] = pair.split("/");
298
+ const slash = pair.lastIndexOf("/");
299
+ const nodePath = pair.slice(0, slash);
300
+ const compName = pair.slice(slash + 1);
96
301
  return {
97
302
  name: pair,
98
- description: `Bind to the **${compName}** component on node \`${nodeName}\`.`,
303
+ description: `Bind to the **${compName}** component on node \`${nodePath}\`.`,
99
304
  };
100
305
  });
101
306
 
@@ -109,7 +314,7 @@ export function generateHtmlCustomData(entries) {
109
314
  value: [
110
315
  "Binds this HTML element to a Needle Engine scene component.",
111
316
  "",
112
- "**Format:** `NodeName/ComponentName`",
317
+ "**Format:** `NodePath/ComponentName`",
113
318
  "",
114
319
  "The Needle Engine runtime will associate this element with the specified",
115
320
  "component instance in the live scene graph.",
@@ -6,10 +6,13 @@
6
6
  * type resolution into the `BindingEntry[]` array consumed by codegen.
7
7
  */
8
8
 
9
- import { resolveEntrypointGlbs, collectSceneFiles } from './glb.discovery.js';
9
+ import { resolveEntrypointGlbs, collectSceneFiles, glbFriendlyName } from './glb.discovery.js';
10
10
  import { readGlbJsonChunk, readGltfJsonFile, readRemoteGlbJsonChunk } from './glb.reader.js';
11
11
  import { extractComponentBindings, inferTsType } from './glb.extractor.js';
12
12
  import { componentsManifest } from './manifest.types.js';
13
+ import { needleLog } from '../vite/logging.js';
14
+
15
+ const PLUGIN = "needle:dts-generator";
13
16
 
14
17
  /**
15
18
  * @typedef {Object} BindingEntry
@@ -19,11 +22,14 @@ import { componentsManifest } from './manifest.types.js';
19
22
  * @property {Record<string, string>} fieldTypes field name → TS type string
20
23
  * @property {boolean} isEngineComponent true if the component exists in components.needle.json
21
24
  * @property {string} nodeThreeType Three.js type of the parent node (e.g. `import("three").Mesh`)
25
+ * @property {string} glbKey Friendly identifier derived from the GLB name (e.g. "myScene", "MaterialXNodes")
26
+ * @property {string} glbSrc Project-relative path or URL of the GLB file
27
+ * @property {string[]} [glbSourceFiles] Source files (relative to project root) that reference this GLB
22
28
  */
23
29
 
24
30
  /**
25
31
  * Scan GLB/glTF files and return structured binding data.
26
- * Uses entrypoint GLBs (from index.html or gen.js) when available,
32
+ * Uses entrypoint GLBs (from index.html, gen.js, or source files) when available,
27
33
  * otherwise falls back to scanning all GLBs in assetsDir.
28
34
  *
29
35
  * @param {string} assetsDir Absolute path to the assets directory
@@ -32,21 +38,41 @@ import { componentsManifest } from './manifest.types.js';
32
38
  * @returns {Promise<BindingEntry[]>}
33
39
  */
34
40
  export async function scanBindings(assetsDir, projectRoot, codegenDir) {
35
- /** @type {Array<{path: string, type: "glb"|"gltf", remote?: boolean}>} */
41
+ /** @type {Array<{path: string, type: "glb"|"gltf", remote?: boolean, key?: string}>} */
36
42
  const files = /** @type {any} */ ((projectRoot ? resolveEntrypointGlbs(projectRoot, assetsDir, codegenDir) : null)
37
43
  ?? collectSceneFiles(assetsDir));
38
44
 
45
+ needleLog(PLUGIN, `Discovered ${files.length} GLB(s):\n${files.map(f => ` ${f.path}`).join("\n")}`);
46
+
39
47
  /** @type {BindingEntry[]} */
40
48
  const entries = [];
41
49
 
42
50
  for (const file of files) {
43
- const json = file.remote
44
- ? await readRemoteGlbJsonChunk(file.path)
45
- : file.type === "glb"
46
- ? readGlbJsonChunk(file.path)
47
- : readGltfJsonFile(file.path);
51
+ let json = /** @type {Record<string, unknown> | null} */ (null);
52
+ let contentDispositionFilename = /** @type {string | null} */ (null);
53
+
54
+ if (file.remote) {
55
+ needleLog(PLUGIN, `Fetching remote GLB: ${file.path}`);
56
+ const result = await readRemoteGlbJsonChunk(file.path);
57
+ if (!result) { needleLog(PLUGIN, `Skipped (fetch failed): ${file.path}`, "warn"); continue; }
58
+ json = result.json;
59
+ contentDispositionFilename = result.filename;
60
+ needleLog(PLUGIN, `Remote GLB ok — Content-Disposition: ${contentDispositionFilename ?? "(none)"}`);
61
+ } else {
62
+ json = file.type === "glb" ? readGlbJsonChunk(file.path) : readGltfJsonFile(file.path);
63
+ }
48
64
  if (!json) continue;
49
65
 
66
+ // Derive a friendly identifier for this GLB (used as SceneData key).
67
+ // For local files: basename without extension.
68
+ // For remote: Content-Disposition filename > last non-generic URL segment.
69
+ const localPathForName = file.remote ? file.path : (
70
+ projectRoot
71
+ ? file.path.replace(projectRoot + "/", "").replace(projectRoot + "\\", "")
72
+ : file.path
73
+ );
74
+ const glbKey = glbFriendlyName(localPathForName, contentDispositionFilename);
75
+
50
76
  for (const { nodeName, nodePath, componentName, fields, nodeThreeType } of extractComponentBindings(json)) {
51
77
  /** @type {Record<string, string>} */
52
78
  const fieldTypes = {};
@@ -63,7 +89,9 @@ export async function scanBindings(assetsDir, projectRoot, codegenDir) {
63
89
  }
64
90
  }
65
91
  const isEngineComponent = componentName ? componentsManifest.has(componentName) : false;
66
- entries.push({ nodeName, nodePath, componentName, fieldTypes, isEngineComponent, nodeThreeType });
92
+ const glbSrc = file.remote ? file.path : localPathForName;
93
+ const glbSourceFiles = /** @type {string[] | undefined} */ (/** @type {any} */ (file).sourceFiles);
94
+ entries.push({ nodeName, nodePath, componentName, fieldTypes, isEngineComponent, nodeThreeType, glbKey, glbSrc, glbSourceFiles });
67
95
  }
68
96
  }
69
97
 
@@ -20,7 +20,7 @@ import { generateDts, generateHtmlCustomData } from './dts.codegen.js';
20
20
  * @param {string} opts.assetsDir Absolute path to assets directory
21
21
  * @param {string} opts.outputPath Absolute path to write needle-bindings.d.ts
22
22
  * @param {string} [opts.projectRoot] Project root — enables entrypoint GLB detection
23
- * @param {string} [opts.codegenDir] Codegen directory — used to find gen.js
23
+ * @param {string} [opts.codegenDir] Codegen directory — used to find gen.js and write needle-html-data.json (no binding copy written here)
24
24
  * @returns {Promise<number | false>}
25
25
  */
26
26
  export async function generateBindingsDts({ assetsDir, outputPath, projectRoot, codegenDir }) {