@needle-tools/engine 5.1.0-alpha → 5.1.0-canary.30cc545

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 (166) hide show
  1. package/CHANGELOG.md +9 -1
  2. package/SKILL.md +39 -21
  3. package/components.needle.json +1 -1
  4. package/dist/{gltf-progressive-DJBMx-zB.umd.cjs → gltf-progressive-BmblPzFj.umd.cjs} +4 -4
  5. package/dist/{gltf-progressive-BryRjllq.min.js → gltf-progressive-CN_mbb66.min.js} +2 -2
  6. package/dist/{gltf-progressive-Cl167Vjx.js → gltf-progressive-DUlhxdv4.js} +5 -2
  7. package/dist/{needle-engine.bundle-wM-BWPX9.umd.cjs → needle-engine.bundle-BMlLSACE.umd.cjs} +250 -174
  8. package/dist/{needle-engine.bundle-qDahLTqW.min.js → needle-engine.bundle-BXPPQRer.min.js} +242 -166
  9. package/dist/{needle-engine.bundle-CwhCzjep.js → needle-engine.bundle-d_9mSxN4.js} +12930 -12465
  10. package/dist/needle-engine.d.ts +267 -16
  11. package/dist/needle-engine.js +569 -563
  12. package/dist/needle-engine.min.js +1 -1
  13. package/dist/needle-engine.umd.cjs +1 -1
  14. package/dist/{postprocessing-B_9sKVU7.min.js → postprocessing-B571qGWR.min.js} +34 -34
  15. package/dist/{postprocessing-WDc9WwI3.js → postprocessing-CfrLAbLX.js} +0 -1
  16. package/dist/{postprocessing-B2wb6pzI.umd.cjs → postprocessing-CiGkAeM9.umd.cjs} +17 -17
  17. package/dist/{vendor-CAcsI0eU.js → vendor-BFrMaK9q.js} +8983 -9136
  18. package/dist/vendor-CJmyOrCq.min.js +1116 -0
  19. package/dist/vendor-DkMW3WY4.umd.cjs +1116 -0
  20. package/lib/engine/api.d.ts +12 -0
  21. package/lib/engine/api.js +2 -0
  22. package/lib/engine/api.js.map +1 -1
  23. package/lib/engine/debug/debug_environment.js +1 -1
  24. package/lib/engine/debug/debug_environment.js.map +1 -1
  25. package/lib/engine/engine_application.js +8 -6
  26. package/lib/engine/engine_application.js.map +1 -1
  27. package/lib/engine/engine_components.js +5 -1
  28. package/lib/engine/engine_components.js.map +1 -1
  29. package/lib/engine/engine_constants.js +6 -0
  30. package/lib/engine/engine_constants.js.map +1 -1
  31. package/lib/engine/engine_context.d.ts +25 -0
  32. package/lib/engine/engine_context.js +27 -0
  33. package/lib/engine/engine_context.js.map +1 -1
  34. package/lib/engine/engine_context_registry.js +1 -1
  35. package/lib/engine/engine_context_registry.js.map +1 -1
  36. package/lib/engine/engine_init.js +2 -0
  37. package/lib/engine/engine_init.js.map +1 -1
  38. package/lib/engine/engine_input.d.ts +3 -2
  39. package/lib/engine/engine_input.js +3 -2
  40. package/lib/engine/engine_input.js.map +1 -1
  41. package/lib/engine/engine_license.js +11 -9
  42. package/lib/engine/engine_license.js.map +1 -1
  43. package/lib/engine/engine_networking_blob.d.ts +1 -1
  44. package/lib/engine/engine_networking_blob.js +5 -11
  45. package/lib/engine/engine_networking_blob.js.map +1 -1
  46. package/lib/engine/engine_physics_rapier.d.ts +3 -0
  47. package/lib/engine/engine_physics_rapier.js +13 -10
  48. package/lib/engine/engine_physics_rapier.js.map +1 -1
  49. package/lib/engine/engine_pmrem.js +2 -2
  50. package/lib/engine/engine_pmrem.js.map +1 -1
  51. package/lib/engine/engine_scenedata.d.ts +36 -0
  52. package/lib/engine/engine_scenedata.js +111 -0
  53. package/lib/engine/engine_scenedata.js.map +1 -0
  54. package/lib/engine/engine_ssr.d.ts +16 -0
  55. package/lib/engine/engine_ssr.js +38 -0
  56. package/lib/engine/engine_ssr.js.map +1 -0
  57. package/lib/engine/engine_three_utils.d.ts +14 -7
  58. package/lib/engine/engine_three_utils.js +14 -7
  59. package/lib/engine/engine_three_utils.js.map +1 -1
  60. package/lib/engine/engine_utils.js +4 -2
  61. package/lib/engine/engine_utils.js.map +1 -1
  62. package/lib/engine/engine_utils_hash.d.ts +9 -0
  63. package/lib/engine/engine_utils_hash.js +112 -0
  64. package/lib/engine/engine_utils_hash.js.map +1 -0
  65. package/lib/engine/webcomponents/logo-element.d.ts +2 -1
  66. package/lib/engine/webcomponents/logo-element.js +2 -1
  67. package/lib/engine/webcomponents/logo-element.js.map +1 -1
  68. package/lib/engine/webcomponents/needle menu/needle-menu.d.ts +2 -1
  69. package/lib/engine/webcomponents/needle menu/needle-menu.js +2 -1
  70. package/lib/engine/webcomponents/needle menu/needle-menu.js.map +1 -1
  71. package/lib/engine/webcomponents/needle-button.d.ts +2 -1
  72. package/lib/engine/webcomponents/needle-button.js +2 -1
  73. package/lib/engine/webcomponents/needle-button.js.map +1 -1
  74. package/lib/engine/webcomponents/needle-engine.d.ts +2 -1
  75. package/lib/engine/webcomponents/needle-engine.js +2 -1
  76. package/lib/engine/webcomponents/needle-engine.js.map +1 -1
  77. package/lib/engine/xr/NeedleXRSession.d.ts +1 -0
  78. package/lib/engine/xr/NeedleXRSession.js +5 -5
  79. package/lib/engine/xr/NeedleXRSession.js.map +1 -1
  80. package/lib/engine/xr/events.d.ts +30 -3
  81. package/lib/engine/xr/events.js +38 -0
  82. package/lib/engine/xr/events.js.map +1 -1
  83. package/lib/engine/xr/init.js +1 -7
  84. package/lib/engine/xr/init.js.map +1 -1
  85. package/lib/engine-components/AnimatorController.d.ts +135 -2
  86. package/lib/engine-components/AnimatorController.js +218 -2
  87. package/lib/engine-components/AnimatorController.js.map +1 -1
  88. package/lib/engine-components/GroundProjection.d.ts +1 -0
  89. package/lib/engine-components/GroundProjection.js +184 -48
  90. package/lib/engine-components/GroundProjection.js.map +1 -1
  91. package/lib/engine-components/RigidBody.js +3 -3
  92. package/lib/engine-components/RigidBody.js.map +1 -1
  93. package/lib/engine-components/SceneSwitcher.js +2 -0
  94. package/lib/engine-components/SceneSwitcher.js.map +1 -1
  95. package/lib/engine-components/api.d.ts +1 -0
  96. package/lib/engine-components/api.js +1 -0
  97. package/lib/engine-components/api.js.map +1 -1
  98. package/lib/engine-components/codegen/components.d.ts +1 -0
  99. package/lib/engine-components/codegen/components.js +1 -0
  100. package/lib/engine-components/codegen/components.js.map +1 -1
  101. package/lib/engine-components/postprocessing/Effects/BloomEffect.d.ts +1 -1
  102. package/lib/engine-components/postprocessing/Effects/Sharpening.js +1 -2
  103. package/lib/engine-components/postprocessing/Effects/Sharpening.js.map +1 -1
  104. package/lib/engine-components/postprocessing/PostProcessingHandler.js +5 -6
  105. package/lib/engine-components/postprocessing/PostProcessingHandler.js.map +1 -1
  106. package/lib/engine-components/web/ScrollFollow.d.ts +0 -1
  107. package/lib/engine-components/web/ScrollFollow.js +3 -2
  108. package/lib/engine-components/web/ScrollFollow.js.map +1 -1
  109. package/package.json +6 -5
  110. package/plugins/common/logger.js +42 -19
  111. package/plugins/dts-generator/dts.codegen.js +129 -0
  112. package/plugins/dts-generator/dts.scan.js +71 -0
  113. package/plugins/dts-generator/dts.writer.js +59 -0
  114. package/plugins/dts-generator/glb.discovery.js +162 -0
  115. package/plugins/dts-generator/glb.extractor.js +175 -0
  116. package/plugins/dts-generator/glb.reader.js +114 -0
  117. package/plugins/dts-generator/index.js +36 -0
  118. package/plugins/dts-generator/manifest.types.js +174 -0
  119. package/plugins/types/index.d.ts +2 -1
  120. package/plugins/types/needle-bindings.d.ts +19 -0
  121. package/plugins/types/userconfig.d.ts +9 -2
  122. package/plugins/vite/dts-generator.d.ts +7 -0
  123. package/plugins/vite/dts-generator.js +157 -0
  124. package/plugins/vite/index.d.ts +1 -0
  125. package/plugins/vite/index.js +4 -0
  126. package/plugins/vite/logger.client.js +4 -3
  127. package/plugins/vite/logging.js +2 -2
  128. package/plugins/vite/reload.js +2 -1
  129. package/src/engine/api.ts +15 -1
  130. package/src/engine/debug/debug_environment.ts +1 -1
  131. package/src/engine/engine_application.ts +8 -6
  132. package/src/engine/engine_components.ts +7 -4
  133. package/src/engine/engine_constants.ts +11 -6
  134. package/src/engine/engine_context.ts +29 -0
  135. package/src/engine/engine_context_registry.ts +1 -1
  136. package/src/engine/engine_init.ts +2 -0
  137. package/src/engine/engine_input.ts +3 -2
  138. package/src/engine/engine_license.ts +11 -9
  139. package/src/engine/engine_networking_blob.ts +5 -11
  140. package/src/engine/engine_physics_rapier.ts +14 -12
  141. package/src/engine/engine_pmrem.ts +3 -3
  142. package/src/engine/engine_scenedata.ts +110 -0
  143. package/src/engine/engine_ssr.ts +45 -0
  144. package/src/engine/engine_three_utils.ts +15 -7
  145. package/src/engine/engine_utils.ts +3 -2
  146. package/src/engine/engine_utils_hash.ts +65 -0
  147. package/src/engine/webcomponents/logo-element.ts +2 -1
  148. package/src/engine/webcomponents/needle menu/needle-menu.ts +2 -1
  149. package/src/engine/webcomponents/needle-button.ts +2 -1
  150. package/src/engine/webcomponents/needle-engine.ts +2 -1
  151. package/src/engine/xr/NeedleXRSession.ts +6 -6
  152. package/src/engine/xr/events.ts +44 -1
  153. package/src/engine/xr/init.ts +0 -7
  154. package/src/engine-components/AnimatorController.ts +286 -4
  155. package/src/engine-components/GroundProjection.ts +226 -52
  156. package/src/engine-components/RigidBody.ts +3 -3
  157. package/src/engine-components/SceneSwitcher.ts +1 -0
  158. package/src/engine-components/api.ts +1 -0
  159. package/src/engine-components/codegen/components.ts +1 -0
  160. package/src/engine-components/postprocessing/Effects/BloomEffect.ts +1 -1
  161. package/src/engine-components/postprocessing/Effects/Sharpening.ts +1 -2
  162. package/src/engine-components/postprocessing/PostProcessingHandler.ts +4 -8
  163. package/src/engine-components/web/ScrollFollow.ts +2 -2
  164. package/src/vite-env.d.ts +16 -0
  165. package/dist/vendor-CEM38hLE.umd.cjs +0 -1116
  166. package/dist/vendor-HRlxIBga.min.js +0 -1116
@@ -0,0 +1,71 @@
1
+ // @ts-check
2
+ /**
3
+ * Scans GLB/glTF files and returns structured binding data.
4
+ *
5
+ * Combines file discovery, JSON reading, component extraction, and manifest
6
+ * type resolution into the `BindingEntry[]` array consumed by codegen.
7
+ */
8
+
9
+ import { resolveEntrypointGlbs, collectSceneFiles } from './glb.discovery.js';
10
+ import { readGlbJsonChunk, readGltfJsonFile, readRemoteGlbJsonChunk } from './glb.reader.js';
11
+ import { extractComponentBindings, inferTsType } from './glb.extractor.js';
12
+ import { componentsManifest } from './manifest.types.js';
13
+
14
+ /**
15
+ * @typedef {Object} BindingEntry
16
+ * @property {string} nodeName
17
+ * @property {string} nodePath Full hierarchy path e.g. "Scene/Cube/Child_Of_Cube"
18
+ * @property {string} componentName
19
+ * @property {Record<string, string>} fieldTypes field name → TS type string
20
+ * @property {boolean} isEngineComponent true if the component exists in components.needle.json
21
+ * @property {string} nodeThreeType Three.js type of the parent node (e.g. `import("three").Mesh`)
22
+ */
23
+
24
+ /**
25
+ * Scan GLB/glTF files and return structured binding data.
26
+ * Uses entrypoint GLBs (from index.html or gen.js) when available,
27
+ * otherwise falls back to scanning all GLBs in assetsDir.
28
+ *
29
+ * @param {string} assetsDir Absolute path to the assets directory
30
+ * @param {string} [projectRoot] Absolute path to the project root (enables entrypoint detection)
31
+ * @param {string} [codegenDir] Absolute path to the codegen directory
32
+ * @returns {Promise<BindingEntry[]>}
33
+ */
34
+ export async function scanBindings(assetsDir, projectRoot, codegenDir) {
35
+ /** @type {Array<{path: string, type: "glb"|"gltf", remote?: boolean}>} */
36
+ const files = /** @type {any} */ ((projectRoot ? resolveEntrypointGlbs(projectRoot, assetsDir, codegenDir) : null)
37
+ ?? collectSceneFiles(assetsDir));
38
+
39
+ /** @type {BindingEntry[]} */
40
+ const entries = [];
41
+
42
+ 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);
48
+ if (!json) continue;
49
+
50
+ for (const { nodeName, nodePath, componentName, fields, nodeThreeType } of extractComponentBindings(json)) {
51
+ /** @type {Record<string, string>} */
52
+ const fieldTypes = {};
53
+ if (componentName) {
54
+ const manifestFields = componentsManifest.get(componentName);
55
+ if (manifestFields) {
56
+ for (const [k, tsType] of manifestFields) {
57
+ fieldTypes[k] = tsType;
58
+ }
59
+ } else {
60
+ for (const [k, v] of Object.entries(fields)) {
61
+ fieldTypes[k] = inferTsType(v);
62
+ }
63
+ }
64
+ }
65
+ const isEngineComponent = componentName ? componentsManifest.has(componentName) : false;
66
+ entries.push({ nodeName, nodePath, componentName, fieldTypes, isEngineComponent, nodeThreeType });
67
+ }
68
+ }
69
+
70
+ return entries;
71
+ }
@@ -0,0 +1,59 @@
1
+ // @ts-check
2
+ /**
3
+ * File writer — the only module that performs I/O writes.
4
+ *
5
+ * Orchestrates scanning, codegen, and writes the output files.
6
+ * Returns false if content was already up-to-date (no write performed).
7
+ */
8
+
9
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
10
+ import { join, dirname } from 'path';
11
+ import { scanBindings } from './dts.scan.js';
12
+ import { generateDts, generateHtmlCustomData } from './dts.codegen.js';
13
+
14
+ /**
15
+ * Scan assets and write `needle-bindings.d.ts` and `needle-html-data.json`
16
+ * to the output directory.
17
+ * Returns the number of bindings written, or `false` if content was already up-to-date.
18
+ *
19
+ * @param {object} opts
20
+ * @param {string} opts.assetsDir Absolute path to assets directory
21
+ * @param {string} opts.outputPath Absolute path to write needle-bindings.d.ts
22
+ * @param {string} [opts.projectRoot] Project root — enables entrypoint GLB detection
23
+ * @param {string} [opts.codegenDir] Codegen directory — used to find gen.js
24
+ * @returns {Promise<number | false>}
25
+ */
26
+ export async function generateBindingsDts({ assetsDir, outputPath, projectRoot, codegenDir }) {
27
+ const entries = await scanBindings(assetsDir, projectRoot, codegenDir);
28
+ const content = generateDts(entries);
29
+ const htmlContent = generateHtmlCustomData(entries);
30
+
31
+ mkdirSync(dirname(outputPath), { recursive: true });
32
+
33
+ let dtsChanged = false;
34
+ try {
35
+ const existing = existsSync(outputPath) ? readFileSync(outputPath, "utf8") : "";
36
+ if (existing !== content) {
37
+ writeFileSync(outputPath, content, "utf8");
38
+ dtsChanged = true;
39
+ }
40
+ } catch (_e) {
41
+ writeFileSync(outputPath, content, "utf8");
42
+ dtsChanged = true;
43
+ }
44
+
45
+ const htmlDataPath = join(dirname(outputPath), "needle-html-data.json");
46
+ try {
47
+ const existingHtml = existsSync(htmlDataPath) ? readFileSync(htmlDataPath, "utf8") : "";
48
+ if (existingHtml !== htmlContent) {
49
+ writeFileSync(htmlDataPath, htmlContent, "utf8");
50
+ }
51
+ } catch (_e) {
52
+ writeFileSync(htmlDataPath, htmlContent, "utf8");
53
+ }
54
+
55
+ if (!dtsChanged) return false;
56
+ const nodeCount = new Set(entries.map(e => e.nodeName)).size;
57
+ const bindingCount = new Set(entries.filter(e => e.componentName).map(e => `${e.nodeName}/${e.componentName}`)).size;
58
+ return bindingCount > 0 ? bindingCount : nodeCount;
59
+ }
@@ -0,0 +1,162 @@
1
+ // @ts-check
2
+ /**
3
+ * GLB / glTF file discovery.
4
+ *
5
+ * Resolves which scene files to process — either from the project's entrypoints
6
+ * (index.html `<needle-engine src>` or gen.js push calls) or by recursively
7
+ * walking the assets directory.
8
+ */
9
+
10
+ import { existsSync, readFileSync, readdirSync } from 'fs';
11
+ import { join, resolve } from 'path';
12
+
13
+ /**
14
+ * Parse `<needle-engine src="...">` from HTML.
15
+ * Handles both single strings and JSON arrays.
16
+ * @param {string} html
17
+ * @returns {string[]}
18
+ */
19
+ function parseSrcAttribute(html) {
20
+ const re = /<needle-engine[^>]*\ssrc=["']([^"']+)["']/gi;
21
+ /** @type {string[]} */
22
+ const out = [];
23
+ let m;
24
+ while ((m = re.exec(html)) !== null) {
25
+ const val = m[1].trim();
26
+ if (val.startsWith("[")) {
27
+ try {
28
+ const arr = JSON.parse(val);
29
+ if (Array.isArray(arr)) out.push(...arr.filter(v => typeof v === "string"));
30
+ } catch (_e) { /* malformed JSON, skip */ }
31
+ } else {
32
+ out.push(val);
33
+ }
34
+ }
35
+ return out;
36
+ }
37
+
38
+ /**
39
+ * Parse `needle_exported_files.push("path/to/file.glb")` lines from gen.js.
40
+ * @param {string} src
41
+ * @returns {string[]}
42
+ */
43
+ function parseGenJs(src) {
44
+ const re = /needle_exported_files\.push\(["']([^"']+\.(?:glb|gltf))["']\)/gi;
45
+ /** @type {string[]} */
46
+ const out = [];
47
+ let m;
48
+ while ((m = re.exec(src)) !== null) {
49
+ out.push(m[1]);
50
+ }
51
+ return out;
52
+ }
53
+
54
+ /**
55
+ * Resolve a list of (possibly relative) GLB path strings to absolute paths.
56
+ * Remote URLs (http/https) are passed through as-is.
57
+ *
58
+ * @param {string[]} paths
59
+ * @param {string} projectRoot
60
+ * @param {string} assetsDir
61
+ * @returns {Array<{path: string, type: "glb"|"gltf", remote?: boolean}>}
62
+ */
63
+ function resolveGlbPaths(paths, projectRoot, assetsDir) {
64
+ /** @type {Array<{path: string, type: "glb"|"gltf", remote?: boolean}>} */
65
+ const out = [];
66
+ for (const p of paths) {
67
+ if (p.startsWith("http://") || p.startsWith("https://")) {
68
+ const type = p.toLowerCase().endsWith(".gltf") ? "gltf" : "glb";
69
+ out.push({ path: p, type, remote: true });
70
+ continue;
71
+ }
72
+ const clean = p.replace(/^\.\//, "").replace(/^\//, "");
73
+ const candidates = [
74
+ join(projectRoot, clean),
75
+ join(assetsDir, clean.replace(/^assets\//, "")),
76
+ ];
77
+ for (const candidate of candidates) {
78
+ if (existsSync(candidate)) {
79
+ const type = candidate.toLowerCase().endsWith(".gltf") ? "gltf" : "glb";
80
+ out.push({ path: candidate, type });
81
+ break;
82
+ }
83
+ }
84
+ }
85
+ return out;
86
+ }
87
+
88
+ /**
89
+ * Resolve the entrypoint GLB file paths for a project.
90
+ *
91
+ * Priority:
92
+ * 1. `<needle-engine src="...">` attribute in `index.html`
93
+ * 2. `needle_exported_files.push("...")` lines in `{codegenDir}/gen.js`
94
+ * 3. Returns `null` — caller should fall back to `collectSceneFiles`.
95
+ *
96
+ * @param {string} projectRoot Absolute path to the project root
97
+ * @param {string} assetsDir Absolute path to the assets directory
98
+ * @param {string} [codegenDir] Absolute path to the codegen directory
99
+ * @returns {Array<{path: string, type: "glb"|"gltf", remote?: boolean}> | null}
100
+ */
101
+ export function resolveEntrypointGlbs(projectRoot, assetsDir, codegenDir) {
102
+ const htmlPath = join(projectRoot, "index.html");
103
+ if (existsSync(htmlPath)) {
104
+ try {
105
+ const srcPaths = parseSrcAttribute(readFileSync(htmlPath, "utf8"));
106
+ if (srcPaths.length > 0) {
107
+ const resolved = resolveGlbPaths(srcPaths, projectRoot, assetsDir);
108
+ if (resolved.length > 0) return resolved;
109
+ }
110
+ } catch (_e) { /* ignore */ }
111
+ }
112
+
113
+ const genDir = codegenDir ?? join(projectRoot, "src", "generated");
114
+ const genPath = join(genDir, "gen.js");
115
+ if (existsSync(genPath)) {
116
+ try {
117
+ const genPaths = parseGenJs(readFileSync(genPath, "utf8"));
118
+ if (genPaths.length > 0) {
119
+ const resolved = resolveGlbPaths(genPaths, projectRoot, assetsDir);
120
+ if (resolved.length > 0) return resolved;
121
+ }
122
+ } catch (_e) { /* ignore */ }
123
+ }
124
+
125
+ return null;
126
+ }
127
+
128
+ /**
129
+ * Recursively collect all scene GLB/glTF files in a directory.
130
+ * Skips LOD and image sub-glbs that aren't scene roots.
131
+ *
132
+ * @param {string} assetsDir
133
+ * @returns {Array<{path: string, type: "glb"|"gltf"}>}
134
+ */
135
+ export function collectSceneFiles(assetsDir) {
136
+ if (!existsSync(assetsDir)) return [];
137
+
138
+ /** @type {Array<{path: string, type: "glb"|"gltf"}>} */
139
+ const out = [];
140
+
141
+ /** @param {string} dir */
142
+ function walk(dir) {
143
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
144
+ const fullPath = join(dir, entry.name);
145
+ if (entry.isDirectory()) {
146
+ walk(fullPath);
147
+ continue;
148
+ }
149
+ if (/^image_\d+_.*\.glb$/i.test(entry.name)) continue;
150
+ if (/^mesh_lod_\d+_.*\.glb$/i.test(entry.name)) continue;
151
+
152
+ if (/\.glb$/i.test(entry.name)) {
153
+ out.push({ path: fullPath, type: "glb" });
154
+ } else if (/\.gltf$/i.test(entry.name)) {
155
+ out.push({ path: fullPath, type: "gltf" });
156
+ }
157
+ }
158
+ }
159
+
160
+ walk(assetsDir);
161
+ return out;
162
+ }
@@ -0,0 +1,175 @@
1
+ // @ts-check
2
+ /**
3
+ * glTF node tree walker and NEEDLE_components extractor.
4
+ *
5
+ * Walks the parsed glTF JSON, sanitizes node names to match Three.js runtime
6
+ * behaviour, and extracts component data from the NEEDLE_components extension.
7
+ */
8
+
9
+ const NEEDLE_COMPONENTS_EXTENSION = "NEEDLE_components";
10
+
11
+ /**
12
+ * Replicate Three.js GLTFLoader's sanitizeNodeName + createUniqueName logic.
13
+ * - Whitespace → `_`
14
+ * - Reserved chars `[ ] . : /` → removed
15
+ * - Duplicate names get `_1`, `_2`, … suffix
16
+ *
17
+ * @param {string} rawName
18
+ * @param {Record<string, number>} namesUsed Mutated in-place — pass the same object for all nodes in a GLB
19
+ * @returns {string}
20
+ */
21
+ export function sanitizeNodeName(rawName, namesUsed) {
22
+ const sanitized = rawName.replace(/\s/g, "_").replace(/[\[\].:\/]/g, "");
23
+ if (sanitized in namesUsed) {
24
+ return sanitized + "_" + (++namesUsed[sanitized]);
25
+ }
26
+ namesUsed[sanitized] = 0;
27
+ return sanitized;
28
+ }
29
+
30
+ /**
31
+ * Infer the Three.js runtime type of a glTF node from its JSON properties.
32
+ * - `mesh` present → `import("three").Mesh`
33
+ * - `camera` present → `import("three").Camera`
34
+ * - KHR_lights_punctual → `import("three").Light`
35
+ * - otherwise → `import("three").Object3D`
36
+ *
37
+ * @param {Record<string, unknown>} node
38
+ * @returns {string}
39
+ */
40
+ export function inferNodeThreeType(node) {
41
+ if ("mesh" in node) return `import("three").Mesh`;
42
+ if ("camera" in node) return `import("three").Camera`;
43
+ if (node.extensions && /** @type {any} */ (node.extensions)["KHR_lights_punctual"] != null) return `import("three").Light`;
44
+ return `import("three").Object3D`;
45
+ }
46
+
47
+ /**
48
+ * Infer a TypeScript type string from a raw JSON value.
49
+ * Only primitives are typed precisely; everything else → `unknown`.
50
+ *
51
+ * @param {unknown} value
52
+ * @returns {string}
53
+ */
54
+ export function inferTsType(value) {
55
+ if (value === null || value === undefined) return "unknown";
56
+ switch (typeof value) {
57
+ case "number": return "number";
58
+ case "string": return "string";
59
+ case "boolean": return "boolean";
60
+ default: return "unknown";
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Build a map of nodeIndex → sanitized name and nodeIndex → parent index
66
+ * from the glTF node tree, then compute the full path for each node.
67
+ *
68
+ * @param {Array<Record<string, unknown>>} nodes Raw glTF nodes array
69
+ * @param {Record<string, number>} namesUsed Already-used name registry (shared with extraction pass)
70
+ * @returns {{ nameMap: Map<number, string>, pathMap: Map<number, string> }}
71
+ */
72
+ function buildNodePaths(nodes, namesUsed) {
73
+ /** @type {Map<number, string>} nodeIndex → sanitized name */
74
+ const nameMap = new Map();
75
+ /** @type {Map<number, number>} nodeIndex → parent nodeIndex */
76
+ const parentMap = new Map();
77
+
78
+ // First pass: sanitize all names
79
+ for (let i = 0; i < nodes.length; i++) {
80
+ const node = nodes[i];
81
+ if (!node || typeof node !== "object") continue;
82
+ const rawName = typeof node.name === "string" ? node.name : "";
83
+ if (!rawName) continue;
84
+ nameMap.set(i, sanitizeNodeName(rawName, namesUsed));
85
+ }
86
+
87
+ // Second pass: build parent map from children arrays
88
+ for (let i = 0; i < nodes.length; i++) {
89
+ const node = nodes[i];
90
+ if (!node || typeof node !== "object") continue;
91
+ const children = Array.isArray(node.children) ? node.children : [];
92
+ for (const childIdx of children) {
93
+ parentMap.set(childIdx, i);
94
+ }
95
+ }
96
+
97
+ // Third pass: compute full path per node by walking up the parent chain
98
+ /** @type {Map<number, string>} nodeIndex → full path string */
99
+ const pathMap = new Map();
100
+ for (const [idx] of nameMap) {
101
+ const parts = [];
102
+ /** @type {number | undefined} */
103
+ let cur = idx;
104
+ while (cur !== undefined && nameMap.has(cur)) {
105
+ parts.unshift(/** @type {string} */ (nameMap.get(cur)));
106
+ cur = parentMap.get(cur);
107
+ }
108
+ pathMap.set(idx, parts.join("/"));
109
+ }
110
+
111
+ return { nameMap, pathMap };
112
+ }
113
+
114
+ /**
115
+ * Walk the glTF JSON and collect every node name + any NEEDLE_components extension blocks.
116
+ * Node names are sanitized and de-duplicated to match Three.js GLTFLoader behaviour.
117
+ *
118
+ * Nodes without NEEDLE_components are returned with an empty componentName so that
119
+ * callers can decide whether to emit them.
120
+ *
121
+ * @param {Record<string, unknown>} json Parsed glTF JSON
122
+ * @returns {Array<{nodeName: string, nodePath: string, componentName: string, fields: Record<string, unknown>, nodeThreeType: string}>}
123
+ */
124
+ export function extractComponentBindings(json) {
125
+ const nodes = /** @type {Array<Record<string, unknown>>} */ (Array.isArray(json.nodes) ? json.nodes : []);
126
+ /** @type {Array<{nodeName: string, nodePath: string, componentName: string, fields: Record<string, unknown>, nodeThreeType: string}>} */
127
+ const results = [];
128
+ /** @type {Record<string, number>} */
129
+ const namesUsed = {};
130
+
131
+ const { nameMap, pathMap } = buildNodePaths(nodes, namesUsed);
132
+
133
+ for (let i = 0; i < nodes.length; i++) {
134
+ const node = nodes[i];
135
+ if (!node || typeof node !== "object") continue;
136
+
137
+ const nodeName = nameMap.get(i);
138
+ if (!nodeName) continue;
139
+
140
+ const nodePath = pathMap.get(i) ?? nodeName;
141
+ const nodeThreeType = inferNodeThreeType(/** @type {Record<string, unknown>} */ (node));
142
+ const ext = node.extensions?.[NEEDLE_COMPONENTS_EXTENSION];
143
+
144
+ if (!ext || typeof ext !== "object") {
145
+ results.push({ nodeName, nodePath, componentName: "", fields: {}, nodeThreeType });
146
+ continue;
147
+ }
148
+
149
+ const lists = [
150
+ ...(Array.isArray(ext.components) ? ext.components : []),
151
+ ...(Array.isArray(ext.builtin_components) ? ext.builtin_components : []),
152
+ ];
153
+
154
+ const internalKeys = new Set([
155
+ "name", "type", "guid", "gameObject", "enabled",
156
+ "didAwake", "didStart", "transformHandle", "destroyCancellationToken",
157
+ ]);
158
+
159
+ for (const comp of lists) {
160
+ if (!comp || typeof comp !== "object") continue;
161
+ const componentName = typeof comp.name === "string" ? comp.name.trim() : "";
162
+ if (!componentName) continue;
163
+
164
+ /** @type {Record<string, unknown>} */
165
+ const fields = {};
166
+ for (const [k, v] of Object.entries(comp)) {
167
+ if (!internalKeys.has(k)) fields[k] = v;
168
+ }
169
+
170
+ results.push({ nodeName, nodePath, componentName, fields, nodeThreeType });
171
+ }
172
+ }
173
+
174
+ return results;
175
+ }
@@ -0,0 +1,114 @@
1
+ // @ts-check
2
+ /**
3
+ * GLB / glTF JSON chunk readers — local files and remote URLs.
4
+ *
5
+ * All functions return the parsed glTF JSON object or null on failure.
6
+ * Remote fetches use HTTP Range requests and cache by Last-Modified header.
7
+ */
8
+
9
+ import { openSync, readSync, closeSync, readFileSync } from 'fs';
10
+
11
+ /** @type {Map<string, { lastModified: string, json: Record<string, unknown> }>} */
12
+ const _remoteGlbCache = new Map();
13
+
14
+ /**
15
+ * Read the JSON chunk from a binary GLB file without loading the binary blob.
16
+ * @param {string} filePath
17
+ * @returns {Record<string, unknown> | null}
18
+ */
19
+ export function readGlbJsonChunk(filePath) {
20
+ const fd = openSync(filePath, "r");
21
+ try {
22
+ const header = Buffer.allocUnsafe(20);
23
+ const bytesRead = readSync(fd, header, 0, header.length, 0);
24
+ if (bytesRead < 20) return null;
25
+
26
+ const magic = header.readUInt32LE(0);
27
+ const version = header.readUInt32LE(4);
28
+ const chunkLength = header.readUInt32LE(12);
29
+ const chunkType = header.readUInt32LE(16);
30
+
31
+ if (magic !== 0x46546c67 || version !== 2) return null;
32
+ if (chunkType !== 0x4E4F534A) return null; // not JSON chunk
33
+
34
+ const jsonBuffer = Buffer.allocUnsafe(chunkLength);
35
+ const jsonBytesRead = readSync(fd, jsonBuffer, 0, chunkLength, 20);
36
+ if (jsonBytesRead < chunkLength) return null;
37
+
38
+ return JSON.parse(jsonBuffer.toString("utf8").replace(/\u0000+$/g, ""));
39
+ } catch (_e) {
40
+ return null;
41
+ } finally {
42
+ closeSync(fd);
43
+ }
44
+ }
45
+
46
+ /**
47
+ * @param {string} filePath
48
+ * @returns {Record<string, unknown> | null}
49
+ */
50
+ export function readGltfJsonFile(filePath) {
51
+ try {
52
+ return JSON.parse(readFileSync(filePath, "utf8"));
53
+ } catch (_e) {
54
+ return null;
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Fetch only the JSON chunk of a remote GLB using two HTTP Range requests.
60
+ * Uses `Last-Modified` header for caching — avoids re-parsing if unchanged.
61
+ * Returns `null` on any network/parse error or if Range is not supported.
62
+ *
63
+ * @param {string} url
64
+ * @returns {Promise<Record<string, unknown> | null>}
65
+ */
66
+ export async function readRemoteGlbJsonChunk(url) {
67
+ try {
68
+ const cached = _remoteGlbCache.get(url);
69
+
70
+ const headerRes = await fetch(url, { headers: { Range: "bytes=0-19" } });
71
+ if (!headerRes.ok) {
72
+ console.warn(`[needle:dts-generator] Remote GLB fetch failed (${headerRes.status}): ${url}`);
73
+ return null;
74
+ }
75
+
76
+ const lastModified = headerRes.headers.get("Last-Modified") ?? "";
77
+ if (cached && lastModified && cached.lastModified === lastModified) {
78
+ return cached.json;
79
+ }
80
+
81
+ const rangeSupported = headerRes.status === 206;
82
+ const headerBytes = Buffer.from(await headerRes.arrayBuffer());
83
+ if (headerBytes.length < 20) return null;
84
+
85
+ const magic = headerBytes.readUInt32LE(0);
86
+ const version = headerBytes.readUInt32LE(4);
87
+ const chunkLength = headerBytes.readUInt32LE(12);
88
+ const chunkType = headerBytes.readUInt32LE(16);
89
+
90
+ if (magic !== 0x46546c67 || version !== 2) return null; // not a GLB
91
+ if (chunkType !== 0x4E4F534A) return null; // chunk0 not JSON
92
+
93
+ if (!rangeSupported) {
94
+ console.warn(`[needle:dts-generator] Remote GLB server does not support Range requests, skipping: ${url}`);
95
+ return null;
96
+ }
97
+
98
+ const jsonEnd = 20 + chunkLength - 1;
99
+ const jsonRes = await fetch(url, { headers: { Range: `bytes=20-${jsonEnd}` } });
100
+ if (!jsonRes.ok) {
101
+ console.warn(`[needle:dts-generator] Remote GLB JSON chunk fetch failed (${jsonRes.status}): ${url}`);
102
+ return null;
103
+ }
104
+
105
+ const json = /** @type {Record<string, unknown>} */ (
106
+ JSON.parse(Buffer.from(await jsonRes.arrayBuffer()).toString("utf8").replace(/\u0000+$/g, ""))
107
+ );
108
+ _remoteGlbCache.set(url, { lastModified, json });
109
+ return json;
110
+ } catch (e) {
111
+ console.warn(`[needle:dts-generator] Failed to fetch remote GLB: ${url} —`, /** @type {any} */ (e)?.message ?? e);
112
+ return null;
113
+ }
114
+ }
@@ -0,0 +1,36 @@
1
+ // @ts-check
2
+ /**
3
+ * Needle Engine — HTML binding DTS generator
4
+ *
5
+ * Scans all GLB/glTF files in the project's assets directory, extracts
6
+ * NEEDLE_components data, and emits a `needle-bindings.d.ts` virtual-module
7
+ * declaration so that TypeScript can type-check HTML ↔ 3D component bindings.
8
+ *
9
+ * Typical generated output:
10
+ *
11
+ * declare module "needle:bindings" {
12
+ * interface SceneData {
13
+ * Sphere: {
14
+ * MyBall: { speed: number; label: string; };
15
+ * };
16
+ * }
17
+ * }
18
+ *
19
+ * How component field types are resolved:
20
+ * - For built-in Needle Engine components, types are read from
21
+ * `components.needle.json` which lists only @serializable fields with
22
+ * their proper TypeScript types.
23
+ * - For user-defined components (not in the manifest), types are inferred
24
+ * from the JSON value in the GLB (number/string/boolean → typed, else → unknown).
25
+ * - Known Three.js types (Color, Vector3, Object3D, …) are emitted as
26
+ * `import("three").TypeName` and known Needle types (RGBAColor, AssetReference, …)
27
+ * as `import("@needle-tools/engine").TypeName`.
28
+ * - Truly unknown types fall back to `unknown`.
29
+ */
30
+
31
+ export { resolveEntrypointGlbs, collectSceneFiles } from './glb.discovery.js';
32
+ export { readGlbJsonChunk, readGltfJsonFile, readRemoteGlbJsonChunk } from './glb.reader.js';
33
+ export { extractComponentBindings, sanitizeNodeName, inferNodeThreeType, inferTsType } from './glb.extractor.js';
34
+ export { scanBindings } from './dts.scan.js';
35
+ export { generateDts, generateHtmlCustomData } from './dts.codegen.js';
36
+ export { generateBindingsDts } from './dts.writer.js';