@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.
- package/.needle/generated/needle-bindings.gen.d.ts +5 -0
- package/CHANGELOG.md +34 -0
- package/components.needle.json +1 -1
- package/dist/{needle-engine.bundle-YnpzzOPL.min.js → needle-engine.bundle-1s2gOoKZ.min.js} +144 -144
- package/dist/{needle-engine.bundle-B29kieh0.js → needle-engine.bundle-CvtELXh0.js} +6650 -6584
- package/dist/{needle-engine.bundle-Dq0Ly8fW.umd.cjs → needle-engine.bundle-j4nGJXCs.umd.cjs} +138 -138
- package/dist/needle-engine.d.ts +101 -89
- package/dist/needle-engine.js +188 -186
- package/dist/needle-engine.min.js +1 -1
- package/dist/needle-engine.umd.cjs +1 -1
- package/lib/engine/api.d.ts +1 -1
- package/lib/engine/debug/debug_spatial_console.d.ts +2 -0
- package/lib/engine/debug/debug_spatial_console.js +10 -7
- package/lib/engine/debug/debug_spatial_console.js.map +1 -1
- package/lib/engine/engine_addressables.d.ts +2 -0
- package/lib/engine/engine_addressables.js +6 -3
- package/lib/engine/engine_addressables.js.map +1 -1
- package/lib/engine/engine_context.d.ts +21 -20
- package/lib/engine/engine_context.js +25 -14
- package/lib/engine/engine_context.js.map +1 -1
- package/lib/engine/engine_init.js +15 -0
- package/lib/engine/engine_init.js.map +1 -1
- package/lib/engine/engine_license.d.ts +2 -0
- package/lib/engine/engine_license.js +14 -6
- package/lib/engine/engine_license.js.map +1 -1
- package/lib/engine/engine_lifecycle_functions_internal.js +5 -0
- package/lib/engine/engine_lifecycle_functions_internal.js.map +1 -1
- package/lib/engine/engine_pmrem.js +2 -2
- package/lib/engine/engine_pmrem.js.map +1 -1
- package/lib/engine/engine_scenedata.d.ts +13 -17
- package/lib/engine/engine_scenedata.js +56 -29
- package/lib/engine/engine_scenedata.js.map +1 -1
- package/lib/engine/engine_serialization_builtin_serializer.d.ts +10 -16
- package/lib/engine/engine_serialization_builtin_serializer.js +28 -41
- package/lib/engine/engine_serialization_builtin_serializer.js.map +1 -1
- package/lib/engine/engine_ssr.d.ts +2 -0
- package/lib/engine/engine_ssr.js +20 -0
- package/lib/engine/engine_ssr.js.map +1 -1
- package/lib/engine/engine_types.d.ts +2 -0
- package/lib/engine/engine_types.js.map +1 -1
- package/lib/engine/webcomponents/jsx.d.ts +51 -0
- package/lib/engine/webcomponents/logo-element.js.map +1 -1
- package/lib/engine/webcomponents/needle menu/needle-menu.d.ts +2 -3
- package/lib/engine/webcomponents/needle menu/needle-menu.js.map +1 -1
- package/lib/engine/webcomponents/needle-button.js.map +1 -1
- package/lib/engine/webcomponents/needle-engine.js.map +1 -1
- package/lib/engine-components/AnimatorController.d.ts +2 -0
- package/lib/engine-components/AnimatorController.js +4 -1
- package/lib/engine-components/AnimatorController.js.map +1 -1
- package/lib/engine-components/Light.d.ts +6 -8
- package/lib/engine-components/Light.js +40 -27
- package/lib/engine-components/Light.js.map +1 -1
- package/lib/engine-components/ReflectionProbe.js +2 -0
- package/lib/engine-components/ReflectionProbe.js.map +1 -1
- package/lib/engine-components/postprocessing/VolumeParameter.d.ts +2 -0
- package/lib/engine-components/postprocessing/VolumeParameter.js +4 -1
- package/lib/engine-components/postprocessing/VolumeParameter.js.map +1 -1
- package/lib/needle-engine.d.ts +2 -0
- package/lib/needle-engine.js +2 -0
- package/lib/needle-engine.js.map +1 -1
- package/package.json +3 -2
- package/plugins/dts-generator/dts.codegen.js +255 -50
- package/plugins/dts-generator/dts.scan.js +37 -9
- package/plugins/dts-generator/dts.writer.js +1 -1
- package/plugins/dts-generator/glb.discovery.js +140 -23
- package/plugins/dts-generator/glb.extractor.js +48 -8
- package/plugins/dts-generator/glb.reader.js +80 -27
- package/plugins/dts-generator/index.js +1 -1
- package/plugins/types/needle-bindings.d.ts +25 -14
- package/plugins/types/userconfig.d.ts +12 -0
- package/plugins/vite/asap.js +1 -1
- package/plugins/vite/dependency-watcher.d.ts +2 -2
- package/plugins/vite/dependency-watcher.js +3 -4
- package/plugins/vite/drop.d.ts +2 -2
- package/plugins/vite/drop.js +3 -4
- package/plugins/vite/dts-generator.d.ts +2 -2
- package/plugins/vite/dts-generator.js +43 -9
- package/plugins/vite/index.d.ts +9 -3
- package/plugins/vite/index.js +23 -10
- package/plugins/vite/meta.js +4 -2
- package/plugins/vite/poster.d.ts +2 -2
- package/plugins/vite/poster.js +3 -5
- package/plugins/vite/reload.d.ts +2 -2
- package/plugins/vite/reload.js +22 -22
- package/src/engine/api.ts +1 -1
- package/src/engine/debug/debug_spatial_console.ts +10 -7
- package/src/engine/engine_addressables.ts +6 -3
- package/src/engine/engine_context.ts +34 -20
- package/src/engine/engine_init.ts +14 -0
- package/src/engine/engine_license.ts +12 -10
- package/src/engine/engine_lifecycle_functions_internal.ts +7 -0
- package/src/engine/engine_pmrem.ts +3 -3
- package/src/engine/engine_scenedata.ts +53 -27
- package/src/engine/engine_serialization_builtin_serializer.ts +32 -43
- package/src/engine/engine_ssr.ts +29 -3
- package/src/engine/engine_types.ts +2 -0
- package/src/engine/webcomponents/jsx.d.ts +51 -0
- package/src/engine/webcomponents/logo-element.ts +1 -0
- package/src/engine/webcomponents/needle menu/needle-menu.ts +2 -1
- package/src/engine/webcomponents/needle-button.ts +1 -0
- package/src/engine/webcomponents/needle-engine.ts +1 -0
- package/src/engine-components/AnimatorController.ts +4 -1
- package/src/engine-components/Light.ts +40 -26
- package/src/engine-components/ReflectionProbe.ts +2 -0
- package/src/engine-components/postprocessing/VolumeParameter.ts +4 -1
- 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
|
-
*
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
if (!
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
|
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
|
|
47
|
-
`
|
|
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 [
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
lines.push(``);
|
|
280
|
+
interfaceLines.push(` }`);
|
|
281
|
+
interfaceLines.push(`}`);
|
|
79
282
|
|
|
80
|
-
return
|
|
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.
|
|
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
|
|
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 \`${
|
|
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:** `
|
|
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
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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 }) {
|